Răsfoiți Sursa

fix(goal): emit bare GoalRef in fold lastRef and goal/changed notifications

goalChangeRef returned the full GoalSnapshot for every snapshot operation,
so foldGoal(...).lastRef and the goal/changed notification ref carried
objective, phase, and maxGoalRounds fields instead of the declared
GoalRef { id, revision }. Only the clear tombstone was bare. Emit an exact
{ id, revision } ref for snapshot changes and pin the contract with a
regression test covering create/edit/block notifications and the fold.
Tianyi Cui 1 lună în urmă
părinte
comite
23610c6abd
2 a modificat fișierele cu 23 adăugiri și 2 ștergeri
  1. 3 1
      packages/goal/goal/src/fold.ts
  2. 20 1
      packages/goal/goal/tests/goal.spec.ts

+ 3 - 1
packages/goal/goal/src/fold.ts

@@ -261,7 +261,9 @@ function validateSnapshotTransition(
  * @returns stable identity used to reconcile a deferred change with its log event.
  */
 export function goalChangeRef(change: GoalChangeMeta): GoalRef {
-  return change.operation === 'clear' ? change.cleared : change.goal
+  return change.operation === 'clear'
+    ? change.cleared
+    : { id: change.goal.id, revision: change.goal.revision }
 }
 
 /**

+ 20 - 1
packages/goal/goal/tests/goal.spec.ts

@@ -11,7 +11,7 @@ import GoalService, {
   foldGoal,
   renderGoalChange,
 } from '@deepseek-ai/dsh-goal'
-import type { GoalChangeMeta, GoalRef, GoalSnapshotChangeMeta } from '@deepseek-ai/dsh-goal'
+import type { GoalChangeMeta, GoalChanged, GoalRef, GoalSnapshotChangeMeta } from '@deepseek-ai/dsh-goal'
 
 type DeferredInjection = UserMessage
 
@@ -381,6 +381,25 @@ describe('GoalService mutations', () => {
     expect(next.id).not.toBe(goal.id)
   })
 
+  it('emits bare compare-and-set refs in folded lastRef and goal/changed notifications', async () => {
+    const { ctx, agent, session } = await harness()
+    const seen: GoalChanged['ref'][] = []
+    ctx.on('goal/changed', (_subject, change) => { seen.push(change.ref) })
+    const created = ctx.goals.create(agent, { objective: 'bare refs', maxGoalRounds: 3 })
+    const edited = ctx.goals.edit(agent, created, { objective: 'bare refs edited' })
+    const blocked = ctx.goals.block(agent, edited, { code: 'bare-blocker', message: 'Bare refs.' })
+    // GoalRef is exactly { id, revision }: every notification ref must be bare.
+    for (const ref of seen) {
+      expect(Object.keys(ref).sort()).toEqual(['id', 'revision'])
+      expect(ref).toEqual({ id: created.id, revision: ref.revision })
+    }
+    expect(seen).toHaveLength(3)
+    // The durable fold's lastRef is the same bare ref, not a full snapshot.
+    const folded = foldGoal(session.events)
+    expect(folded.lastRef).toEqual({ id: blocked.id, revision: blocked.revision })
+    expect(Object.keys(folded.lastRef as object).sort()).toEqual(['id', 'revision'])
+  })
+
   it('keeps per-goal mutation timestamps monotonic when the wall clock moves backward', async () => {
     vi.useFakeTimers()
     vi.setSystemTime(100)