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):
-
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.
-
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.
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 samejourney 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):
No per-run scoping on a value. Provenance records
conversationId,nodeKey,moduleSlug, etc., but nothing ties a reading to a run of thejourney. 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.
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.tscomposes cross-user head reads for the admin console; that'sa different surface.)
Proposed change (additive, back-compatible, no migration)
Both land in
lib/framework/data-slots/values.ts.provenanceis already aJsoncolumn, so the new field needs no schema/migration; every existing caller and test
compiles unchanged.
1. Optional
runIdonSlotValueProvenance:2.
getSlotHistory(userId, slotSlug)— the counterpart togetSlotHeads:Kept per-slug/per-user deliberately (mirrors
getSlotHeads); a batched variant is afurther additive change if a hot path ever wants one.
Scope / non-goals
appendSlotValueorgetSlotHeads, nomigration, no new table.
runIdis stored, never interpreted, by the engine — the consuming app's writepath decides whether to populate it.
framework:boundaryandtype-checkstay green; the existingvalues.test.tsis extended (superseded rows returned, nosupersededAtfilter).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-asksledger; onceDaybreak lands it we'll drop our copy and delegate to yours.