fix: address master merge gate failures

This commit is contained in:
Dudu-0223
2026-07-09 13:01:14 +08:00
parent 78318f5d12
commit 571c6025d5
5 changed files with 75 additions and 13 deletions

View File

@@ -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))

View File

@@ -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).

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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.