Bläddra i källkod

refactor(session): trim projection migration diff

_Kerman 1 vecka sedan
förälder
incheckning
6717cb8d19

+ 2 - 2
docs/config-catalog.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/config-catalog.md
-config-catalog.md: 88a7d34b5ed72c1e7535d62f3b9163ea7d677756
-config-catalog.zh.md: f4ad394528dcde48a7c1c499acaece82ada894f4
+config-catalog.md: b7bc65cc2f2111ac549067f31518c18cf636c9b9
+config-catalog.zh.md: f934799dbab07d394732447ae2a4cd4184e10d61

+ 2 - 2
docs/config-catalog.md

@@ -1508,7 +1508,7 @@ export interface PresetSpec {
 
 Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsystems/sandbox.md)
 
-Source: [`packages/interaction/permission-presets/src/index.ts:134`](../packages/interaction/permission-presets/src/index.ts)
+Source: [`packages/interaction/permission-presets/src/index.ts:143`](../packages/interaction/permission-presets/src/index.ts)
 
 <a id="deepseek-aidsh-persona"></a>
 
@@ -1992,7 +1992,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/session/session-title/src/index.ts:53`](../packages/session/session-title/src/index.ts)
+Source: [`packages/session/session-title/src/index.ts:54`](../packages/session/session-title/src/index.ts)
 
 <a id="deepseek-aidsh-session-title-all-prompts-llm"></a>
 

+ 2 - 2
docs/config-catalog.zh.md

@@ -1510,7 +1510,7 @@ export interface PresetSpec {
 
 依赖:[`ApprovalPolicy`](subsystems/approval.zh.md) · [`SandboxMode`](subsystems/sandbox.zh.md)
 
-来源:[`packages/interaction/permission-presets/src/index.ts:134`](../packages/interaction/permission-presets/src/index.ts)
+来源:[`packages/interaction/permission-presets/src/index.ts:143`](../packages/interaction/permission-presets/src/index.ts)
 
 <a id="deepseek-aidsh-persona"></a>
 
@@ -1994,7 +1994,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/session/session-title/src/index.ts:53`](../packages/session/session-title/src/index.ts)
+来源:[`packages/session/session-title/src/index.ts:54`](../packages/session/session-title/src/index.ts)
 
 <a id="deepseek-aidsh-session-title-all-prompts-llm"></a>
 

+ 2 - 2
docs/event-producer-consumer.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/event-producer-consumer.md
-event-producer-consumer.md: 12efe17c1c02fa744c556996ed6e2bc9bf07deb9
-event-producer-consumer.zh.md: 108a8d1838cbd872137111735b35fe25e89303dc
+event-producer-consumer.md: cb215c9e50902cac7eaa07f6bb294f798bcf848a
+event-producer-consumer.zh.md: ccf1fb91259ac63e019c82d2b4d283f3213ab991

+ 1 - 1
docs/event-producer-consumer.md

@@ -77,7 +77,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 
 | Event string | Dispatchers | Listeners |
 | --- | --- | --- |
-| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) |
+| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) |
 | `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `modules`, `webserver` |
 | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
 | `internal/status` | - | [`agent`](../packages/core/agent) |

+ 1 - 1
docs/event-producer-consumer.zh.md

@@ -79,7 +79,7 @@
 
 | 事件字符串 | 派发方 | 监听方 |
 | --- | --- | --- |
-| `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) |
+| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) |
 | `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `modules`, `webserver` |
 | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
 | `internal/status` | - | [`agent`](../packages/core/agent) |

+ 2 - 2
docs/persistence-catalog.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/persistence-catalog.md
-persistence-catalog.md: 857d3783d57decf99596527ad3b8cc5f344be616
-persistence-catalog.zh.md: 591a3951f094d470b4522fe22250154a0d1cf293
+persistence-catalog.md: 29d07f674c2ebd054b0aafa47f729504807460bc
+persistence-catalog.zh.md: 4c191e6f01a7513df2723502a88f3f973b7596e3

+ 2 - 2
docs/persistence-catalog.md

@@ -519,7 +519,7 @@ Source: [`packages/api/session-controller/src/types.ts:40`](../packages/api/sess
 'permission/preset': { preset: string }
 ```
 
