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

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 (65) hide show
  1. package/dist/cli.js +108954 -103254
  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/cancellation.d.ts +15 -0
  7. package/dist/sdk/cancellation.js +18 -0
  8. package/dist/sdk/commandAdapter.d.ts +77 -0
  9. package/dist/sdk/commandAdapter.js +180 -0
  10. package/dist/sdk/commandProcess.d.ts +27 -0
  11. package/dist/sdk/commandProcess.js +130 -0
  12. package/dist/sdk/executionGuidance.d.ts +7 -0
  13. package/dist/sdk/executionGuidance.js +37 -0
  14. package/dist/sdk/index.d.ts +14 -0
  15. package/dist/sdk/index.js +9 -0
  16. package/dist/sdk/mcpServer.d.ts +9 -5
  17. package/dist/sdk/mcpServer.js +51 -9
  18. package/dist/sdk/operations/command.d.ts +24 -0
  19. package/dist/sdk/operations/command.js +48 -0
  20. package/dist/sdk/operations/host.d.ts +3 -0
  21. package/dist/sdk/operations/host.js +376 -0
  22. package/dist/sdk/operations/json.d.ts +6 -0
  23. package/dist/sdk/operations/json.js +24 -0
  24. package/dist/sdk/operations/reference.d.ts +15 -0
  25. package/dist/sdk/operations/reference.js +19 -0
  26. package/dist/sdk/operations/store.d.ts +26 -0
  27. package/dist/sdk/operations/store.js +300 -0
  28. package/dist/sdk/operations/types.d.ts +171 -0
  29. package/dist/sdk/operations/types.js +8 -0
  30. package/dist/sdk/pendingApprovalIdentity.d.ts +35 -0
  31. package/dist/sdk/pendingApprovalIdentity.js +27 -0
  32. package/dist/sdk/permissionDelegation.d.ts +19 -0
  33. package/dist/sdk/permissionDelegation.js +11 -0
  34. package/dist/sdk/query.d.ts +3 -1
  35. package/dist/sdk/query.js +142 -30
  36. package/dist/sdk/queryHandle.js +28 -8
  37. package/dist/sdk/queryLifecycle.d.ts +19 -0
  38. package/dist/sdk/queryLifecycle.js +93 -0
  39. package/dist/sdk/queryLifecycleTypes.d.ts +160 -0
  40. package/dist/sdk/queryLifecycleTypes.js +47 -0
  41. package/dist/sdk/scheduledOccurrenceTypes.d.ts +103 -0
  42. package/dist/sdk/scheduledOccurrenceTypes.js +12 -0
  43. package/dist/sdk/types.d.ts +21 -3
  44. package/docs/background-runs.md +229 -0
  45. package/docs/command-tool-recovery.md +62 -0
  46. package/docs/dev-quickstart.md +29 -7
  47. package/docs/error-recovery-usage.md +18 -0
  48. package/docs/hook-input-provenance.md +77 -0
  49. package/docs/index.md +2 -0
  50. package/docs/mcp-tool-recovery.md +82 -0
  51. package/docs/releases/1.5.0-alpha.5.md +26 -0
  52. package/docs/releases/1.5.0-alpha.6.md +15 -0
  53. package/docs/releases/1.5.1-alpha.1.md +11 -0
  54. package/docs/releases/1.5.1-alpha.2.md +17 -0
  55. package/docs/releases/1.5.1-alpha.3.md +23 -0
  56. package/docs/releases/1.5.1-alpha.md +22 -0
  57. package/docs/runtime-resources.md +6 -3
  58. package/examples/background-runs/agents/background-reviewer.md +6 -0
  59. package/examples/background-runs/agents/background-worker.md +6 -0
  60. package/examples/background-runs/worker-reviewer.mjs +42 -0
  61. package/examples/command-recovery/durable-server.mjs +44 -0
  62. package/examples/command-recovery/receipt-command.mjs +40 -0
  63. package/package.json +17 -2
  64. package/sdk/API.md +206 -1
  65. package/sdk/README.md +6 -0
