From 2a4f6541d67077227573edcdfd3d74fa454f54c9 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 19 Aug 2026 17:46:02 +0800 Subject: [PATCH] fix: revert the projection-registration form per review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/subsystems/goal.i18n.yaml | 4 ++-- docs/subsystems/goal.md | 2 +- docs/subsystems/goal.zh.md | 2 +- docs/subsystems/session-projection.i18n.yaml | 4 ++-- packages/goal/goal/src/index.ts | 24 ++++++++++++------- packages/goal/goal/tests/goal.spec.ts | 9 ------- packages/goal/goal/tests/projection.spec.ts | 10 +++++++- .../permission-presets/src/index.ts | 18 ++++++++------ packages/plan/plan-mode/src/index.ts | 5 +++- 9 files changed, 45 insertions(+), 33 deletions(-) diff --git a/docs/subsystems/goal.i18n.yaml b/docs/subsystems/goal.i18n.yaml index 3e48766d14..df731a5dba 100644 --- a/docs/subsystems/goal.i18n.yaml +++ b/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 diff --git a/docs/subsystems/goal.md b/docs/subsystems/goal.md index e5c8d77e7a..676b2f49d0 100644 --- a/docs/subsystems/goal.md +++ b/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) diff --git a/docs/subsystems/goal.zh.md b/docs/subsystems/goal.zh.md index a4c8f26173..1d46bc2c9b 100644 --- a/docs/subsystems/goal.zh.md +++ b/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) diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index 33f7103a9e..5331ba0581 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/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 diff --git a/packages/goal/goal/src/index.ts b/packages/goal/goal/src/index.ts index 045c773bcd..baf8c921a9 100644 --- a/packages/goal/goal/src/index.ts +++ b/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 = 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, + }) }) } diff --git a/packages/goal/goal/tests/goal.spec.ts b/packages/goal/goal/tests/goal.spec.ts index 1eb0c90d2e..e94e0754a0 100644 --- a/packages/goal/goal/tests/goal.spec.ts +++ b/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') } diff --git a/packages/goal/goal/tests/projection.spec.ts b/packages/goal/goal/tests/projection.spec.ts index e5fb642e84..6282a6f739 100644 --- a/packages/goal/goal/tests/projection.spec.ts +++ b/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' diff --git a/packages/interaction/permission-presets/src/index.ts b/packages/interaction/permission-presets/src/index.ts index 67b7f523ca..11822ce4af 100644 --- a/packages/interaction/permission-presets/src/index.ts +++ b/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 - 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 diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts index 676baef697..4a8924d0b7 100644 --- a/packages/plan/plan-mode/src/index.ts +++ b/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) => {