-Source: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/interaction/permission-presets/src/index.ts)
+Source: [`packages/interaction/permission-presets/src/index.ts:53`](../packages/interaction/permission-presets/src/index.ts)
 
 ### `plan/*`
 
@@ -662,7 +662,7 @@ Source: [`packages/core/session/src/types.ts:324`](../packages/core/session/src/
 
 Types: [SessionTitleEventData](subsystems/session-title.md)
 
-Source: [`packages/session/session-title/src/index.ts:74`](../packages/session/session-title/src/index.ts)
+Source: [`packages/session/session-title/src/index.ts:75`](../packages/session/session-title/src/index.ts)
 
 <a id="sessiontitle-llm-request--log-only"></a>
 

+ 2 - 2
docs/persistence-catalog.zh.md

@@ -521,7 +521,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 'permission/preset': { preset: string }
 ```
 
-来源:[`packages/interaction/permission-presets/src/index.ts:59`](../packages/interaction/permission-presets/src/index.ts)
+来源:[`packages/interaction/permission-presets/src/index.ts:53`](../packages/interaction/permission-presets/src/index.ts)
 
 ### `plan/*`
 
@@ -664,7 +664,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 类型:[SessionTitleEventData](subsystems/session-title.zh.md)
 
-来源:[`packages/session/session-title/src/index.ts:74`](../packages/session/session-title/src/index.ts)
+来源:[`packages/session/session-title/src/index.ts:75`](../packages/session/session-title/src/index.ts)
 
 <a id="sessiontitle-llm-request--log-only"></a>
 

+ 2 - 2
docs/subsystems/sandbox.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/sandbox.md
-sandbox.md: 8afa1d2ef2673b240938f5c70c8f58f8e16c16df
-sandbox.zh.md: 6ee68d7d26cdcaed6f62a27fecfc3604e4d4f7a5
+sandbox.md: 7160ac699a21a319c991ae70262a8bf61da86b0c
+sandbox.zh.md: 6d2b10d63a0e180bc0fe39cc47930e66dee72466

+ 1 - 1
docs/subsystems/sandbox.md

@@ -192,7 +192,7 @@ Source: [`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox
 
 ### `ctx.sandboxPolicy` — `SandboxPolicyService`
 
-The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log.
+The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
 
 ```ts cordis-catalog
 /**

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

@@ -192,7 +192,7 @@ Source: [`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox
 
 ### `ctx.sandboxPolicy` — `SandboxPolicyService`
 
-The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log.
+The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
 
 ```ts cordis-catalog
 /**

+ 3 - 5
packages/context/time-context/src/index.ts

@@ -39,7 +39,7 @@ const timeContextStateSchema = zod.object({
 })
 
 /** Folded time-context readings. */
-export type TimeContextProjection = zod.infer<typeof timeContextStateSchema>
+type TimeContextProjection = zod.infer<typeof timeContextStateSchema>
 
 /** The agent registry that owns pre-step processing. */
 export const inject = ['agents', 'sessionProjections']
@@ -182,15 +182,13 @@ export function apply(ctx: Context, config: Config): void {
     const decision = await next()
     if (decision.kind === 'reject' || signal.aborted) return decision
     const now = Date.now()
-    const state = ctx.sessionProjections.stateOf(agent.session, 'timeContext')
-    if (state !== undefined && refreshIntervalMs !== undefined && refreshIntervalMs > 0) {
+    const state = ctx.sessionProjections.stateOf(agent.session, 'timeContext') as TimeContextProjection
+    if (refreshIntervalMs !== undefined && refreshIntervalMs > 0) {
       const lastInjection = state.lastInjectionTime
       if (lastInjection != null
         && now >= lastInjection
         && now - lastInjection < refreshIntervalMs) return decision
     }
-    /* v8 ignore next 2 -- time-context registers its own unit in apply, so the key is always present */
-    if (state === undefined) return decision
     /* v8 ignore next 6 -- every later step follows a recorded injection in the same turn */
     const previous = step === 1
       ? state.lastMessageTime ?? undefined

+ 1 - 1
packages/context/tmux-context/src/index.ts

@@ -241,7 +241,7 @@ export function apply(ctx: Context, config: Config): void {
     if (decision.kind === 'reject' || signal.aborted || step !== 1) return decision
     const bash = ctx.get('shell')
     if (bash === undefined) return decision
-    const previous = ctx.sessionProjections.stateOf(agent.session, 'tmuxContext') ?? null
+    const previous = ctx.sessionProjections.stateOf(agent.session, 'tmuxContext') as TmuxContextState
     if (refreshIntervalMs !== undefined && refreshIntervalMs > 0 && previous !== null) {
       const now = Date.now()
       if (now >= previous.time && now - previous.time < refreshIntervalMs) return decision

+ 21 - 3
packages/experimental/agent-team/src/invariant.ts

@@ -1,7 +1,13 @@
 /** Agent Teams runtime invariant companion. */
 
 import type { Context } from '@deepseek-ai/cordis'
-import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
+import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
+import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
+import {
+  isTeamEvent,
+  teamProjectionDefinition,
+  type TeamProjectionState,
+} from './projection.ts'
 
 const PACKAGE_NAME = '@deepseek-ai/dsh-experimental-agent-team'
 
@@ -10,8 +16,20 @@ export const name = 'team-invariant'
 /** Invariant registry required by the companion. */
 export const inject = ['invariants']
 
-/** No runtime invariant: the Team projection owns event decoding and relational state transitions. */
-const install: InvariantInstaller = () => {}
+/** Validate candidate Team events against the projected committed prefix. */
+const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
+  ctx.on('internal/dispatch', (_mode, eventName, args) => {
+    if (eventName !== 'session/event') return
+    const [session, event] = args as [Session, SessionEvent]
+    /* v8 ignore next -- non-Team Session events have no Agent Teams invariant. */
+    if (!isTeamEvent(event)) return
+    const state = ctx.sessionProjections.stateOf(session, 'agentTeam') as TeamProjectionState
+    const candidate = teamProjectionDefinition.apply(structuredClone(state), event)
+    if (candidate.failure !== undefined) {
+      fail(`session event ${event.seq} violates the Agent Teams stream: ${candidate.failure}`)
+    }
+  }, { global: true })
+}, { inject: ['sessionProjections'] })
 
 /** Register the package invariant companion. */
 export const apply = (ctx: Context): Promise<() => void> =>

+ 2 - 2
packages/experimental/agent-team/src/projection.ts

@@ -186,7 +186,7 @@ type TeamSessionEvent = SessionEvent<TeamEventType>
  * @param event - candidate Session event.
  * @returns whether the event has a Team-owned type.
  */
-function isTeamEvent(event: SessionEvent): event is TeamSessionEvent {
+export function isTeamEvent(event: SessionEvent): event is TeamSessionEvent {
   return event.type === 'team/member'
     || event.type === 'team/task'
     || event.type === 'team/message/queued'
@@ -220,8 +220,8 @@ function parseCurrentTeamEvent(event: TeamSessionEvent): TeamSessionEvent {
 }
 
 function applyProjectionEvent(state: TeamProjectionState, event: SessionEvent): void {
-  if (!isTeamEvent(event)) return
   if (state.failure !== undefined) return
+  if (!isTeamEvent(event)) return
   try {
     const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data)
     if (selector.teamId !== state.id) return

+ 74 - 0
packages/experimental/agent-team/tests/invariant.spec.ts

@@ -0,0 +1,74 @@
+import { describe, expect, it } from 'vitest'
+import { Context } from '@deepseek-ai/cordis'
+import InvariantService, { InvariantError } from '@deepseek-ai/dsh-invariants'
+import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
+import * as TeamInvariant from '../src/invariant.ts'
+import { teamProjectionDefinition } from '../src/projection.ts'
+import { TeamId, TeamTaskId } from '../src/types.ts'
+
+async function setup(): Promise<Context> {
+  const ctx = new Context()
+  await ctx.plugin(SessionStore)
+  await ctx.plugin(SessionProjectionRegistry)
+  ctx.sessionProjections.register(teamProjectionDefinition)
+  await ctx.plugin(InvariantService, { enabled: true })
+  await ctx.plugin(TeamInvariant)
+  return ctx
+}
+
+describe('Agent Teams stream invariant', () => {
+  it('accepts provisioning and rejects a terminal member as the first edge', async () => {
+    const ctx = await setup()
+    const session = ctx.sessions.create(SessionId('team-invariant'))
+    const member = {
+      id: SessionId('team-invariant-child'),
+      name: 'worker',
+      description: 'worker responsibility',
+      provider: 'spawn',
+      context: 'fresh' as const,
+      phase: 'provisioning' as const,
+    }
+    expect(() => {
+      session.append('team/member', { version: 1, teamId: TeamId(session.id), member })
+    }).not.toThrow()
+
+    const invalid = ctx.sessions.create(SessionId('team-invariant-invalid'))
+    expect(() => {
+      invalid.append('team/member', {
+        version: 1,
+        teamId: TeamId(invalid.id),
+        member: { ...member, phase: 'active' },
+      })
+    }).toThrow(expect.objectContaining<Partial<InvariantError>>({
+      code: 'INVARIANT',
+      packageName: '@deepseek-ai/dsh-experimental-agent-team',
+    }))
+    expect(invalid.events).toEqual([])
+  })
+
+  it('rejects an invalid task dependency before publication', async () => {
+    const ctx = await setup()
+    const session = ctx.sessions.create(SessionId('team-task-invariant'))
+
+    expect(() => {
+      session.append('team/task', {
+        version: 1,
+        teamId: TeamId(session.id),
+        task: {
+          id: TeamTaskId('task-1'),
+          revision: 1,
+          subject: 'invalid dependency',
+          description: 'references a missing blocker',
+          status: 'pending',
+          blockedBy: [TeamTaskId('missing')],
+          writeScopes: [],
+        },
+      })
+    }).toThrow(expect.objectContaining<Partial<InvariantError>>({
+      code: 'INVARIANT',
+      packageName: '@deepseek-ai/dsh-experimental-agent-team',
+    }))
+    expect(session.events).toEqual([])
+  })
+})

+ 2 - 2
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -1290,8 +1290,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
   },
   {
     key: 'sandboxPolicy',
-    summary: 'The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log.',
-    description: 'The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log.',
+    summary: 'The sandbox-policy service (`ctx.sandboxPolicy`).',
+    description: 'The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session\'s mode log and immutable cwd travel together to every enforcing capability.',
     methods: [
       {
         signature: 'readonly defaultMode: SandboxMode',

+ 3 - 3
packages/hooks/hooks-claude-code/src/index.ts

@@ -12,7 +12,7 @@
 import { readFileSync } from 'node:fs'
 import type { Context } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
-import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
+import type { Agent, PreStepDecision, TurnBoundaryProjection } from '@deepseek-ai/dsh-agent'
 import type {} from '@deepseek-ai/dsh-session-projection'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
@@ -310,8 +310,8 @@ const SUBAGENT_TYPE = 'general-purpose'
 /** The last open turn number in the agent's log, or 0 without an agent. */
 function lastTurn(ctx: Context, agent: Agent | undefined): number {
   if (!agent) return 0
-  /* v8 ignore next -- agent-present hook points run inside AgentLoop, which owns this projection. */
-  return ctx.sessionProjections.stateOf(agent.session, 'turnBoundary')?.lastTurn ?? 0
+  const boundary = ctx.sessionProjections.stateOf(agent.session, 'turnBoundary') as TurnBoundaryProjection
+  return boundary.lastTurn
 }
 
 /** Flatten content blocks to the text a hook payload carries (the common case). */

+ 22 - 13
packages/interaction/permission-presets/src/index.ts

@@ -25,7 +25,7 @@ import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-sett
 // Type-only: resolves the optional projection and command children.
 import type {} from '@deepseek-ai/dsh-session-projection'
 import type {} from '@deepseek-ai/dsh-commands'
-import type { KnobState, PermissionSelect, PresetOption } from './types.ts'
+import type { PermissionSelect, PresetOption } from './types.ts'
 
 export type * from './types.ts'
 
@@ -35,12 +35,6 @@ declare module '@deepseek-ai/cordis' {
   }
 }
 
-/** Projection state for permission overrides and constructor-seed provenance. */
-interface PermissionProjectionState extends KnobState {
-  /** Whether the log contains a constructor-seed boundary. */
-  seeded: boolean
-}
-
 declare module '@deepseek-ai/dsh-session-projection/types' {
   interface SessionProjectionStateMap {
     /** Latest logged permission overrides and constructor-seed provenance. */
@@ -81,8 +75,26 @@ export const CUSTOM_PRESET = 'custom'
 /** Settings namespace carrying the default for future sessions. */
 export const PERMISSION_SETTINGS_NAMESPACE = settingsNamespace('permission')
 
-const permissionStateSchema: zod.ZodType<PermissionProjectionState> = zod.object({
+/**
+ * The projection unit's knob state: the last seen value of each knob event,
+ * null before an override (composition defaults apply at view time).
+ */
+export interface KnobState {
   /** Last `permission/preset` payload, or null. */
+  preset: string | null
+  /** Last `sandbox/mode` payload, or null. */
+  sandbox: SandboxMode | null
+  /** Last `approval/policy` payload, or null. */
+  approval: ApprovalPolicy | null
+}
+
+/** Projection state for permission overrides and constructor-seed provenance. */
+interface PermissionProjectionState extends KnobState {
+  /** Whether the log contains a constructor-seed boundary. */
+  seeded: boolean
+}
+
+const permissionStateSchema: zod.ZodType<PermissionProjectionState> = zod.object({
   preset: zod.string().nullable(),
   sandbox: zod.union([
     zod.literal('read-only'),
@@ -96,9 +108,6 @@ const permissionStateSchema: zod.ZodType<PermissionProjectionState> = zod.object
 /** State for the empty log: every knob at its composition default. */
 const EMPTY_KNOBS: KnobState = { preset: null, sandbox: null, approval: null }
 
-/** Projection state for a genuinely fresh session. */
-const EMPTY_PERMISSION_STATE: PermissionProjectionState = { ...EMPTY_KNOBS, seeded: false }
-
 /**
  * One-event permission-state transition (the projection unit's `apply`). Unrelated
  * events return the same reference — the registry's change gate.
@@ -118,7 +127,7 @@ function applyPermissionEvent(
     case 'approval/policy':
       return { ...state, approval: event.data.policy }
     case 'session/end-seed':
-      return state.seeded ? state : { ...state, seeded: true }
+      return { ...state, seeded: true }
     default:
       return state
   }
@@ -230,7 +239,7 @@ export class PermissionPresetService extends Service {
         key: 'permissions',
         stateVersion: 2,
         stateSchema: permissionStateSchema,
-        init: () => EMPTY_PERMISSION_STATE,
+        init: () => ({ ...EMPTY_KNOBS, seeded: false }),
         apply: applyPermissionEvent,
         wire: { viewSchema: selectSchema, view: state => this.selectFor(state) },
       })

+ 3 - 15
packages/interaction/permission-presets/src/types.ts

@@ -2,15 +2,13 @@
  * Pure types of the permission domain: the ONE home of the `permissions`
  * projection-key declaration plus its payload types, free of this package's
  * host-side value imports (cordis, schemastery). Two namespace projections
- * serve it — the package root re-export for host consumers and `./client` for
- * client aggregates — with zero content duplication.
+ * serve it — the package root re-export for host consumers, `./client` (the
+ * browser half-entry's re-export) for client aggregates — with zero content
+ * duplication.
  *
  * @module @deepseek-ai/dsh-permission-presets/types
  */
 
-import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
-import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
-
 /** The select-option shape a presentation layer advertises for one preset (or for the derived `custom` state). */
 export interface PresetOption {
   /** Stable option value: the table key, or `custom`. */
@@ -33,16 +31,6 @@ export interface PermissionSelect {
   currentValue: string
 }
 
-/** Latest logged permission overrides. */
-export interface KnobState {
-  /** Last `permission/preset` payload, or null. */
-  preset: string | null
-  /** Last `sandbox/mode` payload, or null. */
-  sandbox: SandboxMode | null
-  /** Last `approval/policy` payload, or null. */
-  approval: ApprovalPolicy | null
-}
-
 declare module '@deepseek-ai/dsh-session-projection/types' {
   interface SessionProjectionMap {
     /**

+ 2 - 2
packages/llm/llm-retry/src/index.ts

@@ -217,8 +217,8 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna
     }
 
     const policyKey = retryPolicyKey(policy)
-    const retryState = ctx.sessionProjections.stateOf(agent.session, 'llmRetry')
-    const previous = retryState?.[retryStateKey(provider, policyKey)]
+    const retryState = ctx.sessionProjections.stateOf(agent.session, 'llmRetry') as LlmRetryState
+    const previous = retryState[retryStateKey(provider, policyKey)]
     const previousRetry = previous?.retry ?? 0
     if (policy.mode === 'normal' && previousRetry >= policy.maxRetries) return next()
     const retry = previousRetry + 1

+ 6 - 7
packages/sandbox/sandbox-policy/src/index.ts

@@ -85,12 +85,6 @@ export interface SandboxPolicyRequest {
   mode?: SandboxMode
 }
 
-/**
- * The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
- * default mode, fallback workspace root, and current request-time policy
- * section. Tool layers call {@link resolve} for each execution so a session's
- * mode log and immutable cwd travel together to every enforcing capability.
- */
 /** The sandbox-mode projection's state schema (state equals the public shape). */
 const sandboxModeStateSchema = zod.union([
   zod.literal('read-only'),
@@ -106,7 +100,12 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
   }
 }
 
-/** The sandbox policy seam: the deployment default mode and workspace-write root, with per-session overrides folded from the log. */
+/**
+ * The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment
+ * default mode, fallback workspace root, and current request-time policy
+ * section. Tool layers call {@link resolve} for each execution so a session's
+ * mode log and immutable cwd travel together to every enforcing capability.
+ */
 export class SandboxPolicyService extends Service {
   // Inline schema call: the config catalog walks `static Config` statically.
   static Config: z<Config> = z.object({

+ 10 - 13
packages/session/session-title/src/index.ts

@@ -7,6 +7,7 @@ import { Context, FiberState, Service, type Fiber } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
 import { z as zod } from 'zod'
 import type { ZodType } from 'zod'
+import type { Branded } from '@deepseek-ai/dsh-brand'
 import { assertNever, deepFreeze, isAgentLoopRequest } from '@deepseek-ai/dsh-llm'
 import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
 import type {
@@ -35,8 +36,8 @@ import type {
   TitleProjection,
 } from './types.ts'
 
-/** Branded session-title provider identity. */
-export type SessionTitleProviderId = import('./types.ts').SessionTitleProviderId
+/** Identifies one session-title provider registration. */
+export type SessionTitleProviderId = Branded<'SessionTitleProviderId'>
 
 /**
  * Brand a raw provider id.
@@ -192,15 +193,12 @@ function assertPositiveInteger(name: keyof Config, value: number): void {
   }
 }
 
-/** The title unit's folded state — the projection's full public face. */
-export type TitleUnitState = TitleProjection
-
 /**
  * Convert title projection state into an immutable snapshot.
  * @param state - the title unit's folded state.
  * @returns the immutable snapshot.
  */
-export function titleSnapshotFromState(state: TitleUnitState): SessionTitleSnapshot {
+function titleSnapshotFromState(state: TitleProjection): SessionTitleSnapshot {
   return deepFreeze({
     title: state.title,
     messageSeqs: [...state.messageSeqs],
@@ -272,7 +270,7 @@ const titleProjectionSchema = zod.object({
   ]),
   eventSeq: zod.number().int().nonnegative(),
   updatedAt: zod.number(),
-}).nullable() as unknown as ZodType<TitleUnitState | null>
+}).nullable() as unknown as ZodType<TitleProjection | null>
 
 const titleViewSchema: ZodType<string | null> = zod.string().min(1).nullable()
 
@@ -295,7 +293,7 @@ export const titleProjectionDefinition = {
     viewSchema: titleViewSchema,
     view: state => state?.title ?? null,
   },
-} satisfies ProjectionDefinition<'title', TitleUnitState | null>
+} satisfies ProjectionDefinition<'title', TitleProjection | null>
 
 /**
  * Fold the latest title from a session log.
@@ -303,7 +301,7 @@ export const titleProjectionDefinition = {
  * @returns the immutable latest title snapshot, or `undefined`.
  */
 export function foldSessionTitle(events: readonly SessionEvent[]): SessionTitleSnapshot | undefined {
-  let state: TitleUnitState | null = titleProjectionDefinition.init()
+  let state: TitleProjection | null = titleProjectionDefinition.init()
   for (const event of events) state = titleProjectionDefinition.apply(state, event)
   return state === null ? undefined : titleSnapshotFromState(state)
 }
@@ -400,8 +398,8 @@ export class SessionTitleService extends Service {
    * @returns latest title snapshot, or `undefined` before eligible input.
    */
   get(session: Session): SessionTitleSnapshot | undefined {
-    const state = this.ctx.sessionProjections.stateOf(session, 'title')
-    return state === null || state === undefined ? undefined : titleSnapshotFromState(state)
+    const state = this.ctx.sessionProjections.stateOf(session, 'title') as TitleProjection | null
+    return state === null ? undefined : titleSnapshotFromState(state)
   }
 
   /**
@@ -734,8 +732,7 @@ export class SessionTitleService extends Service {
   }
 
   private titleInputOf(session: Session): TitleInputState {
-    /* v8 ignore next -- session-title registers its own titleInput unit, so the key is always present */
-    return this.ctx.sessionProjections.stateOf(session, 'titleInput') ?? EMPTY_TITLE_INPUT
+    return this.ctx.sessionProjections.stateOf(session, 'titleInput') as TitleInputState
   }
 
   /** Queue detached service work and retain it through service disposal. */

+ 1 - 1
packages/subagent/tool-subagent/src/model-selection-state.ts

@@ -46,7 +46,7 @@ export const subagentModelSelectionProjectionDefinition = {
     if (allowedModels.length === 0) {
       throw new Error('subagent/model-selection-policy requires at least one route')
     }
-    return allowedModels.map(route => ({ ...route }))
+    return allowedModels
   },
 } satisfies ProjectionDefinition<'subagentModelSelectionPolicy', AllowedModelRoute[] | null>