From 571c6025d56f827c0aeed55e0c9b8a9fa9f974ff Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Thu, 9 Jul 2026 13:01:14 +0800 Subject: [PATCH] fix: address master merge gate failures --- docs/config-catalog.md | 36 +++++++++++++++++++++++++ packages/README.md | 12 ++++----- packages/spill/spill-local/src/store.ts | 16 ++++++++++- packages/spill/spill/src/types.ts | 7 ++++- packages/util/timeout/README.md | 17 ++++++++---- 5 files changed, 75 insertions(+), 13 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 2429fe3163..c4a982e12a 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -474,6 +474,40 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' Source: [`packages/session-persistence/session-persistence-sqlite/src/index.ts:50`](../packages/session-persistence/session-persistence-sqlite/src/index.ts) +## `@deepseek-ai/dsh-spill-local` + +```ts config-catalog +/** Plugin config (all optional — `static Config` supplies the defaults). */ +export interface Config { + /** + * Root directory for spill files. Omitted uses a lazily-created private + * (0700) per-process directory under the OS temp dir — the safe default for + * a local deployment. Set it to keep spill files under a known location. + */ + root?: string +} +``` + +Source: [`packages/spill/spill-local/src/index.ts:22`](../packages/spill/spill-local/src/index.ts) + +## `@deepseek-ai/dsh-spill-policy` + +Requires: `tools` + +```ts config-catalog +/** Plugin config. */ +export interface Config { + /** + * The model-facing context cap for a plain-text tool result, in UTF-8 bytes. + * Omitted disables the policy entirely (no-op). When set, a result larger than + * this is spilled and replaced with a preview derived from this same budget. + */ + maxInlineBytes?: number +} +``` + +Source: [`packages/spill/spill-policy/src/index.ts:45`](../packages/spill/spill-policy/src/index.ts) + ## `@deepseek-ai/dsh-stdio-agent` ```ts config-catalog @@ -879,6 +913,7 @@ Abstract service classes — a deployment loads a concrete implementation packag - `@deepseek-ai/dsh-compact` — abstract `CompactService` ([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts)) - `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts)) - `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session-persistence/session-persistence/src/index.ts`](../packages/session-persistence/session-persistence/src/index.ts)) +- `@deepseek-ai/dsh-spill` — abstract `SpillFiles` ([`packages/spill/spill/src/index.ts`](../packages/spill/spill/src/index.ts)) ## Library packages (no plugin entry) @@ -888,5 +923,6 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-app-boot` ([`packages/ui/app-boot/src/index.ts`](../packages/ui/app-boot/src/index.ts)) - `@deepseek-ai/dsh-brand` ([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts)) - `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts)) +- `@deepseek-ai/dsh-retention` ([`packages/util/retention/src/index.ts`](../packages/util/retention/src/index.ts)) - `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts)) - `@deepseek-ai/dsh-timeout` ([`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts)) diff --git a/packages/README.md b/packages/README.md index 98d3b22719..7060760311 100644 --- a/packages/README.md +++ b/packages/README.md @@ -1,10 +1,10 @@ # Packages -Harness packages, all under the `@deepseek-ai/dsh-*` scope. Each package is a Cordis plugin (microkernel-style): it exports either a default `Service` subclass or a functional plugin, declares its ctx key/events through declaration merging, and exposes extension points through `ctx.effect()`, `ctx.on()`, and `ctx.waterfall()`. Authoring conventions: [AGENTS.md](AGENTS.md) (subtree) and the root [AGENTS.md](../AGENTS.md) § Conventions. +Harness packages live under the `@deepseek-ai/dsh-*` scope. Each is a Cordis plugin: it exports a `Service` subclass or functional plugin, declares ctx keys/events through declaration merging, and extends through `ctx.effect()`, `ctx.on()`, and `ctx.waterfall()`. Authoring conventions: [AGENTS.md](AGENTS.md) and root [AGENTS.md](../AGENTS.md) § Conventions. ## Hierarchy -Packages are grouped by modular role at `packages///`. The group directory is a pure container (no `package.json` of its own); the package name stays `@deepseek-ai/dsh-` regardless of group. **Each group README is the canonical per-package map** — package roles, ctx keys, and the product-vs-support split live there, next to the code. +Packages are grouped by role at `packages///`. The group directory is a pure container; package names stay `@deepseek-ai/dsh-`. Group READMEs are the canonical maps for package roles, ctx keys, and product-vs-support split. | Group | Role | Release expectation | |---|---|---| @@ -17,7 +17,7 @@ Packages are grouped by modular role at `packages///`. The group dir | [`subagent/`](subagent/README.md) | Subagent capability family: the provider-registry seam and the model-facing delegation tool | Product — stable surface | | [`web/`](web/README.md) | Web capability family: the abstract seam, search/fetch provider impls, and the model-facing web tools | Product — stable surface | | [`spill/`](spill/README.md) | Spill capability family: the storage seam, a local impl, and the tool-result spill policy | Product — stable surface | -| [`todo/`](todo/README.md) | Todo/planning family: the model-facing `todo_write` tool (whole-list task tracking on the session log) | Product — stable surface | +| [`todo/`](todo/README.md) | Todo/planning family: the model-facing `todo_write` tool | Product — stable surface | | [`timeout/`](timeout/README.md) | Tool-call timeout policy: the `tools/execute` deadline enforcer | Product — stable surface | | [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders | Product — stable surface | | [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface | @@ -26,12 +26,12 @@ Packages are grouped by modular role at `packages///`. The group dir | [`support/`](support/README.md) | Dev/test/example infrastructure (invariants, replay adapter, subagent mock) | Support — lower compatibility expectations | | [`util/`](util/README.md) | Low-level zero-dependency primitives shared across groups (branding, timeout, retention) | Support — small, stable, harness-dep-free | -The split is the point: a package's group says whether it is part of the product API or support/test/example infrastructure, so release and removal decisions do not treat every package as an equal public contract. New packages join an existing group; adding a new top-level group is a deliberate act (extend the group READMEs and this table). +The split marks product API versus support/test/example infrastructure, so release and removal decisions do not treat every package as equally public. New packages join an existing group; a new top-level group updates the group READMEs and this table. ## Dependencies -The inter-package dependency graph is generated: [docs/module-graph.md](../docs/module-graph.md) (`pnpm run gen-module-graph`, freshness-gated in CI). +The dependency graph is generated: [docs/module-graph.md](../docs/module-graph.md) (`pnpm run gen-module-graph`, freshness-gated in CI). -The rule it must obey: **extension plugins depend on interfaces, never on the concrete loop.** `dsh-agent-loop` is swappable — UI/hook/tool plugins keep working against the `dsh-agent` vocabulary if the loop is replaced. The sanctioned exception is a **composition/bundle** package like `dsh-agent-core`, whose whole job is to assemble the concrete spine: it depends on `dsh-agent-loop` (and the other concrete spine plugins) on purpose. The rule constrains plugins that EXTEND the system, not the bundle that COMPOSES it. A swappable capability splits into interface / implementation / consumer packages (the bash trio is the template — see [capability seams](../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)). +The rule it must obey: **extension plugins depend on interfaces, never on the concrete loop.** `dsh-agent-loop` is swappable, so UI/hook/tool plugins keep working against `dsh-agent` if the loop changes. The exception is a composition bundle like `dsh-agent-core`: it depends on `dsh-agent-loop` because it assembles the concrete spine. Swappable capabilities split into interface / implementation / consumer packages (the bash trio is the template — see [capability seams](../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)). Each package has its own `README.md` with purpose, service API, events, extension points, and deliberate non-goals (TODOs). diff --git a/packages/spill/spill-local/src/store.ts b/packages/spill/spill-local/src/store.ts index adf538e740..44e4ee7129 100644 --- a/packages/spill/spill-local/src/store.ts +++ b/packages/spill/spill-local/src/store.ts @@ -21,6 +21,8 @@ let defaultRoot: string | undefined * tmpdir, created lazily. Predictable world-readable paths would let other * local users read spilled tool output or pre-create symlinks; `mkdtemp` gives * an unpredictable suffix and 0700 semantics. + * + * @returns The lazily-created private spill root. */ export function privateRoot(): string { defaultRoot ??= mkdtempSync(join(tmpdir(), 'dsh-spill-')) @@ -36,6 +38,9 @@ export function privateRoot(): string { * inputs never collide. The whole-segment tokens `.`/`..` are escaped so they * can never traverse. An empty string encodes to `~` (never an empty segment). * (Mirrors the JSONL persistence backend's `encodeSegment`.) + * + * @param raw The untrusted string to encode as one safe path segment. + * @returns An injective, filesystem-safe single path segment. */ export function encodeSegment(raw: string): string { if (raw.length === 0) return '~' @@ -54,7 +59,13 @@ export function encodeSegment(raw: string): string { return out } -/** The session-scoped directory: `/session-`, a short stable hash. */ +/** + * The session-scoped directory: `/session-`, a short stable hash. + * + * @param root The spill root directory. + * @param sessionId The owning session id to hash into a stable directory name. + * @returns The absolute session-scoped spill directory path. + */ export function sessionDir(root: string, sessionId: string): string { const hash = createHash('sha256').update(sessionId).digest('hex').slice(0, 12) return join(root, `session-${hash}`) @@ -85,6 +96,9 @@ export interface SavedText { * a shared root) AND stays readable. The open is exclusive + owner-only * (`'wx', 0o600`): it fails on any existing path — symlink or not — so a * pre-planted target cannot redirect the write. + * + * @param options The resolved root and request fields required to save the file. + * @returns The written file path and UTF-8 byte length. */ export async function saveTextFile(options: SaveTextOptions): Promise { const dir = sessionDir(options.root, options.sessionId) diff --git a/packages/spill/spill/src/types.ts b/packages/spill/spill/src/types.ts index 8dfd4c1d1d..28be96c738 100644 --- a/packages/spill/spill/src/types.ts +++ b/packages/spill/spill/src/types.ts @@ -19,7 +19,12 @@ import type { SessionId } from '@deepseek-ai/dsh-session' */ export type SpillPath = Branded<'SpillPath'> -/** Brand a string as a {@link SpillPath}. */ +/** + * Brand a string as a {@link SpillPath}. + * + * @param path The backend-produced path string to brand. + * @returns The branded spill path. + */ export function SpillPath(path: string): SpillPath { return path as SpillPath } diff --git a/packages/util/timeout/README.md b/packages/util/timeout/README.md index db2b06ba53..a15a552847 100644 --- a/packages/util/timeout/README.md +++ b/packages/util/timeout/README.md @@ -25,12 +25,19 @@ import { clampTimeout, deadline, timeoutOf, TimeoutReason } from '@deepseek-ai/d ## Usage shape -```ts ignore-check +```ts +import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout' + +declare function runWork(options: { signal: AbortSignal }): Promise + // Scope-lifetime consumer (foreground bash, one fetch): `using` disposes the timer. -using d = deadline(upstream, timeoutMs, 'BASH_TIMEOUT') -const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself -const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined // classify the first abort, scoped to OUR code -const aborted = d.signal.aborted && !timedOut // mutually exclusive: timeout won, or cancel did +export async function runWithDeadline(upstream: AbortSignal | undefined, timeoutMs: number): Promise { + using d = deadline(upstream, timeoutMs, 'BASH_TIMEOUT') + const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself + const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined // classify the first abort, scoped to OUR code + const aborted = d.signal.aborted && !timedOut // mutually exclusive: timeout won, or cancel did + return { outcome, timedOut, aborted } +} ``` The signal only *notifies* — the caller MUST attach its own termination (`d.signal.addEventListener('abort', kill)`, or hand `d.signal` to `fetch`). Racing a promise against a timer would resolve the tool-call while the child process or socket leaks on; handing out a signal forces a real termination path to exist.