diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index ab6c2a4598..76b19b8953 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/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
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 88a7d34b5e..b7bc65cc2f 100644
--- a/docs/config-catalog.md
+++ b/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)
@@ -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)
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index f4ad394528..f934799dba 100644
--- a/docs/config-catalog.zh.md
+++ b/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)
@@ -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)
diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml
index c6569b3349..24e448b40d 100644
--- a/docs/event-producer-consumer.i18n.yaml
+++ b/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
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index 12efe17c1c..cb215c9e50 100644
--- a/docs/event-producer-consumer.md
+++ b/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) |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index 108a8d1838..ccf1fb9125 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/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) |
diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml
index a94e12b812..7f40c918d7 100644
--- a/docs/persistence-catalog.i18n.yaml
+++ b/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
diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md
index 857d3783d5..29d07f674c 100644
--- a/docs/persistence-catalog.md
+++ b/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)
diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md
index 591a3951f0..4c191e6f01 100644
--- a/docs/persistence-catalog.zh.md
+++ b/docs/persistence-catalog.zh.md
@@ -521,7 +521,7 @@ export type SessionEvent = {
'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 = {
类型:[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)
diff --git a/docs/subsystems/sandbox.i18n.yaml b/docs/subsystems/sandbox.i18n.yaml
index 6cec1e9dff..735c78927d 100644
--- a/docs/subsystems/sandbox.i18n.yaml
+++ b/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
diff --git a/docs/subsystems/sandbox.md b/docs/subsystems/sandbox.md
index 8afa1d2ef2..7160ac699a 100644
--- a/docs/subsystems/sandbox.md
+++ b/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
/**
diff --git a/docs/subsystems/sandbox.zh.md b/docs/subsystems/sandbox.zh.md
index 6ee68d7d26..6d2b10d63a 100644
--- a/docs/subsystems/sandbox.zh.md
+++ b/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
/**
diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts
index c551b83f58..8a1e7880c6 100644
--- a/packages/context/time-context/src/index.ts
+++ b/packages/context/time-context/src/index.ts
@@ -39,7 +39,7 @@ const timeContextStateSchema = zod.object({
})
/** Folded time-context readings. */
-export type TimeContextProjection = zod.infer
+type TimeContextProjection = zod.infer
/** 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
diff --git a/packages/context/tmux-context/src/index.ts b/packages/context/tmux-context/src/index.ts
index cc4c136076..bfcec10247 100644
--- a/packages/context/tmux-context/src/index.ts
+++ b/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
diff --git a/packages/experimental/agent-team/src/invariant.ts b/packages/experimental/agent-team/src/invariant.ts
index 90a2076bd6..dc5fd3ddc1 100644
--- a/packages/experimental/agent-team/src/invariant.ts
+++ b/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> =>
diff --git a/packages/experimental/agent-team/src/projection.ts b/packages/experimental/agent-team/src/projection.ts
index bb23ecc65e..0a873718e6 100644
--- a/packages/experimental/agent-team/src/projection.ts
+++ b/packages/experimental/agent-team/src/projection.ts
@@ -186,7 +186,7 @@ type TeamSessionEvent = SessionEvent
* @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
diff --git a/packages/experimental/agent-team/tests/invariant.spec.ts b/packages/experimental/agent-team/tests/invariant.spec.ts
new file mode 100644
index 0000000000..ba12a07c74
--- /dev/null
+++ b/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 {
+ 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>({
+ 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>({
+ code: 'INVARIANT',
+ packageName: '@deepseek-ai/dsh-experimental-agent-team',
+ }))
+ expect(session.events).toEqual([])
+ })
+})
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index 7ae4f780a5..df7c13bf5c 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/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',
diff --git a/packages/hooks/hooks-claude-code/src/index.ts b/packages/hooks/hooks-claude-code/src/index.ts
index 4dfa3a58f1..e82b728a8e 100644
--- a/packages/hooks/hooks-claude-code/src/index.ts
+++ b/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). */
diff --git a/packages/interaction/permission-presets/src/index.ts b/packages/interaction/permission-presets/src/index.ts
index 5d6eef92f9..b6025fd98b 100644
--- a/packages/interaction/permission-presets/src/index.ts
+++ b/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 = 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 = zod.object({
preset: zod.string().nullable(),
sandbox: zod.union([
zod.literal('read-only'),
@@ -96,9 +108,6 @@ const permissionStateSchema: zod.ZodType = 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) },
})
diff --git a/packages/interaction/permission-presets/src/types.ts b/packages/interaction/permission-presets/src/types.ts
index 9447ea5cb5..a978e3b5e8 100644
--- a/packages/interaction/permission-presets/src/types.ts
+++ b/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 {
/**
diff --git a/packages/llm/llm-retry/src/index.ts b/packages/llm/llm-retry/src/index.ts
index a5d013ba29..89ffc14c40 100644
--- a/packages/llm/llm-retry/src/index.ts
+++ b/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
diff --git a/packages/sandbox/sandbox-policy/src/index.ts b/packages/sandbox/sandbox-policy/src/index.ts
index 0c65356c5b..efc61572e6 100644
--- a/packages/sandbox/sandbox-policy/src/index.ts
+++ b/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 = z.object({
diff --git a/packages/session/session-title/src/index.ts b/packages/session/session-title/src/index.ts
index a72f8baa6b..9c22e3c65f 100644
--- a/packages/session/session-title/src/index.ts
+++ b/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
+}).nullable() as unknown as ZodType
const titleViewSchema: ZodType = 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. */
diff --git a/packages/subagent/tool-subagent/src/model-selection-state.ts b/packages/subagent/tool-subagent/src/model-selection-state.ts
index 3ef592f5d7..d3b04b828b 100644
--- a/packages/subagent/tool-subagent/src/model-selection-state.ts
+++ b/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>