diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml index 361e9a6f53..0d0c1ff974 100644 --- a/packages/bundle/sdk-minimal/cordis.patch.yml +++ b/packages/bundle/sdk-minimal/cordis.patch.yml @@ -33,6 +33,11 @@ - id: sandbox name: '@deepseek-ai/dsh-sandbox-local' + # Shared projection registry: sandbox-policy and terminal-bash fold + # sandbox-mode state through its units and require it as a hard injection. + - id: session-projection + name: '@deepseek-ai/dsh-session-projection' + - id: sandbox-policy name: '@deepseek-ai/dsh-sandbox-policy' config: diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index 42940a0ac8..6046e0bb48 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -50,6 +50,7 @@ "@deepseek-ai/dsh-sdk-jsonrpc-server": "workspace:^", "@deepseek-ai/dsh-session-log-deepseek": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", + "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-subprocess-local": "workspace:^", "@deepseek-ai/dsh-terminal": "workspace:^", "@deepseek-ai/dsh-terminal-bash": "workspace:^", diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts index 79461caad6..e570ccf7e7 100644 --- a/packages/core/agent/src/types.ts +++ b/packages/core/agent/src/types.ts @@ -34,7 +34,8 @@ export type InboxTarget = 'next-turn' | 'next-step' * Reader contract: the key is registered by `dsh-agent-loop` and absent * otherwise. Without agent-loop no turn events exist, so readers treat an * absent key as "no open turn / no boundaries" — capability absence, not a - * corrupt state — and never treat it as an error. + * corrupt state. A reader whose behavior has no safe fallback for that + * absence (the step-open decision, for example) may fail loud instead. */ export interface TurnBoundaryProjection { /** Seq of the open turn's `turn/start`, or null between turns. */ diff --git a/packages/e2b/e2b/package.json b/packages/e2b/e2b/package.json index e8e979c880..5cc2b71375 100644 --- a/packages/e2b/e2b/package.json +++ b/packages/e2b/e2b/package.json @@ -50,6 +50,7 @@ "@deepseek-ai/dsh-lsp-stdio": "workspace:^", "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-subprocess-e2b": "workspace:^", "@deepseek-ai/dsh-terminal": "workspace:^", "@deepseek-ai/dsh-terminal-bash": "workspace:^", diff --git a/packages/e2b/e2b/tests/composition.e2e.ts b/packages/e2b/e2b/tests/composition.e2e.ts index 55dcf93b08..dbbdff065e 100644 --- a/packages/e2b/e2b/tests/composition.e2e.ts +++ b/packages/e2b/e2b/tests/composition.e2e.ts @@ -14,6 +14,7 @@ import { import TerminalSessionService, { TerminalSessionId } from '@deepseek-ai/dsh-terminal' import { BashTerminalBackend } from '@deepseek-ai/dsh-terminal-bash' import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { Session, SessionId } from '@deepseek-ai/dsh-session' import E2BSubprocessRuntime from '@deepseek-ai/dsh-subprocess-e2b' @@ -52,6 +53,7 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => { runtimeRoot: '/home/user/.dsh-e2b', getSandbox: async () => sandbox, } as never) + await ctx.plugin(SessionProjectionRegistry) const sandboxPolicyFiber = await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/home/user', diff --git a/packages/e2b/e2b/tests/fixtures/composition/cordis.yml b/packages/e2b/e2b/tests/fixtures/composition/cordis.yml index 618baa8e10..6177630e25 100644 --- a/packages/e2b/e2b/tests/fixtures/composition/cordis.yml +++ b/packages/e2b/e2b/tests/fixtures/composition/cordis.yml @@ -21,6 +21,9 @@ - id: agents name: '@deepseek-ai/dsh-agent' +- id: session-projection + name: '@deepseek-ai/dsh-session-projection' + - id: sandbox-policy name: '@deepseek-ai/dsh-sandbox-policy' config: diff --git a/packages/interaction/permission-presets/src/index.ts b/packages/interaction/permission-presets/src/index.ts index 11822ce4af..67a9401018 100644 --- a/packages/interaction/permission-presets/src/index.ts +++ b/packages/interaction/permission-presets/src/index.ts @@ -189,13 +189,6 @@ export class PermissionPresetService extends Service { onChange: () => {}, }) - ctx.on('session/created', (session) => { - this.pinInitialPermission(session) - }) - for (const session of ctx.sessions.list()) { - this.pinInitialPermission(session) - } - // zod `.optional()` types the key `string | undefined` while the domain // says `description?: string`; on the JSON wire the two serialize // identically (absent), so the cast records exactly that @@ -209,18 +202,25 @@ export class PermissionPresetService extends Service { currentValue: zod.string().min(1), }) as unknown as zod.ZodType // The `permissions` projection unit folds the three whole-value knob - // events; it registers through the projection registry. - ctx.inject(['sessionProjections'], (projectionCtx) => { - projectionCtx.sessionProjections.register({ - key: 'permissions', - stateVersion: 1, - stateSchema: knobStateSchema, - init: () => EMPTY_KNOBS, - apply: applyKnobEvent, - wire: { viewSchema: selectSchema, view: state => this.selectFor(state) }, - }) + // events. `sessionProjections` is a hard injection, so the registration is + // synchronous and lands before the sweep below; otherwise the sweep would + // read an unregistered key and treat every existing session as fresh. + ctx.sessionProjections.register({ + key: 'permissions', + stateVersion: 1, + stateSchema: knobStateSchema, + init: () => EMPTY_KNOBS, + apply: applyKnobEvent, + wire: { viewSchema: selectSchema, view: state => this.selectFor(state) }, }) + ctx.on('session/created', (session) => { + this.pinInitialPermission(session) + }) + for (const session of ctx.sessions.list()) { + this.pinInitialPermission(session) + } + // The /permission command: the one write path a web client uses (the // popup contribution submits the picked preset as this line). The child // activates only when a command registry is composed. diff --git a/packages/interaction/permission-presets/tests/permission-presets.spec.ts b/packages/interaction/permission-presets/tests/permission-presets.spec.ts index 251a917dde..062ae525fe 100644 --- a/packages/interaction/permission-presets/tests/permission-presets.spec.ts +++ b/packages/interaction/permission-presets/tests/permission-presets.spec.ts @@ -267,6 +267,31 @@ describe('new-session default', () => { expect(ctx.permissionPresets.current(existing)).toBe('workspace-write') }) + it('preserves existing knob overrides when the service remounts over a knob-bearing session', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + ctx.provide('shell', { + sandboxMode: 'workspace-write', + resolve() { throw new Error('permission tests do not execute bash') }, + run() { throw new Error('permission tests do not execute bash') }, + start() { throw new Error('permission tests do not execute bash') }, + }) + ctx.provide('approval', { config: { policy: 'ask' } }) + const existing = ctx.sessions.create(SessionId('existing-knobs')) + existing.append('sandbox/mode', { mode: 'read-only' }) + existing.append('approval/policy', { policy: 'never' }) + + await ctx.plugin(PermissionPresetService, {}) + // The remount sweep must read the folded knob events instead of treating + // the session as fresh; no default preset events may overwrite the + // overrides (read-only + never matches no preset table entry). + expect(existing.events.map(event => event.type)).toEqual([ + 'sandbox/mode', 'approval/policy', + ]) + expect(ctx.permissionPresets.current(existing)).toBe(CUSTOM_PRESET) + }) + it('fills only missing legacy facts and preserves an unmatched seeded combination', async () => { const ctx = await mountedStore() const partial = freshSession('partial-source') diff --git a/packages/llm/token-meter/src/index.ts b/packages/llm/token-meter/src/index.ts index cbdfbff05f..e33c70b1df 100644 --- a/packages/llm/token-meter/src/index.ts +++ b/packages/llm/token-meter/src/index.ts @@ -10,6 +10,7 @@ import { BlockAssembler, deepFreeze } from '@deepseek-ai/dsh-llm' import type { LlmImageRequestPricing, Message, TokenUsage } from '@deepseek-ai/dsh-llm' import type { EpochHeader, Session, SessionEvent } from '@deepseek-ai/dsh-session' import { canonicalHeader, headerEquals, isSurfaceEvent } from '@deepseek-ai/dsh-session' +// Type-only: activates the `ctx.sessionProjections` Context declaration. import type {} from '@deepseek-ai/dsh-session-projection' import type { TokenMeasurement, diff --git a/packages/plan/plan-mode/README.i18n.yaml b/packages/plan/plan-mode/README.i18n.yaml index 31b11275ad..e9dbbde222 100644 --- a/packages/plan/plan-mode/README.i18n.yaml +++ b/packages/plan/plan-mode/README.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 packages/plan/plan-mode/README.md -README.md: 14c24507accb642909c1c9ed050368d9fa9120f1 -README.zh.md: 716f22a7b5bf7970b1eae429155d669805ce7d00 +README.md: 063e91e40fb55ea24e4fbfcefc984cd581d065b3 +README.zh.md: 8da8a7b3e01c42e619f882105dfe9e99b891a918 diff --git a/packages/plan/plan-mode/README.md b/packages/plan/plan-mode/README.md index 14c24507ac..063e91e40f 100644 --- a/packages/plan/plan-mode/README.md +++ b/packages/plan/plan-mode/README.md @@ -82,7 +82,7 @@ Plan mode is a product package, not a capability seam: there is no swappable bac ### Durable state and step-boundary appends -The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next accepted in-turn pre-step — the only append point while an agent runs — and an append failure cannot block the turn. The `set`/`get`/`foldPlanMode` helpers and their exact return states live in [`src/index.ts`](src/index.ts). +The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next accepted in-turn pre-step — the only append point while an agent runs — and an append failure cannot block the turn. The `set`/`get` service methods and their exact return states live in [`src/index.ts`](src/index.ts); the registered `plan` projection unit folds the same state for readers. ### The `/plan` command @@ -94,7 +94,7 @@ The command child activates only when a commands service is composed. It maps ba ### Session projection unit -When `ctx.sessionProjections` is composed, the package registers the `plan` unit under an injected child. The unit turns logged `/plan` command runs into a candidate target, commits the logged state on `plan/mode`, and derives `{ active, pending }` for `view`, where `pending` is true only while an unsettled or successful selection differs from the logged state — a pure replay quantity recoverable from the log alone. The key merges into `SessionProjectionMap` from [`src/types.ts`](src/types.ts); the framework drives the unit, and unloading the plugin fiber unregisters the key. +The package requires `ctx.sessionProjections` and registers the `plan` unit on activation. The unit turns logged `/plan` command runs into a candidate target, commits the logged state on `plan/mode`, and derives `{ active, pending }` for `view`, where `pending` is true only while an unsettled or successful selection differs from the logged state — a pure replay quantity recoverable from the log alone. The key merges into `SessionProjectionMap` from [`src/types.ts`](src/types.ts); the framework drives the unit, and unloading the plugin fiber unregisters the key. ### Source map diff --git a/packages/plan/plan-mode/README.zh.md b/packages/plan/plan-mode/README.zh.md index 716f22a7b5..8da8a7b3e0 100644 --- a/packages/plan/plan-mode/README.zh.md +++ b/packages/plan/plan-mode/README.zh.md @@ -82,7 +82,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并 ### 持久状态与步骤边界追加 -本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有轮次开启时,模式变更会立即追加;轮次开启期间,它保持待生效,直到下一个被接受的轮内 pre-step——agent 运行时唯一的追加点——且追加失败不能阻塞轮次。`set`/`get`/`foldPlanMode` 辅助函数及其确切返回状态见 [`src/index.ts`](src/index.ts)。 +本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有轮次开启时,模式变更会立即追加;轮次开启期间,它保持待生效,直到下一个被接受的轮内 pre-step——agent 运行时唯一的追加点——且追加失败不能阻塞轮次。`set`/`get` 服务方法及其确切返回状态见 [`src/index.ts`](src/index.ts);已注册的 `plan` 投影单元为读取方折叠同一状态。 ### `/plan` 命令 @@ -94,7 +94,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并 ### 会话投影单元 -组合 `ctx.sessionProjections` 时,本包在一个注入的子插件中注册 `plan` 单元。该单元把已记录的 `/plan` 命令运行转为候选目标,在 `plan/mode` 上提交已记录状态,并为 `view` 推导 `{ active, pending }`,其中 `pending` 仅在未结算或已成功的选择与已记录状态不同时为 true——这是仅凭日志即可恢复的纯回放量。key 由 [`src/types.ts`](src/types.ts) 的声明合并加入 `SessionProjectionMap`;框架负责驱动该单元,卸载插件 fiber 会注销该 key。 +本包要求 `ctx.sessionProjections` 并在激活时注册 `plan` 单元。该单元把已记录的 `/plan` 命令运行转为候选目标,在 `plan/mode` 上提交已记录状态,并为 `view` 推导 `{ active, pending }`,其中 `pending` 仅在未结算或已成功的选择与已记录状态不同时为 true——这是仅凭日志即可恢复的纯回放量。key 由 [`src/types.ts`](src/types.ts) 的声明合并加入 `SessionProjectionMap`;框架负责驱动该单元,卸载插件 fiber 会注销该 key。 ### 源码地图 diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts index 64a820a43b..e9db30b748 100644 --- a/packages/plan/plan-mode/src/index.ts +++ b/packages/plan/plan-mode/src/index.ts @@ -113,12 +113,15 @@ export function resolveConfig(config: PlanModeConfig): PlanModeConfig { return { section } } -const planUnitStateSchema = zod.object({ +const planUnitStateSchema: ZodType = zod.object({ active: zod.boolean(), wanted: zod.boolean().nullable(), - running: zod.object({ commandId: zod.string() as unknown as ZodType, wanted: zod.boolean() }).nullable(), + running: zod.object({ + commandId: zod.string() as unknown as ZodType, + wanted: zod.boolean(), + }).strict().nullable(), activeAtLastHeader: zod.boolean().nullable(), -}) +}).strict() /** Wire payload schema of the `plan` projection. */ const planProjectionSchema: ZodType = zod.object({ diff --git a/packages/sandbox/sandbox-policy/src/index.ts b/packages/sandbox/sandbox-policy/src/index.ts index 5f3fa72382..0c65356c5b 100644 --- a/packages/sandbox/sandbox-policy/src/index.ts +++ b/packages/sandbox/sandbox-policy/src/index.ts @@ -2,7 +2,9 @@ * The sandbox POLICY home (`ctx.sandboxPolicy`): the single owner of the * deployment's sandbox fallbacks plus per-session resolution: the file-effect * {@link SandboxMode}, the `workspace-write` root, and the override kit (the - * `sandbox/mode` event, its fold, and its write path, from `./session-mode.ts`). + * `sandbox/mode` event, its fold, and its write path; the fold is the + * `sandboxMode` session-projection unit registered here, while the event and + * write path come from `./session-mode.ts`). * Before each agent request, the owner also contributes the resolved policy to * the cache-safe runtime-context snapshot. The agent loop logs that snapshot as * model history, so replay reconstructs the same mode and root the enforcing diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index 5ebd76f1fc..cf984610dd 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.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 packages/session/session-projection/README.md -README.md: 95ee09236966c8cbfdaf79cdcb717e04ee0ec367 -README.zh.md: 0402fbe8e3c56e44f7597bb14fb0379843ecdbca +README.md: 2ef45b42390227d4f5367ea2a4c6023b9e24b5d8 +README.zh.md: 59f0849f5d79b32bb1b5a11ca25aa9af2593ee7f diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index 95ee092369..2ef45b4239 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -29,7 +29,7 @@ Mount `dsh-session-projection` wherever client carriers need current values of l ### When to choose it -Choose it when a domain keeps state that clients should see without re-deriving it — a todo list, a goal snapshot, conversation statistics. The registry drives units eagerly over committed events, so any registered unit's value is current by construction. Skip it for host-only bookkeeping that no client reads: a unit without a `wire` block stays host-only, and headless assemblies without the registry are unaffected. +Choose it when a domain keeps state that clients should see without re-deriving it — a todo list, a goal snapshot, conversation statistics. The registry drives units eagerly over committed events, so any registered unit's value is current by construction. Skip it for host-only bookkeeping that no client reads: a unit without a `wire` block stays host-only. Unit contributors and readers declare `sessionProjections` in their plugin `inject`, so a composition without the registry fails activation instead of silently degrading. ### Define a projection unit diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index 0402fbe8e3..59f0849f5d 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" ### 何时选择 -当领域保存客户端应看到、但不应自行重新派生的状态——todo 清单、goal 快照、对话统计——时选择本包。注册表在已提交事件上主动驱动单元,因此任何已注册单元的值按构造即为当前值。当维护的是无客户端读取的 host-only 记账时跳过:不带 `wire` 块的单元保持 host-only,而不带注册表的 headless 组装不受影响。 +当领域保存客户端应看到、但不应自行重新派生的状态——todo 清单、goal 快照、对话统计——时选择本包。注册表在已提交事件上主动驱动单元,因此任何已注册单元的值按构造即为当前值。当维护的是无客户端读取的 host-only 记账时跳过:不带 `wire` 块的单元保持 host-only。单元贡献方与读取方在插件 `inject` 中声明 `sessionProjections`,因此不带注册表的组合会在激活时失败,而不是静默降级。 ### 定义投影单元 diff --git a/packages/session/session-projection/src/index.ts b/packages/session/session-projection/src/index.ts index 2b870d7bef..1ddcb5cd09 100644 --- a/packages/session/session-projection/src/index.ts +++ b/packages/session/session-projection/src/index.ts @@ -171,9 +171,10 @@ interface Registration { * older than the registry, folds `init` over the in-memory log on first * touch (event or read). Registration is an effect (disposer rides the * calling fiber): an unloaded domain plugin's key disappears from snapshots - * and clients read it as capability absence. Domain - * plugins register under `ctx.inject(['sessionProjections'], …)` so headless - * assemblies without the registry stay unaffected. Registrants sharing a key + * and clients read it as capability absence. Unit contributors and host + * readers declare `sessionProjections` in their plugin `inject`, so a + * composition without the registry fails activation instead of silently + * degrading. Registrants sharing a key * share one unit and are counted: the same tool package mounted in N agent * presets registers N times, and the key survives until the last one * unloads. diff --git a/packages/session/session-title/README.i18n.yaml b/packages/session/session-title/README.i18n.yaml index 80797b9264..af1afb4989 100644 --- a/packages/session/session-title/README.i18n.yaml +++ b/packages/session/session-title/README.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 packages/session/session-title/README.md -README.md: 12335afc1eccd7c9b9f4a33d72549df296cbfb1b -README.zh.md: e61297ed555c93273efc108221cb0141a9c997b7 +README.md: debc737e3f572f15856f7f78a58abe8532ae15dc +README.zh.md: cf8079cd5566023f3dc1da8480eeec5327eebb5a diff --git a/packages/session/session-title/README.md b/packages/session/session-title/README.md index 12335afc1e..debc737e3f 100644 --- a/packages/session/session-title/README.md +++ b/packages/session/session-title/README.md @@ -58,7 +58,7 @@ One optional asynchronous provider may be registered through `ctx.sessionTitle.r ### Reading titles -`get(session)` folds the latest accepted title from the live or replayed log, and `foldSessionTitle(events)` is the pure fold over a log. The service also registers a `title` projection unit — the plain title string — for client list rows when a projection registry is composed. An explicit `refresh(session)` materializes the fallback when needed, then explicitly runs the registered provider over the current eligible messages. +`get(session)` reads the latest folded title from one live or replayed session, and `foldSessionTitle(events)` is the pure fold over a log. The service requires `ctx.sessionProjections` and registers two units: the client-visible `title` unit (the accepted title string for client list rows) and the host-only `titleInput` unit, which folds the first and latest eligible messages plus their count so scheduling and fallback reads are O(1) through `stateOf()`; the full eligible prefix for one provider generation is scanned from the session log at execution time. An explicit `refresh(session)` materializes the fallback when needed, then explicitly runs the registered provider over the current eligible messages. ### Failures and recovery diff --git a/packages/session/session-title/README.zh.md b/packages/session/session-title/README.zh.md index e61297ed55..cf8079cd55 100644 --- a/packages/session/session-title/README.zh.md +++ b/packages/session/session-title/README.zh.md @@ -58,7 +58,7 @@ kind: "package-reference" ### 读取标题 -`get(session)` 从活跃或回放日志折叠最新已接受标题,`foldSessionTitle(events)` 是对日志的纯折叠。服务还会在组合了投影注册表时注册一个 `title` 投影单元——纯标题字符串——供客户端列表行使用。显式 `refresh(session)` 在需要时物化回退,然后对当前符合条件的消息显式运行已注册提供方。 +`get(session)` 从活跃或回放会话读取折叠出的最新标题,`foldSessionTitle(events)` 是对日志的纯折叠。服务要求 `ctx.sessionProjections` 并注册两个单元:客户端可见的 `title` 单元(供客户端列表行使用的已接受标题字符串)和仅供 host 使用的 `titleInput` 单元——后者折叠第一条与最新一条合格消息及其计数,使调度与回退读取通过 `stateOf()` 达到 O(1);某次提供方生成所需的完整合格前缀,则会在执行时从会话日志中扫描取得。显式 `refresh(session)` 在需要时物化回退,然后对当前符合条件的消息显式运行已注册提供方。 ### 失败与恢复 diff --git a/packages/subagent/subagent/src/projection.ts b/packages/subagent/subagent/src/projection.ts index 49ee4af239..09ac8ae078 100644 --- a/packages/subagent/subagent/src/projection.ts +++ b/packages/subagent/subagent/src/projection.ts @@ -41,7 +41,6 @@ const timingStateSchema: z.ZodType = z.object({ settledMs: z.number().int().nonnegative(), active: activeIntervalSchema.optional(), pendingTurnStart: z.number().int().nonnegative().optional(), - /** Whether the fold has crossed a descriptor in this logical log. */ descriptorSeen: z.boolean(), }).strict() diff --git a/packages/terminal/terminal-bash/tests/index.spec.ts b/packages/terminal/terminal-bash/tests/index.spec.ts index e9a992064c..148f9e37bd 100644 --- a/packages/terminal/terminal-bash/tests/index.spec.ts +++ b/packages/terminal/terminal-bash/tests/index.spec.ts @@ -354,6 +354,7 @@ describe('BashTerminalBackend startup rollback', () => { it('bootstraps a pwsh dialect through the prompt function and scrubs bash-only env', async () => { const ctx = new Context() await ctx.plugin(EmptySandbox) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/workspace' }) let spawned: SubprocessTerminalSpawnSpec | undefined let sent: TerminalSendRequest | undefined @@ -391,6 +392,7 @@ describe('BashTerminalBackend startup rollback', () => { it('keeps waiting for stdin_read when the first settled output only echoes the prompt literal', async () => { const ctx = new Context() await ctx.plugin(EmptySandbox) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/workspace' }) const sends: TerminalSendRequest[] = [] const session = { @@ -425,6 +427,7 @@ describe('BashTerminalBackend startup rollback', () => { it('rejects a pwsh bootstrap whose shell exits or times out', async () => { const ctx = new Context() await ctx.plugin(EmptySandbox) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/workspace' }) const sessionFor = (waitReason: TerminalWaitReason): LocalPtySession => ({ startSend: () => ({ @@ -449,6 +452,7 @@ describe('BashTerminalBackend startup rollback', () => { try { const ctx = new Context() await ctx.plugin(EmptySandbox) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/workspace' }) const pending = Promise.withResolvers<{ viewport: string @@ -501,6 +505,7 @@ describe('BashTerminalBackend startup rollback', () => { it('forwards the spawn signal into the pwsh bootstrap sends', async () => { const ctx = new Context() await ctx.plugin(EmptySandbox) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/workspace' }) const sends: TerminalSendRequest[] = [] const session = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b85e1fbb6..ac905a1c8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1345,6 +1345,9 @@ importers: '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ version: link:../../session/session-persistence-jsonl + '@deepseek-ai/dsh-session-projection': + specifier: workspace:^ + version: link:../../session/session-projection '@deepseek-ai/dsh-subprocess-local': specifier: workspace:^ version: link:../../subprocess/subprocess-local @@ -4453,6 +4456,9 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session + '@deepseek-ai/dsh-session-projection': + specifier: workspace:^ + version: link:../../session/session-projection '@deepseek-ai/dsh-subprocess-e2b': specifier: workspace:^ version: link:../subprocess-e2b