Преглед на файлове

fix: address master merge gate failures

Dudu-0223 преди 2 месеца
родител
ревизия
571c6025d5
променени са 5 файла, в които са добавени 75 реда и са изтрити 13 реда
  1. 36 0
      docs/config-catalog.md
  2. 6 6
      packages/README.md
  3. 15 1
      packages/spill/spill-local/src/store.ts
  4. 6 1
      packages/spill/spill/src/types.ts
  5. 12 5
      packages/util/timeout/README.md

+ 36 - 0
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))

+ 6 - 6
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/<group>/<pkg>/`. The group directory is a pure container (no `package.json` of its own); the package name stays `@deepseek-ai/dsh-<pkg>` 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/<group>/<pkg>/`. The group directory is a pure container; package names stay `@deepseek-ai/dsh-<pkg>`. 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/<group>/<pkg>/`. 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/<group>/<pkg>/`. 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).

+ 15 - 1
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: `<root>/session-<hash(sessionId)>`, a short stable hash. */
+/**
+ * The session-scoped directory: `<root>/session-<hash(sessionId)>`, 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<SavedText> {
   const dir = sessionDir(options.root, options.sessionId)

+ 6 - 1
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
 }

+ 12 - 5
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<unknown>
+
 // 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<unknown> {
+  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.