Browse Source

fix: revert the projection-registration form per review

Per the imccyu review, the pre-existing goal, permissions, and plan units
go back to registering through the ctx.inject(['sessionProjections'], …)
child form instead of the required-inject direct register; goal returns to
zero diff (its service never reads projections). The mandatory-seam rework
for these three sites moves to a follow-up PR. New projection units keep
the required-inject direct-register form.

Regenerated catalog and subsystem docs follow the reverted service
signatures.
_Kerman 1 month ago
parent
commit
2a4f6541d6

+ 2 - 2
docs/subsystems/goal.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/goal.md
-goal.md: e5c8d77e7a897b097e7801f65024612d233319a9
-goal.zh.md: a4c8f26173a1ed4b991beaa02de7894258f50b6d
+goal.md: 676b2f49d00681ac7f05b894cda0157ebba4cfcd
+goal.zh.md: 1d46bc2c9ba1f3b35026ff132d323e46ccd3bc19

+ 1 - 1
docs/subsystems/goal.md

@@ -247,7 +247,7 @@ block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView
 
 Types: [Agent](core.md)
 
-Source: [`packages/goal/goal/src/index.ts:182`](../../packages/goal/goal/src/index.ts)
+Source: [`packages/goal/goal/src/index.ts:183`](../../packages/goal/goal/src/index.ts)
 
 <a id="goal-events"></a>
 

+ 1 - 1
docs/subsystems/goal.zh.md

@@ -247,7 +247,7 @@ block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView
 
 Types: [Agent](core.md)
 
-Source: [`packages/goal/goal/src/index.ts:182`](../../packages/goal/goal/src/index.ts)
+Source: [`packages/goal/goal/src/index.ts:183`](../../packages/goal/goal/src/index.ts)
 
 <a id="goal-events"></a>
 

+ 2 - 2
docs/subsystems/session-projection.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md
-session-projection.md: a0704206904880259425b6b32b648d383d815fb2
-session-projection.zh.md: 74392356d980e83b1ca281732b916f7d9e678702
+session-projection.md: edc1d646c01cb95ccc090f69f3868e466c346fdf
+session-projection.zh.md: 9c10737ef8b6d409155039632da86f0573c05f13

+ 15 - 9
packages/goal/goal/src/index.ts

@@ -13,6 +13,7 @@ import { agentEvents } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
 import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol'