@@ -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,23 @@
1
+ # 1.5.1-alpha.3
2
+
3
+ 本预发布版在 [1.5.1-alpha.2](1.5.1-alpha.2.md) 基础上增加宿主取消来源契约(#58)和视觉输出预算配置及有界恢复(#59)。使用明确版本或 `alpha` 标签安装;npm 发布状态以 registry 为准,`latest` 保持不变。
4
+
5
+ ## 宿主停止与权限拒绝
6
+
7
+ SDK `handle.interrupt()`、权限回调超时/异常和会话退役会保留宿主取消来源,不再被误写成用户拒绝或用户偏好。普通工具 Deny 继续保留拒绝依据;取消不撤销其他授权,也不授予新权限。
8
+
9
+ `interrupt()` 可接收 `HostCancellation`,权限回调 deny 可附带 `cancellation`;来源也会出现在 `permission_resolved` 和原生 transcript。旧客户端只返回 `deny('Conversation stopped')` 时仍按拒绝处理,需迁移到 interrupt 或显式 cancellation。AskUserQuestion 的非终止 unavailable/timeout 交互保持原行为,不制造用户答案。
10
+
11
+ 取消与结果返回竞态时保留已完成事实,迟到的 allow 不启动已取消的工具。成功停止 ACK 只确认控制请求被接受;拒绝、畸形 ACK、断连或10秒无 ACK 会明确失败,停止结果可能未知。取消不回滚已发生的外部效果,强杀或close也不保证最后消息持久化;恢复前仍需核对事实,不能盲目重放。
12
+
13
+ ## 视觉输出预算
14
+
15
+ CLI 环境变量 `CCL_VISUAL_MAX_OUTPUT_TOKENS` 默认8192;可选 `CCL_VISUAL_RECOVERY_MAX_OUTPUT_TOKENS` 显式授权一次更高预算恢复,默认不启用。SDK通过已有 `options.env` 配置。值必须是正安全整数,恢复预算必须大于初始预算;非法配置在读图或模型请求前失败。Agent模型不能通过工具参数改预算或覆盖视觉模型。
16
+
17
+ 现有模型输出上限和共享上下文限制仍会约束请求。只有完整终态的输出限额失败、无拒绝/过滤、有效预算确实增加且恢复额度未用时,才会用原模型、协议、提示和已授权图片字节重试。预算被模型上限截平时不发无效重试。空响应与输出限额恢复共用一次内容重试,另允许此前一次能力上下文刷新;至多3次推理共用900秒期限。
18
+
19
+ 成功和失败均记录尝试历史、request ID、请求/有效预算、限制来源、重试关联、耗时及累计已知usage。缺少input/output用量时 `usageComplete=false`,不把未知成本当作零。最终未完成不会报视觉通过。成功结果提示也不再无条件暗示此前发生过失败和恢复。
20
+
21
+ 提高预算可能增加成本,不保证提升视觉准确性。原三图同提示的真实模型对照中,8192和16384都单次完成,未复现旧截断;本次真实运行未触发自动恢复,恢复边界由CLI/SDK接线及单元测试验证。模型仍有精确颜色判断误差,完整输出不能替代业务验收。
22
+
23
+ 使用方式及兼容边界见 [SDK API](../../sdk/API.md)。CLI与SDK保持同版本;已运行进程不会热更新,正常结束后新会话使用新版。未改默认模型、权限策略或业务数据,无需数据迁移。仍不承诺跨外部系统exactly-once、模型始终正确理解图像或任意终止都能回收已发生的副作用。
@@ -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.3",
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,13 @@
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",
58
+ "docs/releases/1.5.1-alpha.3.md"
45
59
  ],
46
60
  "engines": {
47
61
  "node": ">=22.19.0"
@@ -52,6 +66,7 @@
52
66
  "prepublishOnly": "bun run build",
53
67
  "test": "vitest run",
54
68
  "test:smoke": "vitest run -c vitest.smoke.config.ts",
69
+ "test:operations": "vitest run __tests__/operations",
55
70
  "build:sdk": "tsc -p tsconfig.sdk.json"
56
71
  },
