Merge remote-tracking branch 'origin/master' into codex/agent-session-jsonl-location
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/core-data-structures/bash.md # docs/module-graph.md # docs/rfc/INDEX.md # docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md # docs/rfc/implemented/feature/2026-06-30-hook-bridges.md # examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.golden.md # examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.golden.md # examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.golden.md # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl # packages/bash/bash-local/README.md # packages/bash/bash-local/src/run.ts # packages/bash/bash-local/tests/run.spec.ts # packages/bash/bash/README.md # packages/bash/tool-bash/README.md # packages/bash/tool-bash/src/index.ts # packages/bash/tool-bash/tests/tools.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-core/src/index.ts # packages/session-persistence/session-persistence-jsonl/src/index.ts # packages/session-persistence/session-persistence-sqlite/src/index.ts # packages/session-persistence/session-persistence/README.md # packages/session-persistence/session-persistence/src/index.ts # packages/ui/acp-agent/README.md # packages/ui/stdio-agent/README.md
This commit is contained in:
@@ -1,23 +1,9 @@
|
||||
/**
|
||||
* The `Branded<B>` nominal-typing primitive — a type-only utility (no runtime
|
||||
* code, no harness-package dependency) shared by every package that owns a
|
||||
* cross-boundary id.
|
||||
*
|
||||
* A brand makes structurally-identical strings non-interchangeable at the type
|
||||
* level: an `AgentId` cannot be passed where a `CallId` is expected, even
|
||||
* though both are plain strings at runtime. Construction goes through a per-id
|
||||
* factory in the OWNING package (a plain cast inside — zero runtime cost);
|
||||
* comparison, logging, and serialization all behave as ordinary strings.
|
||||
*
|
||||
* Policy: a package brands the ids it owns — `CallId` in dsh-llm (tool-call
|
||||
* correlation), `SessionId` in dsh-session, `AgentId` in dsh-agent,
|
||||
* `BashTaskId`/`OwnerToken` in dsh-bash. Branding is for ids that cross package
|
||||
* boundaries and could plausibly be confused; not every string needs a brand.
|
||||
* This package owns ONLY the primitive — no concrete id, no runtime code beyond
|
||||
* the (erased) type — so the brand vocabulary stays dependency-free and a
|
||||
* package can brand its ids without depending on an unrelated capability
|
||||
* package (e.g. dsh-bash brands its ids without pulling in dsh-llm).
|
||||
*
|
||||
* Dependency-free nominal typing for cross-boundary identifiers. Structurally identical runtime
|
||||
* strings become non-interchangeable statically while retaining ordinary comparison, logging, and
|
||||
* serialization. Each owning package defines its concrete id and zero-cost factory; brand ids that
|
||||
* can plausibly be confused across packages, not arbitrary strings. This package exports only the
|
||||
* erased primitive so an owner need not depend on another capability package.
|
||||
* @module @deepseek-ai/dsh-brand
|
||||
*/
|
||||
|
||||
|
||||
@@ -7,3 +7,11 @@
|
||||
3. The `.dsh` directory under the current user's home directory.
|
||||
|
||||
The resolver reads its inputs at call time. It does not cache a result, create the directory, or mutate `process.env`; consumers keep ownership of their own configuration fields and pass the configured value when resolving the shared home.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through `dsh-tool-bash`, which exposes the resolved path to model bash as `DSH_HOME` without adding a prompt section.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Resolution only** — the resolver makes a path absolute but does not create it, check access, or canonicalize symlinks; each consumer owns those filesystem decisions.
|
||||
|
||||
@@ -40,3 +40,13 @@ Pass your own `code` to `timeoutOf` so classification composes under nesting: wh
|
||||
## What does NOT get a timeout
|
||||
|
||||
Local file `read`/`write`/`edit` take no `timeoutMs`: a syscall is best-effort-abortable at most, a timeout could not force `fsync`/`rename` to stop, and adding one would be an implicit default that violates explicit-over-implicit. See [`fs/`](../../fs/README.md).
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through consumers such as `dsh-timeout-policy`, which may replace a provider result with a retained timeout error or suppress a late result.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Notification only** — a deadline cannot stop work that ignores its signal; every capability still needs its own socket/process/task termination path.
|
||||
- **`timeoutMs <= 0` is internal vocabulary** — it disables the local timer only after an owning backend has resolved policy, never as a public model/plugin knob.
|
||||
- **The first abort reason wins classification** — when an upstream cancellation beats the local timer, this layer cannot later report that its own timeout would also have elapsed.
|
||||
|
||||
@@ -1,43 +1,13 @@
|
||||
/**
|
||||
* The timing-and-classification half of a timeout — a zero-dependency library
|
||||
* of pure functions shared by every capability that clamps a caller's timeout
|
||||
* hint, arms a deadline, and later has to tell "timed out" apart from
|
||||
* "cancelled". It owns NO termination: the returned {@link deadline} signal only
|
||||
* NOTIFIES; actually stopping the work (SIGKILL a process group, tear down a
|
||||
* fetch socket, …) stays in each capability's implementation, because that
|
||||
* mechanism differs per capability and no shared layer can own all of them.
|
||||
*
|
||||
* This is deliberately a library, not a cordis service or plugin: it takes no
|
||||
* `ctx`, registers nothing, holds no cross-call state, and emits no events. A
|
||||
* "timeout service" would have to understand how to stop every capability's
|
||||
* work — exactly the knowledge a microkernel keeps out of shared layers.
|
||||
*
|
||||
* The four exports and their division of labor:
|
||||
* - {@link clampTimeout} — validate a caller's optional positive hint, fill the
|
||||
* backend default, cap at the backend max (pure arithmetic + the shared
|
||||
* positive-finite request contract).
|
||||
* - {@link deadline} — fuse upstream cancellation with a timeout into one
|
||||
* `AbortSignal`, the timeout carrying an identifiable {@link TimeoutReason};
|
||||
* `[Symbol.dispose]` clears the timer.
|
||||
* - {@link timeoutOf} — classify an aborted signal (or error): a
|
||||
* {@link TimeoutReason} means the timeout fired, anything else (or nothing)
|
||||
* means it did not.
|
||||
* - {@link TimeoutReason} — the internal classification reason; providers
|
||||
* translate it into their own public error/result shape before returning.
|
||||
*
|
||||
* Shared timeout arithmetic, signal fusion, and classification. The library
|
||||
* only notifies through abort signals; each capability still owns the mechanism
|
||||
* that stops its work and translates timeout reasons into public outcomes.
|
||||
* @module @deepseek-ai/dsh-timeout
|
||||
*/
|
||||
|
||||
/**
|
||||
* The internal reason attached to a timeout abort so consumers can classify it
|
||||
* after the fact. It carries the failing `code` (each capability's own string —
|
||||
* `BASH_TIMEOUT`, `WEB_FETCH_TIMEOUT`, …) and the `timeoutMs` that elapsed.
|
||||
*
|
||||
* It is an INTERNAL classification reason, not a public error: providers
|
||||
* translate it into their seam-specific error code or result field (via
|
||||
* {@link timeoutOf}) before returning to callers. Native `AbortSignal.timeout()`
|
||||
* yields a fixed `TimeoutError` indistinguishable across timeout kinds; this
|
||||
* type is identifiable and carries the code/duration.
|
||||
* Internal abort reason carrying a capability-owned code and elapsed deadline.
|
||||
* Providers translate it through {@link timeoutOf} before returning to callers.
|
||||
*/
|
||||
export class TimeoutReason extends Error {
|
||||
override name = 'TimeoutReason'
|
||||
@@ -52,16 +22,15 @@ export class TimeoutReason extends Error {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a caller's optional timeout hint, fill it from the backend default,
|
||||
* then cap at the backend max. The shared positive-finite request contract:
|
||||
* a supplied `requested` must be a positive finite number or this throws —
|
||||
* `0` is NOT a caller-facing "disable timeout" value (that sentinel is internal
|
||||
* to {@link deadline}). A missing `requested` falls back to `def`.
|
||||
* Validate a caller's optional timeout hint, use the backend default, then cap
|
||||
* it. Supplied values must be positive and finite; zero is not a public
|
||||
* disable-timeout sentinel.
|
||||
*
|
||||
* @param requested The caller's optional hint; validated when present.
|
||||
* @param def The backend default applied when `requested` is absent.
|
||||
* @param max The backend upper bound the result is capped to.
|
||||
* @param name Field name used in the thrown message (so the caller sees which input was bad).
|
||||
* @param name Field name used in the thrown message (so the caller sees which input was
|
||||
* bad).
|
||||
* @returns The effective timeout in milliseconds: `min(requested ?? def, max)`.
|
||||
*/
|
||||
export function clampTimeout(
|
||||
@@ -85,23 +54,9 @@ export interface Deadline {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a deadline signal that aborts on upstream cancellation OR on timeout,
|
||||
* with the timeout carrying an identifiable {@link TimeoutReason} (unlike
|
||||
* native `AbortSignal.timeout()`, whose fixed `TimeoutError` is opaque). It is
|
||||
* `AbortSignal.any([upstream, <timeout>])` — the single primitive that fuses
|
||||
* two abort sources — with the reason and a disposable timer added on top.
|
||||
*
|
||||
* `timeoutMs <= 0` is the INTERNAL "no timeout" sentinel for backend-owned
|
||||
* background work: arm no timer and forward only the upstream signal; with no
|
||||
* upstream either, return a never-aborting signal so callers keep one call
|
||||
* shape. External request hints validate as positive finite via
|
||||
* {@link clampTimeout} before reaching here, so `0` never arrives from a model
|
||||
* or plugin.
|
||||
*
|
||||
* The returned object's `[Symbol.dispose]` clears the timer — use `using` for a
|
||||
* scope-lifetime consumer, or call it manually for an event-lifetime one. The
|
||||
* signal only NOTIFIES; the caller must attach its own termination (kill the
|
||||
* process group, abort the fetch, …).
|
||||
* Fuse upstream cancellation with an identifiable timeout. `timeoutMs <= 0` is
|
||||
* the internal no-timer sentinel; the returned disposer clears an armed timer.
|
||||
* The signal only notifies, so callers must stop their own work.
|
||||
*
|
||||
* @param upstream The caller's cancellation signal, if any, fused into the result.
|
||||
* @param timeoutMs Deadline in milliseconds; `<= 0` means "no timeout" (arm no timer).
|
||||
@@ -114,9 +69,8 @@ export function deadline(
|
||||
code: string,
|
||||
): Deadline {
|
||||
if (timeoutMs <= 0) {
|
||||
// No timeout (background work): forward only the upstream signal, or a
|
||||
// never-aborting one when there is no upstream. No timer, so dispose is a
|
||||
// no-op — the empty method keeps the one call shape for every caller.
|
||||
// No timeout (background work): forward only the upstream signal, or a never-aborting one
|
||||
// when there is no upstream.
|
||||
return { signal: upstream ?? new AbortController().signal, [Symbol.dispose]() {} }
|
||||
}
|
||||
|
||||
@@ -132,22 +86,9 @@ export function deadline(
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the {@link TimeoutReason} from an aborted signal (or any object with a
|
||||
* `reason`), else `undefined`. This is the classification half: a provider
|
||||
* calls it on the deadline signal after an abort to decide whether the cause
|
||||
* was its timeout (translate to the capability's timeout error/field) or an
|
||||
* ordinary upstream cancellation (`undefined` → the cancel path).
|
||||
*
|
||||
* Pass `code` to scope the match to THIS deadline's timer. It matters under
|
||||
* nesting: when the `upstream` handed to {@link deadline} is itself a deadline
|
||||
* signal (e.g. a future `tools/execute` middleware arming a per-call deadline),
|
||||
* `AbortSignal.any` preserves the OUTER `TimeoutReason` if the outer timer fires
|
||||
* first. Without `code`, the inner capability would misclassify that outer
|
||||
* timeout as its own (`timedOut:true` / `WEB_FETCH_TIMEOUT`) though its local
|
||||
* timer never expired; with `code`, a foreign timeout reads as `undefined` and
|
||||
* falls through to the upstream-cancel path, which is the correct classification
|
||||
* from the inner capability's view. Omit `code` only to ask "was this ANY
|
||||
* timeout" (a generic middleware that owns no single code).
|
||||
* Recover a timeout reason from a reason-bearing object. Supplying `code`
|
||||
* distinguishes this deadline from a nested upstream deadline; a foreign code
|
||||
* follows the ordinary cancellation path.
|
||||
*
|
||||
* @param x An {@link AbortSignal} or any `{ reason }` carrier (e.g. a caught abort error).
|
||||
* @param code When provided, only a {@link TimeoutReason} with this exact `code` matches.
|
||||
|
||||
@@ -171,10 +171,9 @@ describe('timeoutOf', () => {
|
||||
|
||||
describe('deadline — nested deadlines', () => {
|
||||
it("does not misclassify an outer deadline's timeout as the inner code", () => {
|
||||
// The upstream handed to the inner deadline is ITSELF a deadline that has
|
||||
// already timed out (outer). AbortSignal.any preserves the outer reason;
|
||||
// scoping timeoutOf to the inner code keeps the inner capability from
|
||||
// reporting the outer timeout as its own — it reads as an upstream cancel.
|
||||
// The upstream handed to the inner deadline is ITSELF a deadline that has already timed out
|
||||
// (outer). `AbortSignal.any` preserves that reason, but scoping `timeoutOf` to the inner code
|
||||
// must classify it as upstream cancellation rather than the inner capability's timeout.
|
||||
const outer = new AbortController()
|
||||
outer.abort(new TimeoutReason('OUTER_TIMEOUT', 30))
|
||||
using inner = deadline(outer.signal, 60_000, 'BASH_TIMEOUT')
|
||||
|
||||
Reference in New Issue
Block a user