Merge branch 'worktree-config-settings-seam' into worktree-llm-dynamic-config
# Conflicts: # docs/capability-seams.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.i18n.yaml # docs/module-graph.md # examples/tui-agent/cordis.yml # packages/README.i18n.yaml # packages/cordis/tool-cordis/src/api-catalog.ts # packages/llm/llm-deepseek/README.i18n.yaml # packages/llm/llm-deepseek/README.zh.md # packages/llm/llm-pi-ai/README.i18n.yaml # packages/llm/llm-pi-ai/README.zh.md # packages/settings/settings-local/src/index.ts # packages/util/README.i18n.yaml # packages/util/README.md # packages/util/README.zh.md # scripts/doc-budgets.manifest.json
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/util/README.md
|
||||
README.md: 3c7fd29e40c07cb25dc6ad040f86c4e31cd41931
|
||||
README.zh.md: 3b4626c7bac8c294dcbdf52ef3b0da46d39a03c8
|
||||
README.md: 46904aba70c7cf0f98bb75cce79d97bb12b950a9
|
||||
README.zh.md: 59a2dcf7926c12d7005446393cadfd8b0be88f77
|
||||
|
||||
@@ -11,6 +11,7 @@ Zero-dependency primitives shared across the other groups. A package lands here
|
||||
| `timeout/` | The timing/classification half of a timeout — `clampTimeout`/`deadline`/`timeoutOf`/`TimeoutReason` (pure functions, no harness deps); termination stays in each capability |
|
||||
| `retention/` | Bounded model-facing output — `ItemRetainer`/`TextRetainer` + neutral notice helpers (pure, no harness deps); business semantics stay in each tool |
|
||||
| `atomic-write/` | Atomic file replacement — `writeFileAtomic` (exclusive-create temp + rename carrying the caller-stated mode); shared by the settings and credentials stores |
|
||||
| `native-command/` | No-shell `execFile` runner for host-native OS integrations — utf8 capture, abort propagation, Windows hide (no harness deps); command choice stays in each caller |
|
||||
|
||||
`dsh-brand` is the canonical case: it owns ONLY the `Branded<B>` helper, so a capability package can brand the ids it owns (`dsh-tasks`'s `TaskId`, `dsh-session`'s `SessionId`, …) by depending on `dsh-brand` alone, without pulling in an unrelated package just to reach `Branded`.
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
| `timeout/` | 超时的时序/分类部分:`clampTimeout`/`deadline`/`timeoutOf`/`TimeoutReason`(纯函数,无 harness 依赖);终止机制保留在各个功能中 |
|
||||
| `retention/` | 有界的面向模型输出:`ItemRetainer`/`TextRetainer` 加上中性通知辅助工具(纯工具,无 harness 依赖);业务语义保留在各个工具中 |
|
||||
| `atomic-write/` | 原子文件替换:`writeFileAtomic`(独占创建临时文件 + 携带调用方所声明 mode 的 rename);由设置与凭据存储共用 |
|
||||
| `native-command/` | 宿主原生 OS 集成的免 shell `execFile` 运行器——utf8 捕获、abort 传播、Windows 窗口隐藏(无 harness 依赖);命令选择保留在各调用方 |
|
||||
|
||||
`dsh-brand` 是规范示例:它只负责 `Branded<B>` 辅助工具,因此功能包可以为自己拥有的 id 添加品牌(`dsh-tasks` 的 `TaskId`、`dsh-session` 的 `SessionId` 等),而只需依赖 `dsh-brand`,无需仅为使用 `Branded` 而引入不相关的包。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
# pnpm run verify-translation-pairing --write packages/util/brand/README.md
|
||||
README.md: 68401d95a31ed2122386794ad5256a5cd93bb2a6
|
||||
README.zh.md: c8b773937426487e67001bc133cf7fae3aea2c6e
|
||||
README.zh.md: 98fcad0487af3ba1c2aaada4716744fb6088cdfe
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`Branded<B>` 名义类型原语:一个微小的**仅类型** 包(无运行时代码,无 harness 包依赖),由每个拥有跨边界 id 的包共享。
|
||||
`Branded<B>` 名义类型原语:一个微小的**仅类型**包(package),无运行时代码,也不依赖其他 harness 包;所有负责跨边界 id 的包都会共享它。
|
||||
|
||||
## `Branded` 是什么
|
||||
|
||||
@@ -19,10 +19,10 @@ export function SessionId(id: string): SessionId {
|
||||
}
|
||||
```
|
||||
|
||||
构造操作通过所属包中针对每个 id 的工厂完成。比较、日志记录、JSON 序列化和协议格式与普通字符串表现相同;品牌会在编译时被擦除。
|
||||
构造操作通过所属包中各 id 专用的工厂完成。比较、日志记录、JSON 序列化和协议格式(wire format)的行为与普通字符串相同;品牌信息会在编译时被擦除。
|
||||
|
||||
## 策略:为跨包边界的 id 添加品牌
|
||||
|
||||
包为自己拥有的 id 添加品牌:`CallId` 位于 `dsh-llm`,共享的 agent/会话 `SessionId` 位于 `dsh-session`,`TaskId` 位于 `dsh-tasks`。为可能被混淆的跨包 id 添加品牌,但无需为每个字符串都添加。
|
||||
|
||||
该包只负责原语。保持无依赖意味着,例如 `dsh-tasks` 可以为 `TaskId` 添加品牌,而无需仅为使用 `Branded` 而导入不相关的功能包。
|
||||
该包只负责这一原语。保持无依赖意味着,例如 `dsh-tasks` 可以为 `TaskId` 使用品牌类型,而无需仅为使用 `Branded` 而导入不相关的功能包。
|
||||
|
||||
6
packages/util/native-command/README.i18n.yaml
Normal file
6
packages/util/native-command/README.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/util/native-command/README.md
|
||||
README.md: 7fc8b1f4640ef87ada62b6656854feb37080e4e6
|
||||
README.zh.md: 4bc66c4047194de03f236fa7591dad88e5c3fb57
|
||||
27
packages/util/native-command/README.md
Normal file
27
packages/util/native-command/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# dsh-native-command
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
A **zero-dependency no-shell `execFile` runner** shared by host-native OS integrations: one `runNativeCommand(command, args, signal)` call spawns the executable directly (never a shell string), captures utf8 stdout/stderr, propagates the caller's abort into child termination, and hides the transient console window on Windows. Failures reject with the exit `code` and both captured streams attached, so callers classify (missing tool, cancelled, real failure) without re-running anything.
|
||||
|
||||
Its two consumers are the host-side native integrations: the [`directory-picker-native`](../../host/directory-picker-native/README.md) backend's OS chooser commands and the gateway's open-with-default-application hand-off ([`dsh-host-apiproxy`](../../host/apiproxy/README.md) `host.openPath`). The `NativeCommandRunner` type is the injectable command boundary those callers expose for deterministic tests.
|
||||
|
||||
It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds no state, emits no events.
|
||||
|
||||
## Surface
|
||||
|
||||
```ts
|
||||
import { runNativeCommand, type NativeCommandRunner } from '@deepseek-ai/dsh-native-command'
|
||||
```
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as this is host-side subprocess plumbing; nothing here reaches a model request.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None; this package neither assembles nor sends a provider request.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **No output bounding** — both streams buffer unbounded in memory; every current caller invokes small native tools whose output is a path or an error line. Adopt `dsh-retention` bounding before pointing this at commands with meaningful output volume.
|
||||
27
packages/util/native-command/README.zh.md
Normal file
27
packages/util/native-command/README.zh.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# dsh-native-command
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
宿主原生 OS 集成共享的**零依赖免 shell `execFile` 运行器**:一次 `runNativeCommand(command, args, signal)` 调用直接派生可执行文件(绝不拼 shell 字符串),以 utf8 捕获 stdout/stderr,把调用方的 abort 传播为子进程终止,并在 Windows 上隐藏瞬时控制台窗口。失败时以附带退出 `code` 与两路已捕获输出的错误拒绝,调用方无需重跑即可分类(工具缺失、已取消、真实失败)。
|
||||
|
||||
它的两个消费者都是宿主侧原生集成:[`directory-picker-native`](../../host/directory-picker-native/README.md) 后端的 OS 选择器命令,以及网关的按默认应用打开转交([`dsh-host-apiproxy`](../../host/apiproxy/README.md) 的 `host.openPath`)。`NativeCommandRunner` 类型是这些调用方为确定性测试暴露的可注入命令边界。
|
||||
|
||||
它是**库,不是服务或插件**:没有 `ctx`、不注册任何东西、不持有状态、不发事件。
|
||||
|
||||
## Surface
|
||||
|
||||
```ts
|
||||
import { runNativeCommand, type NativeCommandRunner } from '@deepseek-ai/dsh-native-command'
|
||||
```
|
||||
|
||||
## Model Experience
|
||||
|
||||
无;这是宿主侧子进程管道,这里没有任何东西进入模型请求。
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
无;该包既不组装也不发送 provider 请求。
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **不做输出限量**——两路流在内存中无界缓冲;当前每个调用方只运行输出为一个路径或一行错误的小型原生工具。把它指向输出量可观的命令之前,先接入 `dsh-retention` 限量。
|
||||
37
packages/util/native-command/package.json
Normal file
37
packages/util/native-command/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-native-command",
|
||||
"description": "Zero-dependency no-shell execFile runner for host-native OS integrations: utf8 stdio capture, abort propagation, Windows hide",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
}
|
||||
}
|
||||
44
packages/util/native-command/src/index.ts
Normal file
44
packages/util/native-command/src/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Shared no-shell `execFile` runner for host-native OS integrations (the
|
||||
* native directory chooser, the open-with-default-application hand-off):
|
||||
* utf8 stdio capture, abort propagation, Windows console hide. A library,
|
||||
* not a plugin — no ctx, no state, no events.
|
||||
* @module @deepseek-ai/dsh-native-command
|
||||
*/
|
||||
|
||||
import { execFile } from 'node:child_process'
|
||||
|
||||
/** Testable command boundary; native implementations never invoke a shell. */
|
||||
export type NativeCommandRunner = (
|
||||
command: string,
|
||||
args: readonly string[],
|
||||
signal: AbortSignal,
|
||||
) => Promise<{ stdout: string; stderr: string }>
|
||||
|
||||
/**
|
||||
* Run a host command with utf8 stdio, abort propagation, and Windows hide.
|
||||
* @param command - executable path or PATH name.
|
||||
* @param args - argv (never a shell string).
|
||||
* @param signal - caller/connection lifetime; abort terminates the child.
|
||||
* @returns captured stdout/stderr on exit 0.
|
||||
*/
|
||||
export const runNativeCommand: NativeCommandRunner = (command, args, signal) =>
|
||||
new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
command,
|
||||
[...args],
|
||||
{ encoding: 'utf8', signal, windowsHide: true },
|
||||
(error, stdout, stderr) => {
|
||||
if (error !== null) {
|
||||
const failure = Object.assign(new Error(error.message, { cause: error }), {
|
||||
code: error.code,
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
reject(failure)
|
||||
return
|
||||
}
|
||||
resolve({ stdout, stderr })
|
||||
},
|
||||
)
|
||||
})
|
||||
31
packages/util/native-command/src/invariant.ts
Normal file
31
packages/util/native-command/src/invariant.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-native-command`.
|
||||
* @module @deepseek-ai/dsh-native-command/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-native-command'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'native-command-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: each run is one stateless child-process round trip
|
||||
* with no owned event stream or mutable runtime data; behavior is enforced by
|
||||
* unit tests.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
43
packages/util/native-command/tests/native-command.spec.ts
Normal file
43
packages/util/native-command/tests/native-command.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runNativeCommand } from '@deepseek-ai/dsh-native-command'
|
||||
|
||||
const node = process.execPath
|
||||
|
||||
describe('runNativeCommand', () => {
|
||||
it('captures utf8 stdout and stderr on exit 0', async () => {
|
||||
const result = await runNativeCommand(
|
||||
node,
|
||||
['-e', 'process.stdout.write("out✓"); process.stderr.write("err")'],
|
||||
new AbortController().signal,
|
||||
)
|
||||
expect(result).toEqual({ stdout: 'out✓', stderr: 'err' })
|
||||
})
|
||||
|
||||
it('rejects a non-zero exit with code, stdout, and stderr attached', async () => {
|
||||
const failure = await runNativeCommand(
|
||||
node,
|
||||
['-e', 'process.stdout.write("partial"); process.stderr.write("boom"); process.exit(3)'],
|
||||
new AbortController().signal,
|
||||
).then(() => { throw new Error('unexpected resolve') }, (error: unknown) => error)
|
||||
expect(failure).toMatchObject({ code: 3, stdout: 'partial', stderr: 'boom' })
|
||||
expect((failure as Error).cause).toBeInstanceOf(Error)
|
||||
})
|
||||
|
||||
it('rejects a missing executable with the spawn ENOENT code', async () => {
|
||||
const failure = await runNativeCommand(
|
||||
'dsh-definitely-missing-command',
|
||||
[],
|
||||
new AbortController().signal,
|
||||
).then(() => { throw new Error('unexpected resolve') }, (error: unknown) => error)
|
||||
expect(failure).toMatchObject({ code: 'ENOENT' })
|
||||
})
|
||||
|
||||
it('terminates the child when the signal aborts', async () => {
|
||||
const abort = new AbortController()
|
||||
const pending = runNativeCommand(node, ['-e', 'setTimeout(() => {}, 60_000)'], abort.signal)
|
||||
abort.abort()
|
||||
const failure = await pending.then(() => { throw new Error('unexpected resolve') }, (error: unknown) => error)
|
||||
expect(failure).toBeInstanceOf(Error)
|
||||
expect((failure as { code?: unknown }).code).toBe('ABORT_ERR')
|
||||
})
|
||||
})
|
||||
15
packages/util/native-command/tsconfig.json
Normal file
15
packages/util/native-command/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
# pnpm run verify-translation-pairing --write packages/util/paths/README.md
|
||||
README.md: b28e684f3183d739c8e229a9b341801dbf345d86
|
||||
README.zh.md: 20cc033e4d0ee22d75c7ca315c462a7846146444
|
||||
README.zh.md: ab4e8123d19fd56749e3e7a0d59e8cd6bea0c3d0
|
||||
|
||||
@@ -16,9 +16,9 @@ DeepSeek Harness 用户数据的共享文件系统路径辅助工具。
|
||||
|
||||
`expandHomePath()` 使用操作系统主目录展开 `~`、`~/...` 和 Windows 风格的 `~\...` 前缀。它会保留非波浪号路径和 `~user/...` 原样不变。
|
||||
|
||||
该包刻意保持规模小且不依赖 harness,以便产品包共享用户数据路径约定,而不必彼此依赖。
|
||||
该包(package)刻意保持规模小且不依赖 harness,以便产品包共享用户数据路径约定,而不必彼此依赖。
|
||||
|
||||
## 已知限制与待完成工作
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **展开范围刻意保持狭窄**:只有单独的 `~`、`~/...` 和 `~\...` 使用当前操作系统主目录;`~alice/...` 等指定用户的形式、环境变量和 shell 表达式保持不变。
|
||||
- **辅助工具不会操作文件系统**:调用方仍负责目录创建、存在性检查、权限,以及对结果路径应用信任策略。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
# pnpm run verify-translation-pairing --write packages/util/retention/README.md
|
||||
README.md: d257075a67b35e92bce53a88fc6d002f4f4d5d9b
|
||||
README.zh.md: 433d512144612137fbfccf107cfe0987cec091fa
|
||||
README.zh.md: c97c1d5afe504c54851f35b86d7bb893b32bcea4
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
一个轻依赖的**保留** 库:为必须限制返回上下文量的工具提供有界的面向模型输出。调用方将项或文本分片送入有界对象,然后取回保留的内容和精确的省略元数据。
|
||||
一个轻依赖的**保留**库:为必须限制返回上下文量的工具提供有界的面向模型输出。调用方将项或文本分片送入有界对象,然后取回保留的内容和精确的省略元数据。
|
||||
|
||||
该库**只** 负责这个机制问题:*「我们保留了什么,又省略了什么?」*。工具专用代码保留其业务语义:文件分组、行号、退出码、提供方错误状态、每行预览截断、spill 文件以及面向模型的文案。这就是 [Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md) 划定的边界。
|
||||
该库**只**负责这个机制问题:*「我们保留了什么,又省略了什么?」*。工具专用代码保留其业务语义:文件分组、行号、退出码、提供方错误状态、每行预览截断、spill 文件以及面向模型的文案。这就是 [Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md) 划定的边界。
|
||||
|
||||
它是**库,而非服务或插件**:没有 `ctx`,不注册任何内容,不发出任何事件。状态只存在于每个 retainer(一次累积)中,绝不跨调用。工具包直接导入它。
|
||||
它是**库,而非服务或插件**:没有 `ctx`,不注册任何内容,不发出任何事件。状态只存在于每个 retainer(一次累积)中,绝不跨调用。工具包(package)直接导入它。
|
||||
|
||||
## 对外接口
|
||||
|
||||
@@ -32,18 +32,18 @@ import type {
|
||||
|
||||
## 资源模式
|
||||
|
||||
两个 retainer 使用独立名称,而不是同一个通用收集器,因为它们的**资源模型** 不同。
|
||||
两个 retainer 使用独立名称,而不是同一个通用收集器,因为它们的**资源模型**不同。
|
||||
|
||||
- **`ItemRetainer` 限制有序逻辑单元**。搜索工具可收集完整结果集用于 spill 文件恢复,同时只为面向模型的预览保留前 `maxItems` 项。因为调用方会继续送入每个已观察到的项,所以省略数量是精确的。
|
||||
- **`TextRetainer` 限制面向字节的文本**。`head`、`tail` 和 `headTail` 在 `finish()` 时保留 UTF-8 边界;`headTail` 是 `dsh-spill-policy` 用于围绕 spill 文件通知构建有界预览的形态。
|
||||
|
||||
## `truncated` 是预算事实,绝不表示「不完整」
|
||||
|
||||
`truncated` 表示*因为预算限制,retainer 省略了本可获得的内容*。它**不** 表示上游不完整。权限失败、跳过二进制文件、提供方部分失败、不可读候选项和无效 UTF-8 保留在工具领域字段中,绝不合并到 `truncated`。将两者混为一谈是该库命名最容易诱发的缺陷;务必保持分离。
|
||||
`truncated` 表示*因为预算限制,retainer 省略了本可获得的内容*。它**不**表示上游不完整。权限失败、跳过二进制文件、提供方部分失败、不可读候选项和无效 UTF-8 保留在工具领域字段中,绝不合并到 `truncated`。将两者混为一谈是该库命名最容易诱发的缺陷;务必保持分离。
|
||||
|
||||
## 字节,而非字符
|
||||
|
||||
文本上限和 `omittedBytes` 按**字节** 计数,以保证进程/正文安全(子进程 pipe 和 HTTP 正文都是字节流)。跨越码点的分片会被正确处理:`finish()` 会修剪每个切割位置的不完整码点,使返回文本绝不在边界引入替换字符;首尾两侧会分开解码,因此绝不会跨越被省略的中间部分重建码点。按字符或行限制的预览预算属于独立的工具职责。
|
||||
文本上限和 `omittedBytes` 按**字节**计数,以保证进程/正文安全(子进程管道和 HTTP 正文都是字节流)。跨越码点的分片会被正确处理:`finish()` 会修剪每个切割位置的不完整码点,使返回文本绝不在边界引入替换字符;首尾两侧会分开解码,因此绝不会跨越被省略的中间部分重建码点。按字符或行限制的预览预算属于独立的工具职责。
|
||||
|
||||
## 工具映射
|
||||
|
||||
@@ -51,11 +51,11 @@ import type {
|
||||
|
||||
| 工具 | Retainer 与策略 | 说明 |
|
||||
|---|---|---|
|
||||
| `glob` | `ItemRetainer<FsGlobEntry>`, `head` | 收集完整的已排序路径列表用于 spill 文件,同时在内联位置保留第一页。路径映射、已跳过候选项和 `incomplete` 保留在外部。 |
|
||||
| `grep` | `ItemRetainer<FlatGrepMatch>`, `head` | 收集匹配项用于 spill 文件,同时在内联位置保留第一页。每个匹配项的预览截断、分组、排序和 `incomplete` 保留在外部。 |
|
||||
| `bash` | `TextRetainer`, `tail` or `headTail` | 执行器仍负责 spill 文件、退出状态、信号、超时和后台任务。 |
|
||||
| `web_fetch` | `TextRetainer`, `head` or `headTail` | 提供方/资源上限保留为提供方事实;retainer 只提供保留文本和省略元数据。 |
|
||||
| `web_search` | `ItemRetainer<WebSearchSource>`, `head` | 当提供方返回的来源超过面向模型的结果应包含的数量时,标准化「来源已达上限」通知。 |
|
||||
| `glob` | `ItemRetainer<FsGlobEntry>`,`head` | 收集完整的已排序路径列表用于 spill 文件,同时在内联位置保留第一页。路径映射、已跳过候选项和 `incomplete` 保留在外部。 |
|
||||
| `grep` | `ItemRetainer<FlatGrepMatch>`,`head` | 收集匹配项用于 spill 文件,同时在内联位置保留第一页。每个匹配项的预览截断、分组、排序和 `incomplete` 保留在外部。 |
|
||||
| `bash` | `TextRetainer`,`tail` 或 `headTail` | 执行器仍负责 spill 文件、退出状态、信号、超时和后台任务。 |
|
||||
| `web_fetch` | `TextRetainer`,`head` 或 `headTail` | 提供方/资源上限保留为提供方事实;retainer 只提供保留文本和省略元数据。 |
|
||||
| `web_search` | `ItemRetainer<WebSearchSource>`,`head` | 当提供方返回的来源超过面向模型的结果应包含的数量时,标准化「来源已达上限」通知。 |
|
||||
|
||||
`read` **刻意不在 v1 范围内**。其 `read-render` 辅助工具负责文件专用的分页契约:`offset`/`limit`、行号、`totalLines`、偏移越界错误、每行预览截断、针对已选窗口的字节上限。这是行窗口渲染器,而非通用保留机制。单个 `Omitted` 数量无法表示行窗口两侧。
|
||||
|
||||
@@ -87,11 +87,11 @@ const footer = formatRetentionNotice(
|
||||
|
||||
通过渲染保留内容和省略元数据的工具消费方间接影响模型。
|
||||
|
||||
#### KV 缓存影响
|
||||
#### KV Cache 影响
|
||||
|
||||
不直接导致失效;指定的消费方负责其引起的任何请求前缀变更。
|
||||
不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
|
||||
|
||||
## 已知限制与待完成工作
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **项保留只支持 `head`**:tail、head/tail、分页、分组和提供方完整性语义仍由工具负责。
|
||||
- **文本保留面向字节**:`read` 分页等行窗口和字符窗口需要单独的渲染器;切割可能会丢弃部分 UTF-8 边界字节,以保持返回文本有效。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
# pnpm run verify-translation-pairing --write packages/util/timeout/README.md
|
||||
README.md: 11c55a45a1255e14fb551e42ba3965453dbd94ae
|
||||
README.zh.md: 699fb62a4fa19f97d234f83b21f639f61a9f0777
|
||||
README.zh.md: 8b63f00595139f9af2e9a31e8ab3c3494088e0ff
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
超时的**时序与分类** 部分:一个零依赖纯函数库(无运行时 harness 依赖),由每个需要限制调用方超时提示、启动 deadline,并在之后区分「已超时」与「已取消」的功能共享。
|
||||
超时的**时序与分类**部分:一个零依赖纯函数库(无运行时 harness 依赖),由每个需要限制调用方超时提示、启动 deadline,并在之后区分「已超时」与「已取消」的功能共享。
|
||||
|
||||
它**不负责终止**。它发出的信号只会*通知*;真正停止工作仍由各功能负责,因为机制各不相同:bash 对操作系统进程组发送 SIGKILL,web 拆除 `fetch` socket,没有任何共享层能够承担全部终止机制。[Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md) 将边界划定为:共享时序/分类,将强制终止保留在本地。
|
||||
它**不负责终止**。它发出的信号只会*通知*;真正停止工作仍由各功能负责,因为机制各不相同:bash 对操作系统进程组发送 SIGKILL,web 关闭 `fetch` 套接字,没有任何共享层能够承担全部终止机制。[Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md) 将边界划定为:共享时序/分类,将强制终止保留在本地。
|
||||
|
||||
它是**库,而非服务或插件**:没有 `ctx`,不注册任何内容,不持有状态,也不发出事件。「超时服务」必须了解如何停止每项功能的工作,这正是微内核要排除在共享层之外的知识。
|
||||
|
||||
@@ -16,16 +16,16 @@ import { clampTimeout, deadline, idleWatchdog, MAX_TIMER_DELAY_MS, timeoutOf, Ti
|
||||
|
||||
| 导出项 | 职责 |
|
||||
|---|---|
|
||||
| `clampTimeout(requested, def, max, name?)` | 验证调用方可选的正有限提示,从 `def` 填充,并限制在 `max` 以内。如果提示不为正数或有限数,则抛出错误(包含 `name`)。 |
|
||||
| `clampTimeout(requested, def, max, name?)` | 验证调用方可选的、值为正且有限的提示,从 `def` 填充,并限制在 `max` 以内。如果提示为非正数或非有限数,则抛出错误(包含 `name`)。 |
|
||||
| `deadline(upstream, timeoutMs, code)` | 将 `upstream` 取消与超时融合为一个 `AbortSignal`(`AbortSignal.any`);超时携带 `TimeoutReason`。`[Symbol.dispose]` 清除 timer。 |
|
||||
| `idleWatchdog(upstream, timeoutMs, code)` | 保持一个稳定的融合信号,并且只在受保护的异步迭代器 `next()` 尚未完成时启动。解析后取消启动;后续需求重新启动;dispose 清除;并发需求被拒绝。 |
|
||||
| `MAX_TIMER_DELAY_MS` | Node 在不将延迟限制为 1 毫秒时可调度的最大延迟(`2_147_483_647`)。拥有 timer 的配置不得超过该值。 |
|
||||
| `idleWatchdog(upstream, timeoutMs, code)` | 保持一个稳定的融合信号,并且只在受保护的异步迭代器 `next()` 尚未完成时启动 timer。完成后停止 timer;后续需求重新启动 timer;dispose(资源释放)时清除;并发需求被拒绝。 |
|
||||
| `MAX_TIMER_DELAY_MS` | Node 在不将延迟限制为 1 毫秒时可调度的最大延迟(`2_147_483_647`)。负责 timer 的配置不得超过该值。 |
|
||||
| `timeoutOf(signal \| { reason }, code?)` | 从已中止的信号/错误中恢复 `TimeoutReason`,否则返回 `undefined`,即超时与取消的分类器。传入 `code` 可仅匹配这个 deadline 的 timer(见下文的嵌套)。 |
|
||||
| `TimeoutReason` | 印在超时中止上的内部原因(`code` + `timeoutMs`)。它不是公开错误;提供方将其转换为自己的错误/字段。 |
|
||||
| `TimeoutReason` | 标记在超时中止上的内部原因(`code` + `timeoutMs`)。它不是公开错误;提供方将其转换为自己的错误/字段。 |
|
||||
|
||||
## `timeoutMs <= 0` 哨兵值
|
||||
|
||||
`0` 是后端自有后台工作(bash `start()`)使用的「无超时」值,其可见范围为:**内部**。`deadline()` 不启动 timer,只转发 `upstream`;如果也没有 upstream,它将返回永不中止的信号和无操作 disposer,因此每个调用方都能保持同一种调用形态。外部请求提示会通过 `clampTimeout` 验证为**正有限数**,之后才进入 `deadline`,因此 `0` 绝不是面向模型/插件的「禁用超时」值。
|
||||
`0` 是后端自有后台工作(bash `start()`)使用的**内部**「无超时」值。`deadline()` 不启动 timer,只转发 `upstream`;如果也没有 upstream,它将返回永不中止的信号和无操作 disposer,因此每个调用方都能保持同一种调用形态。外部请求提示会通过 `clampTimeout` 验证为**正有限数**,之后才进入 `deadline`,因此 `0` 绝不是面向模型/插件的「禁用超时」值。
|
||||
|
||||
## 使用形态
|
||||
|
||||
@@ -44,27 +44,27 @@ export async function runWithDeadline(upstream: AbortSignal | undefined, timeout
|
||||
}
|
||||
```
|
||||
|
||||
该信号只会*通知*;调用方必须连接自己的终止机制(`d.signal.addEventListener('abort', kill)`,或将 `d.signal` 传给 `fetch`)。让 promise 与 timer 竞速,会在子进程或 socket 泄漏的情况下就解析工具调用;发出信号则会强制要求存在真正的终止路径。
|
||||
该信号只会*通知*;调用方必须接入自己的终止机制(`d.signal.addEventListener('abort', kill)`,或将 `d.signal` 传给 `fetch`)。让 promise 与 timer 竞速,会在子进程或套接字仍在泄漏时就让工具调用完成;发出信号则会强制要求存在真正的终止路径。
|
||||
|
||||
将你自己的 `code` 传给 `timeoutOf`,以便分类可在嵌套中组合:当你收到的 `upstream` *本身*就是 deadline 信号时(未来启动每次调用 deadline 的 `tools/execute` 中间件),如果外层 timer 首先触发,`AbortSignal.any` 会保留外层 `TimeoutReason`。将范围限定为你的 `code`,可将外部超时视为普通 upstream 取消,这才是你所属功能视角下的正确分类,而不会在本地 timer 尚未到期时就声称自己超时。
|
||||
|
||||
对于流式传输,创建一个 `idleWatchdog`,将其稳定的 `signal` 传入传输,并为每次提供方读取调用 `watchdog.next(iterator)`。间隔必须为正有限数,且不得超过 `MAX_TIMER_DELAY_MS`;否则 Node 会将其限制为 1 毫秒。它只测量尚未完成的需求,因此当下游代码进行渲染或在请求下一个分片前以其他方式等待时,timer 不会运行。该原语仍然只会通知,因此传输必须观察稳定信号;DeepSeek 和 pi-ai 适配器证明,超时会关闭它们的真实响应正文或 SDK 请求。
|
||||
对于流式传输,创建一个 `idleWatchdog`,将其稳定的 `signal` 传给传输层,并为提供方的每次读取调用 `watchdog.next(iterator)`。间隔必须为正有限数,且不得超过 `MAX_TIMER_DELAY_MS`;否则 Node 会将其限制为 1 毫秒。它只对尚未完成的读取请求计时,因此当下游代码进行渲染或在请求下一个分片前以其他方式等待时,timer 不会运行。该原语仍然只会通知,因此传输层必须观察稳定信号;DeepSeek 和 pi-ai 适配器证明,超时会关闭它们的真实响应正文或 SDK 请求。
|
||||
|
||||
## 哪些操作不设置超时
|
||||
|
||||
本地文件 `read`/`write`/`edit` 不接受 `timeoutMs`:系统调用最多只能尽力中止,超时无法强制 `fsync`/`rename` 停止,而添加超时将成为违反显式优于隐式的默认值。详见 [`fs/`](../../fs/README.md)。
|
||||
本地文件 `read`/`write`/`edit` 不接受 `timeoutMs`:中止系统调用至多只能尽力而为,超时无法强制 `fsync`/`rename` 停止,而添加超时会引入一个违反显式优于隐式原则的隐式默认值。详见 [`fs/`](../../fs/README.md)。
|
||||
|
||||
## 模型体验
|
||||
|
||||
通过 `dsh-timeout-policy` 等消费方间接影响模型;消费方可能会将提供方结果替换为已保留的超时错误,或抑制延迟结果。
|
||||
|
||||
#### KV 缓存影响
|
||||
#### KV Cache 影响
|
||||
|
||||
不直接导致失效;指定的消费方负责其引起的任何请求前缀变更。
|
||||
不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
|
||||
|
||||
## 已知限制与待完成工作
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **只发出通知**:deadline 无法停止忽略其信号的工作;每项功能仍需要自己的 socket/进程/任务终止路径。
|
||||
- **`timeoutMs <= 0` 是内部词汇**:只有在所属后端已解析策略后,它才会禁用本地 timer;绝不会作为面向模型/插件的公开开关。
|
||||
- **第一个中止原因决定分类**:当 upstream 取消早于本地 timer 发生时,即使自己的超时之后也会到期,该层也无法再报告。
|
||||
- **空闲 watchdog 不是总 deadline**:它针对每个尚未完成的迭代器需求重新启动,并刻意排除消费方的思考时间。
|
||||
- **空闲 watchdog 不是总 deadline**:它针对每个尚未完成的迭代器需求重新启动,并刻意排除消费方的处理时间。
|
||||
|
||||
Reference in New Issue
Block a user