57
72
  "dependencies": {
package/sdk/API.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  原生 `UserPromptSubmit` 的来源、消息 ID、排队与恢复语义见 [输入身份契约](../docs/hook-input-provenance.md);该文档明确区分源码候选与已发布版本。
8
8
 
9
+ `1.5.1-alpha` 接口 `createBackgroundClient` 及独立后台生命周期见 [持久后台接入契约](../docs/background-runs.md)。其事件返回 `{events,nextCursor,hasMore}`,支持 `limit` 分页;不要套用本文历史会话接口的默认 CLI 定位或内存回调机制。工具操作恢复另见 [命令适配](../docs/command-tool-recovery.md) 和 [MCP 恢复](../docs/mcp-tool-recovery.md)。
10
+
9
11
  ---
10
12
 
11
13
  ## 1. 引入与运行要求
@@ -111,6 +113,19 @@ Query 控制方法,`pid` 用于宿主监控。
111
113
  终止进程并拒绝未完成轮次。权限回调可等待宿主决定,interrupt/close 会取消
112
114
  等待;返回 `pending` 仍表示持久挂起,后续用 `resumeWithDecision` 恢复。
113
115
 
116
+ **后台工具与可交互前台是两个层次。** `Agent(run_in_background: true)` 会先返回
117
+ 启动结果,但 CLI 的 print/SDK 路径可能继续等待该输入所属的后台工作及完成通知,
118
+ 才发出这一轮的 `result`。`Session` 按该终态串行发送下一条输入,因此收到
119
+ `assistant_text` 中的接收确认,不代表同一 Session 的下一次 `prompt()` 已可执行。
120
+ 不要把 `Turn.result` 当作“前台确认已显示”事件,也不要用 `steer()` 充当无损的
121
+ 后台移交;它会中断当前轮。
122
+
123
+ 若应用要求后台任务运行期间前台仍可对话,可将工作交给**独立的 worker Session**,
124
+ 主会话不等待 worker 的 `Turn.result`。由应用保存任务与 worker 的关联,消费其结果,
125
+ 再将实际完成结果交给前台。多个 Session 是独立执行上下文,需要明确传递任务、
126
+ 权限与材料引用;进程中的 Session 本身不是持久队列,应用仍需处理保存后派发失败、
127
+ 退出恢复与结果未知。此路径的接口并发可用性不保证模型服务的前台响应时延。
128
+
114
129
  ---
115
130
 
116
131
  ## 3. 事件参考(SDKEvent)
@@ -134,6 +149,39 @@ Query 控制方法,`pid` 用于宿主监控。
134
149
  `result` 只结束对应 `turnId`,后续仍可能有输入。`suspended`、fatal `error`、
135
150
  关闭或输入 EOF 结束流式 query;不要把首个 result 当作常驻 Session 已关闭。
136
151
 
152
+ ### 子任务生命周期事件
153
+
154
+ 启用 `includeRaw: true`,从 `event.type === 'raw'` 的 `event.message` 读取
155
+ 宿主 `type: 'system'` 事件。`createSession` 可通过 `onEvent` 消费同样的事件;
156
+ 这些事件不会另外转换成同名的顶层 `SDKEvent`。
157
+
158
+ | subtype | 主要字段 | 含义 |
159
+ |---|---|---|
160
+ | `task_started` | `session_id`, `task_id`, `tool_use_id?`, `description`, `task_type?`, `uuid` | 进程内已登记子任务;不等于业务绑定已持久保存 |
161
+ | `task_progress` | 上述身份字段、`usage`, `last_tool_name?`, `summary?` | 可选进度,不是完成证明 |
162
+ | `task_notification` | 身份字段、`status`, `output_file`, `summary`, `usage?` | 终态通知;`status` 为 `completed`、`failed` 或 `stopped` |
163
+
164
+ 保存业务执行尝试与实际 `session_id/tool_use_id/task_id` 的关联。内部取消状态
165
+ `killed` 在此映射为 `stopped`。模型文本或工具正文中的同名 XML 不应当作宿主事件;
166
+ 即使任务 `completed`,仍需核对业务产物和实际交付回执。
167
+
168
+ 从 `1.5.0-alpha.6` 起,`TaskOutput` 抢先读取终态、抑制后续模型通知的路径也会
169
+ 补发结构化 SDK 终态;已经通知、重复读取或仍在运行时不新增该终态。这一修复
170
+ 不提供持久投递保证:事件队列、任务表主要在进程内,前台忙碌可能延后通知,
171
+ 退出后没有消费者确认游标或保证重放。应用应保存收到的事件并处理重复、缺失和冲突。
172
+
173
+ 当前没有按父会话和原 `tool_use_id` 查询历史运行的公开 SDK 接口。`TaskOutput`
174
+ 是已标记弃用的模型工具,只查询现有任务表;`stopTask(taskId)` 是当前进程内的
175
+ 取消控制,不是查询,也不能撤销副作用。`SubagentStart` 不提供原 Agent 调用 ID。
176
+ 已保存的父会话 Agent 工具结果可提供 `toolUseResult.agentId` 与
177
+ `message.content[].tool_use_id` 的有限取证关联;记录缺失、日志截断或进程退出
178
+ 应保留为未知,不直接重派。`resume` 会继续执行,不能用作无副作用的状态查询。
179
+
180
+ 普通后台 agent 默认不能弹出权限 UI;规则和权限 hook 未放行、仍需要交互的操作
181
+ 通常返回工具拒绝。不能期待所有权限不足都表现为统一的任务 `waiting` 或 `failed`。
182
+ 显式允许提示的配置另按实际权限事件处理。普通 worker 也不获得嵌套 Agent 工具;
183
+ 独立 reviewer 应由主协调入口派发,保持前台与 worker Session 的分工。
184
+
137
185
  ### 恢复结果与用户介入
138
186
 
139
187
  `result.recovery` 是可选扩展,与过程诊断的 `diagnostic.recovery.phase` 不同:
@@ -165,7 +213,7 @@ type CanUseToolFn = (
165
213
 
166
214
  type CanUseToolResult =
167
215
  | { behavior: 'allow'; updatedInput?: Record<string, unknown> } // 放行;updatedInput=改写输入(modify)
168
- | { behavior: 'deny'; message?: string; interrupt?: boolean } // 拒绝;interrupt=true 同时中断本回合
216
+ | { behavior: 'deny'; message?: string; interrupt?: boolean; cancellation?: HostCancellation }
169
217
  | { behavior: 'pending' } // 挂起走异步审批(§5)
170
218
  ```
171
219
 
@@ -174,6 +222,12 @@ type CanUseToolResult =
174
222
  - `allow` 未提供 `updatedInput`(或提供空对象)时按原始 input 执行。
175
223
  - 未配置 `canUseTool` 时 SDK 不启用权限转发(工具按 CLI 自身权限规则走);若此时 CLI 仍发来请求,按 deny 兜底。
176
224
 
225
+ 宿主停止与权限拒绝是不同事件。停止生成可直接 `await handle.interrupt()`;它取消未完成权限等待,默认来源为 `{origin:'host',reason:'cancelled'}`。也可传完整的 `HostCancellation`,reason 支持 `cancelled`、`timeout`、`connection_closed`、`callback_error`、`permission_denied`。`origin` 表示控制通道来源,不推断是谁点击了停止。原始 stream-json 请求为 `{type:'control_request',request_id,request:{subtype:'interrupt',cancellation}}`,省略 cancellation 同样表示宿主取消。
226
+
227
+ 需要在权限回调结束等待时,可返回 `{behavior:'deny',message:'Conversation stopped',cancellation:{origin:'host',reason:'cancelled'}}`。这结束当前轮,工具不执行;`permission_resolved.cancellation` 与原生 transcript 的 `toolUseResult.cancellation` 保留来源。回调超时/异常自动携带 timeout/callback_error。AskUserQuestion 的 timed_out/interaction_unavailable 继续按已有非终止交互失败处理,不制造用户答案。未带 cancellation 的普通 deny 仍是拒绝;deny+interrupt 记录 permission_denied 并保留原拒绝依据,不推断人工决定。旧客户端的 `deny('Conversation stopped')` 仍是拒绝,宿主应迁移到 interrupt 或显式 cancellation,CCL 不通过文字猜测来源。
228
+
229
+ interrupt 的 Promise 只在匹配成功 ACK 后完成;拒绝、畸形 ACK、断连会 reject,10 秒没有 ACK 也会 reject,表示停止结果未知。还须观察当前轮的 result,不能把 ACK 当作工具已回滚。迟到的 allow 不会启动已取消的工具。已完成结果保留;在途操作缺少结果不能证明没有副作用,恢复时先核对事实,不能盲目重放。取消本身不撤销其他已存在授权,也不授予新权限。`end_session` 支持优雅退役并记录 connection_closed;直接强杀进程或 `close()` 是资源清理,不保证最后一条消息已持久化,需按未知结果恢复。
230
+
177
231
  ---
178
232
 
179
233
  ## 5. pending 挂起 / 跨进程恢复(审批闭环)
@@ -216,6 +270,35 @@ query(...)
216
270
 
217
271
  ## 6. QueryOptions 字段表
218
272
 
273
+ ### 共用执行指引与宿主定制
274
+
275
+ 普通 CLI 和 SDK 默认系统指令共用运行层的工具执行指导:可用且适合时优先专用文件工具;脚本、构建、测试、转换及专用工具不提供的元数据查询使用 Bash。缺工具可采用已授权等价方案,权限拒绝不能成为换工具绕过限制的理由。这是 agent 指引,不是工具调用或模型遵循的强制保证。
276
+
277
+ 保留运行层默认指导并追加岗位或用户配置时,SDK 使用 `appendSystemPrompt`,CLI 使用 `--append-system-prompt` 或 `--append-system-prompt-file`。内容由宿主组合和维护,不改写用户 persona 文件,也不自动同步两个应用的配置。若 SDK 同时提供 `systemPrompt`,追加内容接在替换后的提示之后。仅在确实需要自管全部系统指令时使用 `systemPrompt` / `--system-prompt`;它们会替换默认系统指令,Bash 等工具的说明仍来自实际工具 schema。没有另一个命名 preset 注册或版本协商接口。
278
+
279
+ 需要保留自定义 `systemPrompt`、同时复用运行层执行指引时,可显式使用 `getExecutionGuidance(toolNames)`。**此导出从 `1.5.0-alpha.5` 提供**;内容随同一部署包版本更新,不另建 preset 版本号。SDK 和 CLI 应使用同一固定版本的包,旧安装包不一定具备此导出。
280
+
281
+ ```ts
282
+ import { getExecutionGuidance, query } from '@margay/ccl-core/sdk'
283
+
284
+ const toolNames = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash']
285
+ const execution = getExecutionGuidance(toolNames)
286
+ const result = query(prompt, {
287
+ systemPrompt: personaText, // 原岗位身份,由应用保存;指引更新不改写它
288
+ appendSystemPrompt: [execution, userDefaults].filter(Boolean).join('\n\n'),
289
+ extraArgs: ['--tools', toolNames.join(',')],
290
+ canUseTool, // 沿用应用自身权限处理;生成指引不会授予工具权限
291
+ })
292
+ for await (const event of result) { /* 消费实际结果 */ }
293
+ ```
294
+
295
+ `toolNames` 使用会话实际暴露的规范工具名,非显示别名或权限规则表达式;只有提供的内置文件工具/Bash 才生成对应分工说明,其它工具仍遵循通用的“按已有接口执行、必要时定向调查”规则。空列表只返回通用边界。工具集合变化时由应用重新生成;函数不探测文件、不加载技能、不注册工具、不修改配置、不自动注入会话。普通默认系统提示已经消费同一来源,无需再追加一次。
296
+
297
+ 直接 CLI 的自定义 persona 也可消费同一份文字:应用通过上述导出生成一个自己管理的文本文件,再使用 `--system-prompt-file <persona文件> --append-system-prompt-file <执行指引文件>`。不依赖复制某个内部提示文件,也不需要 persona 改成默认 coding 身份。显式完整替换且不追加时,仍保持既有替换契约。REPL/embedded 等特殊工具表面由调用方传入实际可直接使用的工具名,不能把本函数当作特殊模式的全部运行指令。
298
+
299
+ 宿主应固定部署包版本并检验实际模型输入,而非从工具已注册推断默认指导存在。升级依赖后还需重启实际运行的 CLI/SDK 进程。Desktop 的资源管理、配置保存和 UI 联验仍由宿主负责。
300
+
301
+
219
302
  | 字段 | 类型 | 默认 | 说明 |
220
303
  |---|---|---|---|
221
304
  | `cwd` | string | 继承宿主 | 会话工作目录 |
@@ -374,6 +457,16 @@ for await (const ev of query('用 load_csv 读 /data/x.csv 并报告行列', {
374
457
  - handler 在 **host 进程**执行(可访问 host 的文件/网络/状态)。
375
458
  - 每次调用先过 `canUseTool`(可 allow/deny/pending);deny → 工具不跑。
376
459
 
460
+ 工具发现元数据:`tool` 的第五个参数支持
461
+ `{ searchHint?: string, alwaysLoad?: boolean, annotations?: Record<string, unknown> }`。
462
+ `searchHint` 作为 `anthropic/searchHint` 传给工具搜索,补充名称和描述之外的搜索词;
463
+ SDK 在 `tools/list` 中转发此字段。
464
+ 新增的 `alwaysLoad: true` 作为 `anthropic/alwaysLoad` 传给 CLI,让该工具完整 schema
465
+ 进入首请求,无须先由 ToolSearch 发现。省略或传 `false` 沿用 CLI 的工具发现策略,
466
+ 并非强制隐藏工具。这些选项不授予执行权限,也不保证模型选择该工具;
467
+ `canUseTool` 的拒绝仍阻止 handler 执行。仅对确实需要首请求展示的工具显式启用,
468
+ 避免无条件扩大每次请求。
469
+
377
470
  ### 13.2 Token 级流式(G3)
378
471
  ```ts
379
472
  for await (const ev of query('写一段', { includePartialMessages: true })) {
@@ -527,3 +620,115 @@ For shared updates, retain immutable version directories and select a new root
527
620
  explicitly. Rediscover IDs after upgrading from random SDK wrapper names or after
528
621
  moving a skill root; transcript resume does not restore resource bindings.
529
622
  See [shared skill identity, reload and publisher responsibilities](../docs/shared-skill-resources.md).
623
+ # Query loop observations
624
+
625
+ `SDKEvent` includes `{ type: 'query_lifecycle', lifecycle, sessionId }`. The
626
+ `lifecycle.record.phase === 'terminal'` observation closes one host query loop;
627
+ it does not replace the outer `result` event or verify a business outcome. Native
628
+ Stop hooks carry the same checkpoint identity in their optional `query` field.
629
+ See [input and query correlation](../docs/hook-input-provenance.md) for fields,
630
+ failure states and the distinction between accepted and adapter-dispatched inputs.
631
+
632
+ `readQueryLifecycle({ transcriptPath, sessionId, queryId })` reads only the given
633
+ file and returns `found` with `complete`/`incomplete`, `not_found`, or `unavailable`.
634
+ Use the original query ID when processing late or replayed notifications. Missing
635
+ records do not authorize re-execution of tools.
636
+
637
+ Keep ownership of the public query handle and close it when post-result controls
638
+ are no longer needed:
639
+
640
+ ```js
641
+ const handle = query(prompt, options)
642
+ try {
643
+ for await (const event of handle) {
644
+ if (event.type === 'query_lifecycle') recordObservation(event.lifecycle)
645
+ // Continue consuming the outer result; a Stop checkpoint is not terminal.
646
+ }
647
+ } finally {
648
+ await handle.close()
649
+ }
650
+ ```
651
+
652
+ ## Write without replacing an existing target (1.5.1-alpha candidate)
653
+
654
+ The native `Write` schema accepts optional `mode: "overwrite" | "create_only"`.
655
+ Omitting it keeps the existing read-before-overwrite behavior. For an export that
656
+ must preserve every existing target, use:
657
+
658
+ ```json
659
+ {"file_path":"/absolute/path/new-export.txt","content":"Export content\n","mode":"create_only"}
660
+ ```
661
+
662
+ `create_only` refuses files (including previously read or empty files), directories,
663
+ symbolic links and dangling links. Publication is exclusive even if another process
664
+ creates the target after validation. On conflict, choose another authorized path;
665
+ reading the old target or approval to write does not convert this into overwrite.
666
+ The model receives the same schema through CLI/TUI and SDK. The headless workflow
667
+ `tool("Write", input)` also honors the mode, with its existing permission policy.
668
+
669
+ The implementation stages complete UTF-8 content beside the destination and links
670
+ it into place without replacing a directory entry. Unsupported hard links fail
671
+ without falling back to overwrite. A pre-publication write/flush/cancellation
672
+ failure does not create the target; diagnostics report any temporary residue.
673
+ After publication, cancellation and post-write cleanup/diagnostic failures do not
674
+ undo the created file. The result reports its canonical path and any warnings.
675
+ An interrupted consumer may receive no result; missing output is not proof that
676
+ the file was never created. This is not a power-loss, network-filesystem or durable
677
+ operation-replay guarantee, and does not introduce a new permission gate.
678
+
679
+ Check the actual tool schema for `mode`; an older installation with the same
680
+ candidate version string may not have it. An existence precheck followed by normal
681
+ Write is not an equivalent no-replacement contract.
682
+
683
+ ## Visual specialist output budget (1.5.1-alpha.3)
684
+
685
+ The synchronous `Agent` specialist `visual-ui-analysis` accepts its existing
686
+ `prompt` and `image_paths`. The host controls its output allowance; the model
687
+ cannot set it through Agent arguments or override the selected vision model.
688
+ CLI processes read `CCL_VISUAL_MAX_OUTPUT_TOKENS` (default `8192`) and optional
689
+ `CCL_VISUAL_RECOVERY_MAX_OUTPUT_TOKENS` (disabled by default). SDK hosts use the
690
+ same variables through `options.env`:
691
+
692
+ ```ts
693
+ const handle = query({
694
+ prompt: 'Inspect the supplied rendered slides with the visual specialist.',
695
+ options: {
696
+ env: {
697
+ CCL_VISUAL_MAX_OUTPUT_TOKENS: '16384',
698
+ CCL_VISUAL_RECOVERY_MAX_OUTPUT_TOKENS: '32768',
699
+ },
700
+ // Supply your normal cwd, credentials and permission callback as appropriate.
701
+ },
702
+ })
703
+ ```
704
+
705
+ Values must be decimal positive safe integers; the recovery value must exceed
706
+ the initial value. Invalid configuration fails before image reads or model I/O.
707
+ The existing provider/configured output limits and shared context budget still
708
+ cap each request. Unknown limits remain unknown; a larger allowance is not a
709
+ claim that the model supports it. Reasoning and visible text share the allowance.
710
+
711
+ A terminal output-limit failure can use the higher allowance once, only when
712
+ its effective budget increases. If a model caps both requests at 8192, no recovery
713
+ request is sent. Recovery reuses the selected model, protocol, prompt and already
714
+ authorized image bytes. It shares one content retry with normal-terminal empty
715
+ response recovery. A capability-context refresh is separately allowed once before
716
+ content recovery, so at most three inference attempts share one 900-second
717
+ operation deadline. Cancellation, refusal, filtering, incomplete streams and an
718
+ exhausted recovery do not trigger a further content retry.
719
+
720
+ The returned visual facts retain every attempt's request ID, requested and
721
+ effective budget, safe `requestBudget` limit snapshot, duration, and one-based
722
+ `retryOfAttempt` / `retryOfRequestId` links. Both completed and failed inspections
723
+ include `visualDiagnostics.usage`, `usageComplete`, and total `durationMs` (from
724
+ post-image-load resolution to terminal outcome). `usageComplete` concerns reported
725
+ input/output counts; optional cache breakdowns may be absent. Missing usage is
726
+ not proof of zero cost: totals sum known values, and `usageComplete: false` marks
727
+ the incomplete accounting. Successful SDK tool usage retains the same numeric
728
+ totals for compatibility. A final failure remains an incomplete inspection,
729
+ preserves earlier failures, and asks the Owner to stop same-condition retries.
730
+
731
+ Increasing output allowance can increase cost and does not establish visual
732
+ accuracy. Verify page observations separately; a complete response alone is not
733
+ business acceptance. These variables require a build containing this feature;
734
+ the previously published alpha.2 does not support them.
package/sdk/README.md CHANGED
@@ -34,6 +34,12 @@ for await (const event of query('检查今天的收件箱并起草回复', {
34
34
 
35
35
  Resume an existing session: `query(prompt, { resume: sessionId })`.
36
36
 
37
+ For work that must continue after the foreground closes, the **1.5.1-alpha interface** exports `createBackgroundClient`.
38
+ See [durable background tasks](../docs/background-runs.md) for explicit submission,
39
+ stable keys, paged event recovery, permissions and the worker/reviewer example.
40
+ Its required `cwd` and `cliPath` options are separate from `query()`'s defaults;
41
+ in-process callbacks and SDK MCP instances cannot be carried into a detached worker.
42
+
37
43
  Pending approval (7×24 resident CoS, owner approves via Feishu card):
38
44
 
39
45
  ```ts