refactor(session): trim projection migration diff

This commit is contained in:
_Kerman
2026-08-27 17:18:51 +08:00
parent d9c0aa1cd0
commit 6717cb8d19
25 changed files with 170 additions and 87 deletions
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
@@ -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> =>
@@ -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
@@ -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([])
})
})
@@ -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',
@@ -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). */
@@ -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) },
})
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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. */
@@ -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>