@margay/ccl-core 1.5.0-alpha.4 → 1.5.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/cli.js +69586 -64065
  2. package/dist/sdk/background.d.ts +48 -0
  3. package/dist/sdk/background.js +87 -0
  4. package/dist/sdk/backgroundRunTypes.d.ts +101 -0
  5. package/dist/sdk/backgroundRunTypes.js +1 -0
  6. package/dist/sdk/commandAdapter.d.ts +77 -0
  7. package/dist/sdk/commandAdapter.js +180 -0
  8. package/dist/sdk/commandProcess.d.ts +27 -0
  9. package/dist/sdk/commandProcess.js +130 -0
  10. package/dist/sdk/executionGuidance.d.ts +7 -0
  11. package/dist/sdk/executionGuidance.js +37 -0
  12. package/dist/sdk/index.d.ts +13 -0
  13. package/dist/sdk/index.js +9 -0
  14. package/dist/sdk/mcpServer.d.ts +9 -5
  15. package/dist/sdk/mcpServer.js +51 -9
  16. package/dist/sdk/operations/command.d.ts +24 -0
  17. package/dist/sdk/operations/command.js +48 -0
  18. package/dist/sdk/operations/host.d.ts +3 -0
  19. package/dist/sdk/operations/host.js +376 -0
  20. package/dist/sdk/operations/json.d.ts +6 -0
  21. package/dist/sdk/operations/json.js +24 -0
  22. package/dist/sdk/operations/reference.d.ts +15 -0
  23. package/dist/sdk/operations/reference.js +19 -0
  24. package/dist/sdk/operations/store.d.ts +26 -0
  25. package/dist/sdk/operations/store.js +300 -0
  26. package/dist/sdk/operations/types.d.ts +171 -0
  27. package/dist/sdk/operations/types.js +8 -0
  28. package/dist/sdk/pendingApprovalIdentity.d.ts +35 -0
  29. package/dist/sdk/pendingApprovalIdentity.js +27 -0
  30. package/dist/sdk/permissionDelegation.d.ts +19 -0
  31. package/dist/sdk/permissionDelegation.js +11 -0
  32. package/dist/sdk/query.d.ts +2 -1
  33. package/dist/sdk/query.js +133 -30
  34. package/dist/sdk/queryLifecycle.d.ts +19 -0
  35. package/dist/sdk/queryLifecycle.js +93 -0
  36. package/dist/sdk/queryLifecycleTypes.d.ts +160 -0
  37. package/dist/sdk/queryLifecycleTypes.js +47 -0
  38. package/dist/sdk/scheduledOccurrenceTypes.d.ts +103 -0
  39. package/dist/sdk/scheduledOccurrenceTypes.js +12 -0
  40. package/dist/sdk/types.d.ts +17 -2
  41. package/docs/background-runs.md +229 -0
  42. package/docs/command-tool-recovery.md +62 -0
  43. package/docs/dev-quickstart.md +29 -7
  44. package/docs/error-recovery-usage.md +18 -0
  45. package/docs/hook-input-provenance.md +77 -0
  46. package/docs/index.md +2 -0
  47. package/docs/mcp-tool-recovery.md +82 -0
  48. package/docs/releases/1.5.0-alpha.5.md +26 -0
  49. package/docs/releases/1.5.0-alpha.6.md +15 -0
  50. package/docs/releases/1.5.1-alpha.1.md +11 -0
  51. package/docs/releases/1.5.1-alpha.2.md +17 -0
  52. package/docs/releases/1.5.1-alpha.md +22 -0
  53. package/docs/runtime-resources.md +6 -3
  54. package/examples/background-runs/agents/background-reviewer.md +6 -0
  55. package/examples/background-runs/agents/background-worker.md +6 -0
  56. package/examples/background-runs/worker-reviewer.mjs +42 -0
  57. package/examples/command-recovery/durable-server.mjs +44 -0
  58. package/examples/command-recovery/receipt-command.mjs +40 -0
  59. package/package.json +16 -2
  60. package/sdk/API.md +146 -0
  61. package/sdk/README.md +6 -0
@@ -1,5 +1,82 @@
1
1
  # UserPromptSubmit input identity
2
2
 
