Skip to content

Slot engine: add per-run runId provenance + getSlotHistory() for repeat-run apps #156

Description

@JohnD-EE

Summary

Filed from a Daybreak leaf app (Reclaim Your Week — a facilitated, repeat-audit
programme, and Daybreak's first real end-to-end consumer). Two small, additive,
back-compatible extensions to the slot-value engine
(lib/framework/data-slots/values.ts) that any app with repeat runs of the same
journey
needs, and which are generic facilitation concerns rather than
app-specific. We've implemented them in our fork and would like Daybreak to adopt
them so we can delete our copy and delegate.

Motivation

The slot engine is insert-only and versioned: a new reading supersedes the prior
head, and getSlotHeads() returns the current picture (supersededAt IS NULL).
That's exactly right for "what does the system currently know about this user" — but
two things are missing for an app that runs the same journey more than once per
user (a quarterly audit, a periodic check-in, any recurring assessment):

  1. No per-run scoping on a value. Provenance records conversationId,
    nodeKey, moduleSlug, etc., but nothing ties a reading to a run of the
    journey. Without it, run 2's answer for a slug simply supersedes run 1's, and
    there is no way to attribute a version to the run that produced it.

  2. No way to read superseded versions. getSlotHeads() is head-only by design.
    A trend or before/after comparison across runs needs the full history of a
    slug, grouped by run — and the engine offers no per-user/per-slug history read.
    (admin-queries.ts composes cross-user head reads for the admin console; that's
    a different surface.)

Proposed change (additive, back-compatible, no migration)

Both land in lib/framework/data-slots/values.ts. provenance is already a Json
column, so the new field needs no schema/migration; every existing caller and test
compiles unchanged.

1. Optional runId on SlotValueProvenance:

export interface SlotValueProvenance {
  // ...existing fields...
  /**
   * The run this reading belongs to — a facilitation contextKey that scopes a
   * value to one journey run. A consuming app stamps it at its single write path
   * so a repeat run appends a fresh version of a slug under a new runId instead of
   * overwriting the prior run's picture; readers group a slug's history by it.
   * Optional and generic: the engine only stores it.
   */
  runId?: string;
}

2. getSlotHistory(userId, slotSlug) — the counterpart to getSlotHeads:

/**
 * Every version of one slug for one user — the current head AND every superseded
 * version — oldest first (`version` ascending, monotonic per (userId, slotSlug)).
 * Callers group by `provenance.runId` to line one run up against another. No
 * `supersededAt` filter — the superseded rows are the point. Access scoping
 * (`canRead`) wraps this the same way it wraps `getSlotHeads`.
 */
export async function getSlotHistory(userId: string, slotSlug: string): Promise<SlotValue[]> {
  return prisma.slotValue.findMany({
    where: { userId, slotSlug },
    orderBy: { version: 'asc' },
  });
}

Kept per-slug/per-user deliberately (mirrors getSlotHeads); a batched variant is a
further additive change if a hot path ever wants one.

Scope / non-goals

  • Purely additive: no behaviour change to appendSlotValue or getSlotHeads, no
    migration, no new table.
  • runId is stored, never interpreted, by the engine — the consuming app's write
    path decides whether to populate it.
  • framework:boundary and type-check stay green; the existing
    values.test.ts is extended (superseded rows returned, no supersededAt filter).

Happy to open a PR with the exact diff if that's the easier path to adopt.


Context: this is tracked on our side in the fork's daybreak-asks ledger; once
Daybreak lands it we'll drop our copy and delegate to yours.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions