fix: address master merge gate failures
This commit is contained in:
@@ -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)
|
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`
|
## `@deepseek-ai/dsh-stdio-agent`
|
||||||
|
|
||||||
```ts config-catalog
|
```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-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-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-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)
|
## 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-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-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-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-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))
|
- `@deepseek-ai/dsh-timeout` ([`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts))
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Packages
|
# 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
|
## 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 |
|
| 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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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 |
|
| [`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
|
## 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).
|
Each package has its own `README.md` with purpose, service API, events, extension points, and deliberate non-goals (TODOs).
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ let defaultRoot: string | undefined
|
|||||||
* tmpdir, created lazily. Predictable world-readable paths would let other
|
* tmpdir, created lazily. Predictable world-readable paths would let other
|
||||||
* local users read spilled tool output or pre-create symlinks; `mkdtemp` gives
|
* local users read spilled tool output or pre-create symlinks; `mkdtemp` gives
|
||||||
* an unpredictable suffix and 0700 semantics.
|
* an unpredictable suffix and 0700 semantics.
|
||||||
|
*
|
||||||
|
* @returns The lazily-created private spill root.
|
||||||
*/
|
*/
|
||||||
export function privateRoot(): string {
|
export function privateRoot(): string {
|
||||||
defaultRoot ??= mkdtempSync(join(tmpdir(), 'dsh-spill-'))
|
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
|
* inputs never collide. The whole-segment tokens `.`/`..` are escaped so they
|
||||||
* can never traverse. An empty string encodes to `~` (never an empty segment).
|
* can never traverse. An empty string encodes to `~` (never an empty segment).
|
||||||
* (Mirrors the JSONL persistence backend's `encodeSegment`.)
|
* (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 {
|
export function encodeSegment(raw: string): string {
|
||||||
if (raw.length === 0) return '~'
|
if (raw.length === 0) return '~'
|
||||||
@@ -54,7 +59,13 @@ export function encodeSegment(raw: string): string {
|
|||||||
return out
|
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 {
|
export function sessionDir(root: string, sessionId: string): string {
|
||||||
const hash = createHash('sha256').update(sessionId).digest('hex').slice(0, 12)
|
const hash = createHash('sha256').update(sessionId).digest('hex').slice(0, 12)
|
||||||
return join(root, `session-${hash}`)
|
return join(root, `session-${hash}`)
|
||||||
@@ -85,6 +96,9 @@ export interface SavedText {
|
|||||||
* a shared root) AND stays readable. The open is exclusive + owner-only
|
* 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
|
* (`'wx', 0o600`): it fails on any existing path — symlink or not — so a
|
||||||
* pre-planted target cannot redirect the write.
|
* 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> {
|
export async function saveTextFile(options: SaveTextOptions): Promise<SavedText> {
|
||||||
const dir = sessionDir(options.root, options.sessionId)
|
const dir = sessionDir(options.root, options.sessionId)
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ import type { SessionId } from '@deepseek-ai/dsh-session'
|
|||||||
*/
|
*/
|
||||||
export type SpillPath = Branded<'SpillPath'>
|
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 {
|
export function SpillPath(path: string): SpillPath {
|
||||||
return path as SpillPath
|
return path as SpillPath
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,19 @@ import { clampTimeout, deadline, timeoutOf, TimeoutReason } from '@deepseek-ai/d
|
|||||||
|
|
||||||
## Usage shape
|
## 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.
|
// Scope-lifetime consumer (foreground bash, one fetch): `using` disposes the timer.
|
||||||
using d = deadline(upstream, timeoutMs, 'BASH_TIMEOUT')
|
export async function runWithDeadline(upstream: AbortSignal | undefined, timeoutMs: number): Promise<unknown> {
|
||||||
const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself
|
using d = deadline(upstream, timeoutMs, 'BASH_TIMEOUT')
|
||||||
const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined // classify the first abort, scoped to OUR code
|
const outcome = await runWork({ signal: d.signal }) // work listens on d.signal and terminates itself
|
||||||
const aborted = d.signal.aborted && !timedOut // mutually exclusive: timeout won, or cancel did
|
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.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user