3
+ ## Query and Stop correlation (1.5.1-alpha candidate)
4
+
5
+ Native `Stop`, `SubagentStop` and `StopFailure` payloads can include an optional
6
+ `query` observation. Check that it exists and that `query.record.version === 1`;
7
+ older hosts provide no exact query association. A Stop is a **checkpoint before
8
+ the loop may continue**, not a completion notification. `StopFailure` retains its
9
+ failure semantics. Existing text and hook blocking behavior remain unchanged.
10
+
11
+ The observation contains `record`, `persistence`, and `transcript_path`. The record
12
+ has `session_id`, `query_id`, `event_id`, `sequence`, `phase`, `attempt`, `scope`,
13
+ `agent_id`, `accepted_inputs`, `input_message_ids`, `input_association`, and
14
+ `last_assistant_message_id`. Each independent query, including children and helpers,
15
+ gets a new query ID. Retries, tool rounds and blocked Stop continuations retain that
16
+ ID; every checkpoint gets its own event ID. Two identical answers are distinguishable
17
+ without comparing their text. A copied parent context does not copy query identity.
18
+
19
+ `accepted_inputs` lists the new input identities actually accepted into the query,
20
+ with their recorded origin. `input_message_ids` is the subset handed to the host
21
+ model adapter after the query's compaction/recovery selection. It does not prove
22
+ remote receipt or understanding. Blocked/unvisited queued inputs are excluded;
23
+ accepted inputs cancelled before dispatch are not in this subset. Earlier handoffs
24
+ survive compaction. Unknown historical or transferred-query association stays
25
+ `input_association: "unknown"`; do not substitute the latest user message. These
26
+ identities grant no authority and do not turn host/agent reports into human input.
27
+
28
+ The separate `system/query_lifecycle` stream reports `start`, `stop_attempt`,
29
+ `stop_failure` and `terminal` observations. Only `terminal` has a status:
30
+ `completed`, `failed`, `cancelled`, `stopped` or `interrupted`, with the raw
31
+ `terminal_reason`. This is the end of that query loop, not business completion,
32
+ delivery to a user, all background work finishing, or the outer SDK result.
33
+ An API-error response can be `failed` even when a legacy raw reason is `completed`.
34
+ Consumer closure records interruption/cancellation without yielding more events to
35
+ that closed consumer. Process death can leave only a start/checkpoint, with no end.
36
+
37
+ TUI stores these observations without displaying them or sending them to the model.
38
+ Headless stream-json emits them; the SDK yields typed `query_lifecycle` events.
39
+ Durable background run events retain their native envelopes without treating them
40
+ as the run's final result. Observe the outer SDK result and validate the requested
41
+ business outcome separately.
42
+
43
+ Use the packaged SDK to read the **specified file and original query identity**,
44
+ including when processing a delayed first-round hook after a second round ends:
45
+
46
+ ```js
47
+ import { readQueryLifecycle } from '@margay/ccl-core/sdk'
48
+
49
+ const observation = hookInput.query
50
+ if (observation?.record.version === 1 && observation.transcript_path) {
51
+ const state = await readQueryLifecycle({
52
+ transcriptPath: observation.transcript_path,
53
+ sessionId: observation.record.session_id,
54
+ queryId: observation.record.query_id,
55
+ })
56
+ // state === found with completeness === complete exposes the original terminal.
57
+ // incomplete / not_found / unavailable never prove success or non-execution.
58
+ }
59
+ ```
60
+
61
+ The file format is JSONL metadata `{ "type": "query_lifecycle", "record": ... }`,
62
+ separate from the conversation UUID/parent chain. The reader does not search other
63
+ sessions, start a host/daemon, repair a log or replay tools. Exact repeated events
64
+ are deduplicated; conflicting identities, invalid records and an unterminated tail
65
+ are unavailable. A missing start, event sequence gap or absent terminal is incomplete.
66
+ It streams the file with a 32 Mi-character per-line limit; exceeding that limit is
67
+ unavailable, not a partial success. No matching record in a readable old file is
68
+ `not_found`, not evidence that execution never occurred.
69
+
70
+ Persistence is separate from execution: `saved` means the queued local write and
71
+ flush succeeded (process-crash recovery, not power-loss durability); `unavailable`
72
+ means persistence was disabled or the query has no transcript; `failed` means a
73
+ write failed. A write exceeding two seconds returns `unknown/write_timeout` and
74
+ stops further waits on that query writer. The OS write may still finish with the
75
+ same event ID; read the original query later rather than replaying its tools.
76
+ There is no automatic execution or permission replay when a terminal is absent.
77
+
78
+ ## Input provenance
79
+
3
80
  Introduced in 1.5.0-alpha.3: structured identity for native `UserPromptSubmit` hooks. Verify the actual host CLI version; older installations do not gain this behavior from a documentation update.
4
81
 