+// Type-only: resolves ctx.sessionProjections for the optional unit child.
 import type {} from '@deepseek-ai/dsh-session-projection'
 import {
   applyGoalEvent,
@@ -49,7 +50,7 @@ import type {
 // The pure payload outlet (./types.ts, ONE home of the `goal` projection-key
 // declaration) re-exported onto the package root keeps the module edge in
 // the emitted index.d.ts, so aggregate programs consuming the declarations
-// still receive the SessionProjectionStateMap merge.
+// still receive the SessionProjectionMap merge.
 export type * from './types.ts'
 export type * from './domain.ts'
 export { GOAL_CHANGE_VERSION, GoalError, GoalId } from './runtime.ts'
@@ -180,7 +181,7 @@ function resolveBlockReason(reason: unknown): GoalBlockReason {
 
 /** Goal service (`ctx.goals`) backed exclusively by the owning session log. */
 export class GoalService extends TypertRemoteService {
-  static inject = ['agents', 'sessionProjections']
+  static inject = ['agents']
 
   static Config: z<Config> = z.object({
     defaultMaxGoalRounds: z.number().default(256),
@@ -197,13 +198,18 @@ export class GoalService extends TypertRemoteService {
     ctx.on('agent/session-start', ({ agent }) => {
       this.cache(agent.session).activation = 'disarmed'
     })
-    ctx.sessionProjections.register<'goal', GoalProjection | null>({
-      key: 'goal',
-      stateVersion: 4,
-      stateSchema: goalProjectionSchema,
-      init: () => null,
-      apply: applyGoalProjection,
-      wire: { viewSchema: goalProjectionSchema, view: state => state },
+    // The `goal` projection unit: last-wins fold of goal/change whole values
+    // (see applyGoalProjection). The unit child activates only when a
+    // projection registry is composed (headless assemblies stay unaffected).
+    ctx.inject(['sessionProjections'], (projectionCtx) => {
+      projectionCtx.sessionProjections.register<'goal', GoalProjection | null>({
+        key: 'goal',
+        stateSchema: goalProjectionSchema,
+        init: () => null,
+        apply: applyGoalProjection,
+        wire: { viewSchema: goalProjectionSchema, view: state => state },
+        stateVersion: 4,
+      })
     })
   }
 

+ 0 - 9
packages/goal/goal/tests/goal.spec.ts

@@ -4,7 +4,6 @@ import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm'
 import SessionStore, { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session'
-import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import GoalService, {
   GoalError,
   GoalId,
@@ -61,7 +60,6 @@ function stubAgent(rawId: string, seed?: readonly import('@deepseek-ai/dsh-sessi
 async function harness(config: { defaultMaxGoalRounds?: number } = {}) {
   const ctx = new Context()
   await ctx.plugin(AgentRegistry)
-  await ctx.plugin(SessionProjectionRegistry)
   await ctx.plugin(GoalService, config)
   const stub = stubAgent(`goal-test-${Math.random()}`)
   ctx.agents.register(stub.agent)
@@ -133,7 +131,6 @@ describe('GoalService creation and replay', () => {
   it('also resolves the default when constructed directly without Cordis config normalization', async () => {
     const ctx = new Context()
     await ctx.plugin(AgentRegistry)
-    await ctx.plugin(SessionProjectionRegistry)
     const goals = new GoalService(ctx)
     const stub = stubAgent('goal-direct-construction')
     ctx.agents.register(stub.agent)
@@ -145,7 +142,6 @@ describe('GoalService creation and replay', () => {
   it('rejects invalid direct configuration', async () => {
     const ctx = new Context()
     await ctx.plugin(AgentRegistry)
-    await ctx.plugin(SessionProjectionRegistry)
     await expect(ctx.plugin(GoalService, { defaultMaxGoalRounds: -1 })).rejects.toThrow(expect.objectContaining({
       code: 'GOAL_INVALID_MAX_ROUNDS',
     }))
@@ -159,7 +155,6 @@ describe('GoalService creation and replay', () => {
 
     const ctx = new Context()
     await ctx.plugin(AgentRegistry)
-    await ctx.plugin(SessionProjectionRegistry)
     await ctx.plugin(GoalService)
     const resumed = stubAgent('seeded-goal', first.session.events)
     ctx.agents.register(resumed.agent)
@@ -174,7 +169,6 @@ describe('GoalService creation and replay', () => {
     const ctx = new Context()
     await ctx.plugin(SessionStore)
     await ctx.plugin(AgentRegistry)
-    await ctx.plugin(SessionProjectionRegistry)
     await ctx.plugin(GoalService)
     const parent = stubAgentForSession(ctx.sessions.create(SessionId('goal-fork-parent')))
     ctx.agents.register(parent.agent)
@@ -221,7 +215,6 @@ describe('GoalService creation and replay', () => {
   it('removes the service and its session-start listener with the providing fiber', async () => {
     const ctx = new Context()
     await ctx.plugin(AgentRegistry)
-    await ctx.plugin(SessionProjectionRegistry)
     const fiber = await ctx.plugin(GoalService)
     const first = ctx.goals
     const stub = stubAgent('goal-hmr')
@@ -430,7 +423,6 @@ describe('GoalService mutations', () => {
     const ctx = new Context()
     await ctx.plugin(SessionStore)
     await ctx.plugin(AgentRegistry)
-    await ctx.plugin(SessionProjectionRegistry)
     await ctx.plugin(GoalService)
     const stub = stubAgentForSession(ctx.sessions.create(SessionId('goal-reentrant-observer')))
     ctx.agents.register(stub.agent)
@@ -449,7 +441,6 @@ describe('GoalService mutations', () => {
   it('does not delegate goal persistence to agent injection', async () => {
     const ctx = new Context()
     await ctx.plugin(AgentRegistry)
-    await ctx.plugin(SessionProjectionRegistry)
     await ctx.plugin(GoalService)
     const stub = stubAgent('goal-independent-injection')
     stub.agent.inject = () => { throw new Error('injection must not be called') }

+ 9 - 1
packages/goal/goal/tests/projection.spec.ts

@@ -1,4 +1,12 @@
-/** Goal projection behavior. */
+/**
+ * The `goal` projection unit: mounting GoalService beside the registry
+ * serves the current whole goal on the history tail page with a consistent
+ * asOfSeq; before the first create the value is null; a clear tombstone
+ * returns it to null; a composition without the goal service has no `goal`
+ * key; unmounting drops it (HMR safety). Malformed goal-shaped events are
+ * ignored fail-soft (same-reference return) — strict replay validation
+ * belongs to the write side and foldGoal, never the projection drive.
+ */
 
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'

+ 11 - 7
packages/interaction/permission-presets/src/index.ts

@@ -208,13 +208,17 @@ export class PermissionPresetService extends Service {
       })),
       currentValue: zod.string().min(1),
     }) as unknown as zod.ZodType<PermissionSelect>
-    ctx.sessionProjections.register({
-      key: 'permissions',
-      stateVersion: 1,
-      stateSchema: knobStateSchema,
-      init: () => EMPTY_KNOBS,
-      apply: applyKnobEvent,
-      wire: { viewSchema: selectSchema, view: state => this.selectFor(state) },
+    // The `permissions` projection unit folds the three whole-value knob
+    // events; it registers through the projection registry.
+    ctx.inject(['sessionProjections'], (projectionCtx) => {
+      projectionCtx.sessionProjections.register({
+        key: 'permissions',
+        stateVersion: 1,
+        stateSchema: knobStateSchema,
+        init: () => EMPTY_KNOBS,
+        apply: applyKnobEvent,
+        wire: { viewSchema: selectSchema, view: state => this.selectFor(state) },
+      })
     })
 
     // The /permission command: the one write path a web client uses (the

+ 4 - 1
packages/plan/plan-mode/src/index.ts

@@ -217,7 +217,10 @@ export class PlanModeController extends Service {
       },
     })
 
-    ctx.sessionProjections.register(planProjectionDefinition)
+    // The `plan` projection unit registers through the projection registry.
+    ctx.inject(['sessionProjections'], (projectionCtx) => {
+      projectionCtx.sessionProjections.register(planProjectionDefinition)
+    })
 
     // The command child activates only when a command registry is composed.
     ctx.inject(['commands'], (commandCtx) => {