5
82
  ```json
package/docs/index.md CHANGED
@@ -15,6 +15,8 @@
15
15
  ## Host integration
16
16
 
17
17
  - [SDK overview](../sdk/README.md) and [API contract](../sdk/API.md)
18
+ - [Durable background tasks and assistant handoff (1.5.1-alpha)](background-runs.md)
19
+ - [Command recovery](command-tool-recovery.md) and [MCP recovery](mcp-tool-recovery.md)
18
20
  - [Runtime resources](runtime-resources.md)
19
21
  - [Hook input provenance](hook-input-provenance.md)
20
22
  - [Shared skill resources](shared-skill-resources.md)
@@ -0,0 +1,82 @@
1
+ # 【公开】MCP 工具失败与恢复
2
+
3
+ 本页描述 `1.5.1-alpha` 的 MCP 恢复接口。C3 操作级持久查询/恢复及主、子助手与 CLI/SDK 消费链已完成限定范围的独立审核;这不代表 #55 整体完成。正常 MCP 工具不需要新配置。共用工具执行器会保存操作身份、派发及实际结果;可选适配器进一步提供跨进程回执查询和受控恢复。独立后台执行使用 [BackgroundRun](background-runs.md),不能把保存操作记录当作后台任务已经接管。
4
+
5
+ ## 为什么不能遇到断线就重跑
6
+
7
+ 工具可能已经发出消息、写入文件或完成订单,随后回执丢失。连接断开只说明没有得到可靠结果,不能说明动作没执行。CCL 会让失效连接退出缓存,后续独立调用仍可重连;原动作是否重放另行判断。
8
+
9
+ 错误会保留实际原因及 operationId/attemptId。`effect=not_dispatched` 表示这次操作没有进入工具派发;`effect=unknown` 表示可能已有副作用。后者应通过工具已有的查询能力或真实产物核对,不凭助手记忆推断失败,也不换一个工具名重复同样动作。
10
+
11
+ 取消请求不等于远端已经停止。调用后超时、取消、鉴权失败和工具 `isError` 都可能是 unknown。调用前取消则明确未派发,不能回报成功。
12
+
13
+ 主助手 SDK 的工具错误 envelope 在 `mcpMeta._meta['ccl/recovery']` 中携带宿主事实;现有 SDK 事件消费会将它并入 `tool_use_result._meta`。子 agent 的现有 envelope 不透传 mcpMeta,错误文本仍携带身份和 unknown;不能声称子 agent 已得到同样的结构化接口。服务器不能覆盖宿主保留字段,普通服务器 _meta 保留。
14
+
15
+ ## 可选自动重试
16
+
17
+ 默认不自动重放未知操作。工具提供者若具有可靠的只读或相同输入幂等契约,可由宿主维护者在已有 MCP 配置内设置 `toolRecovery`。该配置只控制自动重试,不改变工具权限,不注入新的 MCP 参数。
18
+
19
+ 每个精确工具名绑定 `adapterVersion`、`targetDigest`、`inputSchemaDigest` 和 `replay`(read_only 或 idempotent)。摘要算法由 `services/mcp/recovery.ts` 的 `mcpRecoveryDigest`/`mcpRecoveryTargetDigest` 定义:递归排序对象键、保留数组顺序、移除 undefined 对象值,对 JSON UTF-8 编码作 SHA-256;目标摘要不含 scope/toolRecovery,本地进程环境和动态账户还有运行时检查。摘要是配置绑定,不能证明外部系统的幂等语义或身份真实性。
20
+
21
+ 初版仅接受 user/local/managed/enterprise 中显式维护的策略;项目文件、插件附带或动态模型配置不会自动成为重放依据。服务器 hints 必须与策略一致,但单独 hints 不够。OAuth、动态 headersHelper、无法核对运行时账户的连接不会因此得到自动重放能力;这些工具的正常调用仍然可用。需要动态幂等有效期、额外远端查回或进程外账户凭据核验的工具,等待后续适配器接口,不靠声明强行开启。
22
+
23
+ 重连后重新检查工具 schema、连接配置、能力和现有权限。参数包括原幂等键必须保持不变。最多两次实际调用,session 恢复与 URL 授权共用一次重放预算;完成 URL 授权不代表有权重做原写操作。底层 SDK 在 tools/call 的 401/403 后也不能绕过该规则自动重发,但连接初始化/发现的原鉴权流程保留。
24
+
25
+ ## 前台退出或执行器崩溃
26
+
27
+ 经 CCL 共用工具执行器调用的 MCP 操作会在派发前建立持久记录;错误和成功结果元数据关联同一个 operationId。可以用 `ccl operations get <operationId> --json` 读取已保存事实,不必 resume 原聊天。旧 transcript 或绕过宿主执行器直接调用底层客户端,不因此获得持久恢复承诺。前台断开但独立执行器仍在运行,与执行器本身崩溃是不同情况。
28
+
29
+ ## 注册跨进程查询
30
+
31
+ 宿主维护者在配置目录(默认 `~/.ccl`)的 `operation-adapters.json` 注册 `mcpAdapters`。模型只选择已注册的引用,不能提交命令、回执或配置路径来改写恢复规则。下面的示意值必须替换成实际工具名、账户/版本及 schema 摘要:
32
+
33
+ ```json
34
+ {
35
+ "schemaVersion": 1,
36
+ "mcpAdapters": {
37
+ "orders": {
38
+ "server": "orders",
39
+ "adapterVersion": "1",
40
+ "targetIdentity": "tenant-42",
41
+ "execute": { "name": "start", "inputSchemaDigest": "<sha256>", "handlePath": ["job", "id"] },
42
+ "identity": {
43
+ "name": "whoami", "inputSchemaDigest": "<sha256>", "arguments": {},
44
+ "targetPath": ["account"], "versionPath": ["version"]
45
+ },
46
+ "query": {
47
+ "name": "status", "inputSchemaDigest": "<sha256>",
48
+ "inputs": { "requestKey": ["requestKey"] },
49
+ "operationInputs": { "jobId": "remoteHandle" },
50
+ "statusPath": ["status"], "appliedStatus": "complete",
51
+ "receiptPath": ["receipt"], "resultPath": ["result"],
52
+ "bindings": [{ "response": ["requestKey"], "input": ["requestKey"] }]
53
+ },
54
+ "replay": "never",
55
+ "credentialRef": "orders-primary",
56
+ "credentialHeaders": ["Authorization"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ `inputSchemaDigest` 使用完整 `tools/list` inputSchema 的规范 JSON 摘要(对象键递归排序、数组顺序保留、SHA-256),不是描述文本摘要。普通 execute 参数保持原样。`query.inputs` 从原获准输入取值;`constants` 可提供固定查询参数;`operationInputs` 只可使用宿主保存的 operationId、requestKey 或 remoteHandle。路径用字符串数组,空数组表示整个对象。字段缺失不会猜值;远端句柄尚未保存时不能按一个新句柄重建原任务。
63
+
64
+ `identity` 是现有的只读身份查询,必须返回当前实际账户和版本;首次执行及恢复都核验。`query.bindings` 把回执中的业务标识与原输入逐项对应,错配保持 unknown。只有配置的完成状态和匹配回执才确认效果。状态值可为字符串、数字或布尔值,`appliedStatus` 默认 `applied`,`inProgressStatus` 默认 `in_progress`。查询返回运行中时,可用 `handlePath` 保存原任务句柄;业务是否达到用户要求仍由 assistant 判断。
65
+
66
+ `replay` 默认 `never`。确有只读契约时可声明 `read_only`;确有相同输入幂等契约时可声明 `idempotent` 并提供 `idempotency: {scope, retentionMs}`。CCL 保留原输入和业务键,超过保留期不能据旧声明重放。该声明必须由外部服务兑现,不能仅凭模型建议或 server hint 添加。
67
+
68
+ 凭据保留在现有 MCP 配置中。`credentialHeaders`/`credentialEnv` 仅声明哪些凭据值可以刷新;引用名、名单、普通环境、URL/命令、schema 或映射改变均改变绑定。同一账户的凭据刷新可恢复;切换账户不能。首次调用的活连接与当前配置不一致时需先刷新工具,不能用新连接的身份核验替旧连接背书。仍沿用当前 MCP 配置策略、禁用状态及原工具权限;查询和身份工具也需要各自的当前许可。
69
+
70
+ CLI 支持 `get`、`events`、`reconcile`、`resume`、`cancel`,也可显式 `submit --adapter mcp:orders --request-key <key> --input <json>` 接受新操作。执行另用 `resume <operationId> --revision <当前revision> --action-id <稳定动作ID>`。`reconcile` 使用同样的 revision/action-id 参数。CLI 非交互模式未获许可时保留等待状态;许可可通过现有 `--allowed-tools` 规则传入,不默认放行。`Operation` 模型工具使用相同核心。取消只请求停止,不能证明远端副作用已撤销。
71
+
72
+ 普通 MCP 调用返回后,宿主另外给模型提供操作引用;原始工具结果保持不变。主助手、子助手和恢复会话都可据此查询。SDK 的 `tool_result` 事件及 `onToolUse` 回调提供可选 `operationReference: {operationId, revision, toolUseId}`;CLI stream-json 对应字段是 `operation_reference`,与具体 tool-use ID 绑定。它来自宿主记录,不从服务端 `_meta` 或业务 JSON 推导。只有已被宿主持久接受的调用才有此引用,旧版本和其他工具可以不提供。
73
+
74
+ `revision` 是引用生成时的版本,恢复动作前应先 `get` 获取当前版本。`get/events` 是读取最后已提交的记录,`running/dispatching` 不能证明远端现在仍存活;需要当前状态时,仅在 `allowedActions` 支持时显式 `reconcile`。拿到引用或远端句柄都不等于业务完成。
75
+
76
+ 若调用来自 [持久后台任务](background-runs.md),assistant 可在原会话退出后,先按稳定业务键取回原 run,再读取 `operationReferences` 查询这些操作。宿主在真正派发工具前保存关联;关联保存失败会阻止派发。已有旧版本未捕获的调用不能追溯补齐,空引用不证明外部效果不存在。MCP 服务没有可信 query/幂等契约时,CCL 仍保留 unknown,不为提高“完成率”自动重试写入。后台事件分页只解决执行记录补收,不代替外部服务回执或 assistant 的真实业务送达记录。
77
+
78
+ ## 当前验证
79
+
80
+ 真实环回 HTTP/stdio MCP 服务先记录副作用,再丢回执/退出;测试经过 CCL 动态工具调用,比较实际请求及磁盘计数。旧 HTTP 路径复现两次副作用,旧调用前取消复现成功 `{data:undefined}`;修复后对应行为有回归用例。主/子助手公共 query/toolExecution envelope 使用脚本化模型验证,不冒充真实模型业务验收。
81
+
82
+ 用例入口:`__tests__/integration/mcp-recovery.test.ts`、`__tests__/integration/mcp-recovery-envelope.test.ts`、`__tests__/unit/mcp-recovery-policy.test.ts`。执行时须使用隔离 HOME/config/TMPDIR 与测试拥有的进程,不连接真实业务 MCP。本片不是 #55 完成声明。
@@ -0,0 +1,26 @@
1
+ # 1.5.0-alpha.5
2
+
3
+ 本版本修复记忆访问、技能上下文恢复和工具发现的通用宿主问题,并提供 CLI/SDK 共用的工具执行指引。通过 `alpha` 通道交付,`latest` 保持不变。
4
+
5
+ - **记忆按需读取**:保留 MEMORY.md 索引,取消启动及逐轮无条件扫描、相关性筛选和正文预取。用户或当前指令要求时,通过工具定向查找。记忆提取先判断是否有值得保存的信息,再读索引及相关主题核对;记忆内容仍需按实际来源判断准确性。
6
+ - **技能发现与恢复**:普通子 agent 首次请求可获得有权限且受长度限制的技能目录;模型或上下文预算变化会刷新目录。按资源身份区分同名技能,保留实际来源和调用标识,修复压缩及会话恢复时的技能契约丢失。
7
+ - **共享执行指引**:公开 SDK 导出 `getExecutionGuidance(toolNames)`,默认主助手、Explore 和 Plan 共用执行与调查的边界;保留岗位身份及用户定制。普通文件操作优先合适的专用工具,脚本等使用 Bash,允许获授权的缺工具回退。GitHub URL 不再强制使用 gh,PR 操作按依赖顺序执行。
8
+ - **MCP 工具元数据**:SDK 将 `searchHint` 转发到工具发现元数据,并提供可选 `alwaysLoad` 以请求首轮展示工具定义;这些选项不授予操作权限。
9
+ - **本地追踪记录**:修复关闭 OpenTelemetry exporter 时 Perfetto 请求、工具与权限记录因共享 span 身份而互相覆盖的问题。
10
+
11
+ 共享指引的组合方式见 [SDK API](../../sdk/API.md)。默认系统提示已经包含该指引;自管 `systemPrompt` 的应用可以显式追加。CLI 与 SDK 应固定为同一包版本,不另复制一套 preset,也不覆盖用户 persona。
12
+
13
+ ## 升级与兼容
14
+
15
+ ```sh
16
+ npm install -g @margay/ccl-core@1.5.0-alpha.5
17
+ ccl --version
18
+ ```
19
+
20
+ SDK 应用更新 `@margay/ccl-core` 依赖后,重启实际运行进程。新增技能身份元数据只对包含该元数据的记录生效;旧记录缺少身份时不会猜测补齐,也不会重写旧会话。
21
+
22
+ ## 已知限制
23
+
24
+ - 本版本不宣称彻底消除模型的多余探查,也没有证明稳定提速;#55 的行为问题继续跟踪。
25
+ - `Agent(run_in_background: true)` 返回启动信息,不等于同一 SDK Session 已释放下一条输入。需要前台继续响应时,应用应维护独立 worker Session 并消费其结果。持久队列、重启恢复、取消和业务交付回执需要应用按实际入口接通并验收。
26
+ - 不改动现有权限边界;专用工具被拒绝不能通过 Bash 或另一个接口绕过。指引存在不保证模型在所有任务中都遵循。
@@ -0,0 +1,15 @@
1
+ # 1.5.0-alpha.6
2
+
3
+ 本版本包含 [alpha.5 的通用宿主修复](1.5.0-alpha.5.md),并修复一条后台任务终态通知遗漏路径。
4
+
5
+ 后台 agent 可能先进入终态,再等待清理并发送完成通知。此前,`TaskOutput`
6
+ 在此期间读取结果,会抑制后续模型通知,却没有通知 SDK,导致消费者仍显示任务运行中。
7
+ 现在这条读取路径会补发一次结构化 SDK 终态,保留原工具调用 ID 和输出路径;
8
+ 失败仍为 `failed`,取消映射为 `stopped`,已通知或重复读取不会再次发送。
9
+
10
+ SDK 应启用 `includeRaw: true` 消费 `task_started/task_progress/task_notification`。
11
+ 用法和失败边界见 [SDK API](../../sdk/API.md)。本次未新增持久任务查询、崩溃后重放
12
+ 或自动重派能力;任务状态也不代替业务核验和交付回执。
13
+
14
+ CLI 与 SDK 应安装相同固定版本,重启需要使用新版本的进程。通过 `alpha` 标签交付,
15
+ 不更改 `latest`。本版本不宣称解决 #55 的全部工具绕路或证明稳定提速。
@@ -0,0 +1,11 @@
1
+ # 1.5.1-alpha.1
2
+
3
+ 本预发布版包含 [1.5.1-alpha 的能力与限制](1.5.1-alpha.md),并包含以下宿主改进。npm 安装使用明确版本或 `alpha` 标签;发布状态以 registry 为准,`latest` 不随本次预发布更新。
4
+
5
+ - 后台权限通知在入队前与实际消费时核对原任务、工具调用和当前状态。已经批准、拒绝、取消或终态的旧请求不再要求重复决定,后续结果可以继续送达。TUI、SDK/headless 和 query 中途消费路径共用该规则。仍未批准的请求不自动放行;读取失败不视为已解决,事件历史与原身份保留。见[后台任务说明](../background-runs.md)。
6
+ - 原生 Write 与工作流文件工具支持显式 create-only 写入:目标已存在时原子拒绝,不覆盖原文件。具体参数与平台限制见 [SDK API](../../sdk/API.md)。
7
+ - 查询诊断关联输入、停止检查点和终态结果,便于区分主会话停止与后台任务状态;诊断不证明远端执行者当前仍存活。
8
+
9
+ 本次权限通知修复通过相关单元/集成测试,以及真实 SDK 和有头 TUI 模型场景:已决权限不再送入前台要求重复批准,原任务只发布一次并送达真实回执;未批准近邻保持等待。它不替代 Pepper 业务接入验收,不承诺所有模型措辞或历史 incomplete_stream 问题均已解决。
10
+
11
+ 升级时保持 CLI 与 SDK 使用同一版本。旧运行进程不会热更新;先核对原任务状态,正常结束并重启后采用新代码。不要因状态未知而换键重派写操作。Windows 和断电恢复等未验证边界沿用上一版说明。
@@ -0,0 +1,17 @@
1
+ # 1.5.1-alpha.2
2
+
3
+ 本预发布版在 [1.5.1-alpha.1](1.5.1-alpha.1.md) 的基础上修复 #60 的周期后台任务权限交接及通知恢复。安装使用明确版本或 `alpha` 标签;npm 实际发布状态以 registry 为准,`latest` 保持不变。
4
+
5
+ - 新建后台 schedule 按任务保存创建会话的有效权限模式与可委派规则。共享 daemon 不因某项任务全局提权;执行时重新加载持久配置。真正需要审批的工具暂停并通知原会话,避免已授权动作因 SDK 回调路由而无声挂起。
6
+ - 挂起先冻结执行、保存 transcript 和原调用凭据,再确认退出;断连不冒充用户拒绝。批准绑定原 occurrence、pending revision、requestId、toolUseId 与稳定 actionId。恢复上下文保留完整原参数,执行前继续严格校验,防止同名工具或参数变化误用批准。
7
+ - TUI、持续运行的 SDK/headless 主循环接收原会话的待批及结果通知;离线后可恢复原会话补收。已有决定的旧审批在队列消费前再次核对,不反复询问。同一 pending 跨周期合并不会重复申请批准。
8
+ - SDK 提供 occurrence 查询、事件分页、精确审批和取消;模型沿用 `BackgroundRun occurrence_id`。`CronCreate operation: reauthorize` 显式更新未来调度的授权及接收会话,既有待批 occurrence 不被新授权升级,也不吞掉新 revision 的触发。
9
+ - 审批决定及恢复意图先持久化再启动执行;owner 崩溃后保留 interrupted 和原收据,不盲目重放结果未知的调用。
10
+
11
+ 入口、参数和模式差异见[后台任务说明](../background-runs.md)。旧 `daemon approve <id> allow` 缺少精确身份,改为返回迁移说明;消费者应使用 `approve-occurrence` 或 SDK `approveOccurrence`。actionId 由调用方生成并保存,同一决定重试复用,不需要用户填写。
12
+
13
+ 相关单元/集成测试、实际 CLI/SDK/daemon 调用、执行器强杀恢复及真实模型有头 TUI 流程已验证。真实模型曾在恢复时删掉原 Write 参数的尾部换行,严格守卫拒绝;补全原参数上下文后复测成功,未放松审批比较。测试证明这些观察到的行为,不保证任意模型始终遵从提示。
14
+
15
+ 升级保持 CLI/SDK 同版本。已有进程不会热更新;先查询原任务状态,正常结束后重启采用新代码。旧无委派任务保持原行为,可显式重交接;不要因状态未知更换任务键重派写操作。读取旧记录不等于允许新代码自动恢复它,原身份与当前权限仍须匹配。
16
+
17
+ 一次性 headless 退出后不承诺向关闭的输出流推送;外部消息送达、业务阶段恢复与最终验收由 assistant/产品负责。取消不能撤回已发生的外部效果,不承诺跨任意系统 exactly-once。TUI 强杀的全部交叉场景、Windows 与断电恢复未单独验证。#58 的宿主取消来源契约与 #59 的视觉预算恢复仍待处理,不包含在本次修复声明中。
@@ -0,0 +1,22 @@
1
+ # 1.5.1-alpha
2
+
3
+ 以下描述 `1.5.1-alpha` 工件的能力和已知限制;安装时固定该版本,不用旧 alpha 标签包代替。发布是否完成以 npm registry 的版本与工件完整性记录为准。
4
+
5
+ 包含此前 alpha.6 的后台任务终态通知修复,并增加两项通用工具行为改进:
6
+
7
+ - 显式启用 Read 去重时,只有原工具结果仍完整保留在当前上下文中,才省略重复正文。结果已清除、截断或恢复缓存缺少来源时,重新返回文件内容。默认去重开关不变。
8
+ - 主助手、Explore/Plan 和 SDK 公开共享指引统一说明:参数已知、无结果依赖或副作用冲突的必要调用可同轮提出;依赖操作等待前一结果。保留权限限制和必要核验,不为组成批次增加无关操作。完整替换 system 的 SDK 调用方仍需显式追加共享指引。
9
+
10
+ 相关测试与有限真实模型案例已验证调用顺序、权限拒绝和产物;不保证所有模型无回归,也不宣称已经消除全部重复探查或获得稳定提速。
11
+
12
+ #55 的通用宿主实现已增加 [持久后台任务](../background-runs.md):独立执行者、按稳定键查询、事件分页补收、权限与取消记录,以及执行器退出后关联原工具操作的恢复入口。SDK、CLI 和模型工具共用同一核心;大量事件不再要求一次读完整历史,SDK 流式解码保留跨 stdout 字节块的 UTF-8 内容。原操作如何核对与重放见 [命令恢复](../command-tool-recovery.md) 和 [MCP 恢复](../mcp-tool-recovery.md)。
13
+
14
+ 已有 macOS 进程故障、ACK 丢失与原任务查回验证;本轮脚本化模型通过 SDK 和有头 TUI 验证后台运行时连续回答两条前台消息,前台退出后后台完成且未重复执行。它证明宿主机制,不代表真实模型提速或 Pepper 已接入新接口。Pepper 新入口业务验收、Windows 和断电恢复仍未完成;不会自动重跑整个 agent 或未知写操作。后台完成不等于业务成果通过核验。
15
+
16
+ 文件工具错误在普通终端模式保留具体原因,避免只显示“Error writing file”而隐藏“必须先读取”等原错误。升级须采用同一固定 CLI/SDK 版本。正在运行的旧进程不会热更新;先核对原任务状态,再正常退出并重启。旧 owner 不支持新提交接口时明确拒绝,不自动接管或重跑任务。
17
+
18
+ 记忆提取未完成提示保留 API/传输、轮次上限、工具结果、取消等实际失败类别;同类失败不刷屏,新类别仍可见。保留原保存确认和重试条件,不提高5轮上限、不增加全目录扫描。具体解释见 [错误恢复说明](../error-recovery-usage.md)。
19
+
20
+ CCL 自建提示词不再默认引导到上游产品文档或内部反馈频道,请求正文 system 中的 `x-anthropic-billing-header` attribution 文本也已移除(不是 HTTP header)。真实协议字段、旧配置名与用户上下文保留。真实 `claude-*` 命名网关目标和直连 OAuth 的认证兼容性未验证,不能假设网关会剥离该文本或所有下游均不依赖它。
21
+
22
+ 已知限制:Operation 查询返回最后保存的状态,不证明远端任务仍存活;真实模型曾把这一快照误述为当前活性。未知效果须核对原操作,不靠换键重派解决。真实案例验证保留过迟执行、reviewer 工具错误和终端宏未正常收尾等失败,不以本版本发布声明这些业务行为已全部修复。
@@ -29,9 +29,12 @@ stream-json hosts may send another `initialize` while the connection remains ope
29
29
  Hosts already consuming raw `system/init` can read the same field there; REPL
30
30
  bridge initialization uses the same builder. Ordinary terminal use is unchanged.
31
31
 
32
- This interface is present in source for the #53 candidate following
33
- `1.5.0-alpha.2`; no release version has been published for it yet. Test the actual
34
- field/version rather than assuming every installed `1.5.0-alpha.2` contains it.
32
+ Use `@margay/ccl-core@1.5.0-alpha.4` or a later release that includes this
33
+ contract. The interface was introduced in alpha.3, which was withdrawn from npm;
34
+ alpha.4 retains it with corrected package contents. Published alpha.2 does not
35
+ include it. Upgrade the CLI actually launched by the host and start a new backend
36
+ process, then check the response field and schema version. Updating a separate
37
+ global CLI does not update a Desktop application's bundled dependency.
35
38
 
36
39
  ## Meaning of the fields
37
40
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: background-reviewer
3
+ description: Independently inspect the actual task and produced files.
4
+ tools: Read, Glob, Grep
5
+ ---
6
+ Read the user's task and the actual source and output files. Check the facts and requested behavior independently; the worker's conclusion is not evidence. State concrete discrepancies and remaining uncertainties. Do not edit files, approve permissions, rerun the worker or send external messages.
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: background-worker
3
+ description: Execute the explicitly delegated file task and preserve actual artifacts.
4
+ tools: Read, Write, Glob, Grep
5
+ ---
6
+ Read the original task and relevant source files. Work only within its authorized scope. Save the requested artifact and report its path. Retain tool failures and permission denials in your account of the outcome. Do not substitute a new task for an uncertain original execution. Do not send external messages.
@@ -0,0 +1,42 @@
1
+ import { createBackgroundClient } from '../../dist/sdk/index.js'
2
+ import { resolve } from 'node:path'
3
+ import { setTimeout as delay } from 'node:timers/promises'
4
+
5
+ // Copy agents/*.md to the task project's .ccl/agents/ before running this example.
6
+ // Usage: node worker-reviewer.mjs PROJECT CLI_PATH STABLE_TASK_KEY ARTIFACT_PATH
7
+ // The project contains task.md (the user's bounded work request).
8
+ const [project, cliPath, taskKey, artifact] = process.argv.slice(2)
9
+ if (!project || !cliPath || !taskKey || !artifact) throw new Error('Expected PROJECT CLI_PATH STABLE_TASK_KEY ARTIFACT_PATH')
10
+ const client = createBackgroundClient({ cwd: resolve(project), cliPath: resolve(cliPath),
11
+ launch: { extraArgs: ['--allowed-tools', 'Read,Write,Glob,Grep'] } })
12
+ async function wait(runId) {
13
+ const deadline = Date.now() + 10 * 60_000
14
+ let cursor
15
+ while (Date.now() < deadline) {
16
+ let page
17
+ do {
18
+ if (Date.now() >= deadline) throw new Error(`Observation deadline elapsed; query original run ${runId}.`)
19
+ page = await client.readRunEvents({ runId, cursor })
20
+ // This demo only observes. A real assistant handles this page's events
21
+ // and persists its cursor after its own processing succeeds.
22
+ cursor = page.nextCursor
23
+ } while (page.hasMore)
24
+ const view = await client.getRun({ runId })
25
+ if (view.status !== 'found') throw new Error(`Original run unavailable: ${runId}`)
26
+ if (view.snapshot.state === 'waiting_permission') throw new Error(`Explicit permission needed; inspect ${runId}. No approval was invented.`)
27
+ if (['succeeded','completed_with_issues','failed','interrupted','cancelled'].includes(view.snapshot.state)) return view
28
+ if (view.snapshot.state !== 'queued' && view.observation.executorLock === 'free') throw new Error(`Executor unavailable: ${runId}; query its recorded operations before continuing`)
29
+ await delay(1000)
30
+ }
31
+ throw new Error(`Observation deadline elapsed; the run may still be executing. Query original run ${runId}.`)
32
+ }
33
+ const worker = await client.submitRun({ requestKey: `${taskKey}:worker`, spec: { agent: 'background-worker', maxTurns: 12,
34
+ prompt: `Working directory: ${resolve(project)}. Read ${resolve(project, 'task.md')} and perform its authorized work. Save the requested artifact at ${resolve(project, artifact)}. Report actual results and tool failures honestly.` } })
35
+ console.log(JSON.stringify({ accepted: worker.runId, sessionId: worker.sessionId }))
36
+ const work = await wait(worker.runId)
37
+ const reviewer = await client.submitRun({ requestKey: `${taskKey}:reviewer`, spec: { agent: 'background-reviewer', maxTurns: 8,
38
+ prompt: `Independently review the task in working directory ${resolve(project)}. Read ${resolve(project, 'task.md')} and the actual artifact ${resolve(project, artifact)}; compare the requested facts with the files. Do not accept the worker's prose as evidence. Worker execution facts: ${JSON.stringify({ runId: worker.runId, state: work.snapshot.state, toolErrors: work.snapshot.toolErrors, deniedToolUseIds: work.snapshot.deniedToolUseIds })}. Start with these supplied paths and the source files named by the task. Keep investigation within this task directory unless concrete task evidence requires another location; do not search parent directories for hypothetical evidence. Report what you verified, actual discrepancies, and limits of the available evidence. A missing before-run baseline limits a historical claim; it is not itself evidence that the source was modified and does not require a filesystem-wide investigation. Do not change files or send external messages.` } })
39
+ if (reviewer.sessionId === worker.sessionId) throw new Error('Worker and reviewer unexpectedly share a session')
40
+ const review = await wait(reviewer.runId)
41
+ // This is the reviewer's result, not a claim that an external recipient received it.
42
+ console.log(JSON.stringify({ worker: work, reviewer: review }, null, 2))
@@ -0,0 +1,44 @@
1
+ import { mkdirSync, realpathSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { z } from 'zod/v4'
5
+ import { createDurableCommandAdapter, createOperationHost, createSdkMcpServer, tool } from '../../dist/sdk/index.js'
6
+
7
+ /**
8
+ * Persistent local-file example. The embedding assistant supplies its CURRENT
9
+ * permission callback; no historical allow decision is reused after restart.
10
+ * Run after building the SDK, or use the copy shipped in the npm package.
11
+ */
12
+ export function durableCommandRecoveryServer(root, authorize) {
13
+ if (typeof authorize !== 'function') throw new Error('Supply the current host authorization callback')
14
+ mkdirSync(root, { recursive: true, mode: 0o700 })
15
+ const directory = realpathSync(root)
16
+ const identity = () => {
17
+ const stat = statSync(directory)
18
+ if (!stat.isDirectory()) throw new Error('Example target is no longer a directory')
19
+ return `${directory}:${stat.dev}:${stat.ino}`
20
+ }
21
+ const script = fileURLToPath(new URL('./receipt-command.mjs', import.meta.url))
22
+ const host = createOperationHost({ storeDir: join(directory, 'operations'), scope: directory, authorize,
23
+ resolveAdapter: async ref => ref === 'local-example' ? createDurableCommandAdapter({
24
+ adapterId: ref, adapterVersion: '1', targetIdentity: identity(),
25
+ execute: { cmd: process.execPath, args: [script, join(directory, 'effects'), 'execute'], cwd: directory, timeoutMs: 5000 },
26
+ query: { cmd: process.execPath, args: [script, join(directory, 'effects'), 'query'], cwd: directory, timeoutMs: 5000 },
27
+ verifyIdentity: async () => ({ targetIdentity: identity(), adapterVersion: '1' }),
28
+ // Query after an unknown effect. Do not advertise an external service's
29
+ // replay guarantee merely because this local demonstration deduplicates.
30
+ }) : undefined,
31
+ })
32
+ return createSdkMcpServer({ name: 'durable-commands', tools: [
33
+ tool('execute', 'Accept and execute a local example; preserve operationId even if its reply is unknown',
34
+ { input: z.json(), requestKey: z.string().min(1) }, async (args, extra) => {
35
+ const accepted = await host.submitOperation({ requestKey: args.requestKey, adapterRef: 'local-example', input: args.input })
36
+ return host.executeOperation({ operationId: accepted.operationId, expectedRevision: accepted.revision, signal: extra.signal })
37
+ }),
38
+ tool('get', 'Read saved execution and result, including after the original host exits',
39
+ { operationId: z.string() }, args => host.getOperation(args), { annotations: { readOnlyHint: true } }),
40
+ tool('query', 'Query the original effect and save its verified receipt; never repeat the write',
41
+ { operationId: z.string(), expectedRevision: z.number().int(), actionId: z.string().min(1) },
42
+ (args, extra) => host.reconcileOperation({ ...args, signal: extra.signal })),
43
+ ] })
44
+ }
@@ -0,0 +1,40 @@
1
+ // Deterministic local example, not a production business service.
2
+ import { createHash } from 'node:crypto'
3
+ import { mkdir, readFile, writeFile, rename } from 'node:fs/promises'
4
+ import { join } from 'node:path'
5
+ const [root, action] = process.argv.slice(2)
6
+ let raw = ''
7
+ for await (const chunk of process.stdin) raw += chunk
8
+ const request = JSON.parse(raw)
9
+ if (action === 'identity') {
10
+ process.stdout.write(JSON.stringify({ targetIdentity: root, adapterVersion: '1' }))
11
+ process.exit(0)
12
+ }
13
+ const key = createHash('sha256').update(`${request.targetIdentity}:${request.requestKey}`).digest('hex')
14
+ const dir = join(root, key)
15
+ const path = join(dir, 'receipt.json')
16
+ const reply = body => process.stdout.write(JSON.stringify({ ...request, input: undefined, ...body }))
17
+ const existing = async () => {
18
+ try {
19
+ const record = JSON.parse(await readFile(path, 'utf8'))
20
+ if (record.inputDigest !== request.inputDigest) return reply({ status: 'unknown', reason: 'Key/input mismatch' })
21
+ reply({ status: 'applied', receipt: record.receipt, result: record.result })
22
+ } catch { reply({ status: 'unknown', reason: 'No complete receipt; an old action may still be running' }) }
23
+ }
24
+ await mkdir(root, { recursive: true })
25
+ if (action === 'query') await existing()
26
+ else if (action === 'execute') {
27
+ try { await mkdir(dir) } catch (error) {
28
+ if (error.code !== 'EEXIST') throw error
29
+ await existing()
30
+ process.exit(0)
31
+ }
32
+ // Exclusive mkdir is the example service's deduplication claim. An interrupted claim
33
+ // without a receipt stays unknown; it never authorizes another write.
34
+ const record = { inputDigest: request.inputDigest, receipt: { key, writes: 1 }, result: request.input }
35
+ await writeFile(join(dir, 'effect.json'), JSON.stringify(request.input))
36
+ await writeFile(join(dir, 'receipt.tmp'), JSON.stringify(record))
37
+ await rename(join(dir, 'receipt.tmp'), path)
38
+ if (request.input?.loseReply) process.exit(9)
39
+ await existing()
40
+ } else throw new Error('Expected execute or query')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@margay/ccl-core",
3
- "version": "1.5.0-alpha.4",
3
+ "version": "1.5.1-alpha.2",
4
4
  "description": "Core runtime extracted from Claude Code (ccl) for Margay integration",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "type": "module",
@@ -27,6 +27,14 @@
27
27
  "sdk/API.md",
28
28
  "docs/runtime-resources.md",
29
29
  "docs/hook-input-provenance.md",
30
+ "docs/command-tool-recovery.md",
31
+ "docs/mcp-tool-recovery.md",
32
+ "docs/background-runs.md",
33
+ "examples/background-runs/worker-reviewer.mjs",
34
+ "examples/background-runs/agents/background-worker.md",
35
+ "examples/background-runs/agents/background-reviewer.md",
36
+ "examples/command-recovery/durable-server.mjs",
37
+ "examples/command-recovery/receipt-command.mjs",
30
38
  "docs/shared-skill-resources.md",
31
39
  "examples/runtime-resources",
32
40
  "docs/local-analytics-quickstart.md",
@@ -41,7 +49,12 @@
41
49
  "examples/assistant",
42
50
  "docs/releases/1.5.0-alpha.3.md",
43
51
  "docs/README.md",
44
- "docs/releases/1.5.0-alpha.4.md"
52
+ "docs/releases/1.5.0-alpha.4.md",
53
+ "docs/releases/1.5.0-alpha.5.md",
54
+ "docs/releases/1.5.0-alpha.6.md",
55
+ "docs/releases/1.5.1-alpha.md",
56
+ "docs/releases/1.5.1-alpha.1.md",
57
+ "docs/releases/1.5.1-alpha.2.md"
45
58
  ],
46
59
  "engines": {
47
60
  "node": ">=22.19.0"
@@ -52,6 +65,7 @@
52
65
  "prepublishOnly": "bun run build",
53
66
  "test": "vitest run",
54
67
  "test:smoke": "vitest run -c vitest.smoke.config.ts",
68
+ "test:operations": "vitest run __tests__/operations",
55
69
  "build:sdk": "tsc -p tsconfig.sdk.json"
56
70
  },
57
71
  "dependencies": {