@dsh-enhanced/assistant-work-items 0.0.0-stage → 0.1.67
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.
- package/LICENSE +21 -0
- package/README.md +685 -2
- package/cordis.patch.yml +11 -0
- package/lib/acceptance-binding.d.ts +86 -0
- package/lib/acceptance-binding.d.ts.map +1 -0
- package/lib/acceptance-binding.js +250 -0
- package/lib/acceptance-binding.js.map +1 -0
- package/lib/admission.d.ts +95 -0
- package/lib/admission.d.ts.map +1 -0
- package/lib/admission.js +642 -0
- package/lib/admission.js.map +1 -0
- package/lib/authority.d.ts +99 -0
- package/lib/authority.d.ts.map +1 -0
- package/lib/authority.js +233 -0
- package/lib/authority.js.map +1 -0
- package/lib/config.d.ts +365 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +672 -0
- package/lib/config.js.map +1 -0
- package/lib/continuation-contract.d.ts +126 -0
- package/lib/continuation-contract.d.ts.map +1 -0
- package/lib/continuation-contract.js +109 -0
- package/lib/continuation-contract.js.map +1 -0
- package/lib/continuation-record.d.ts +32 -0
- package/lib/continuation-record.d.ts.map +1 -0
- package/lib/continuation-record.js +57 -0
- package/lib/continuation-record.js.map +1 -0
- package/lib/continuation.d.ts +89 -0
- package/lib/continuation.d.ts.map +1 -0
- package/lib/continuation.js +607 -0
- package/lib/continuation.js.map +1 -0
- package/lib/diagnosis-contract.d.ts +95 -0
- package/lib/diagnosis-contract.d.ts.map +1 -0
- package/lib/diagnosis-contract.js +167 -0
- package/lib/diagnosis-contract.js.map +1 -0
- package/lib/diagnosis.d.ts +70 -0
- package/lib/diagnosis.d.ts.map +1 -0
- package/lib/diagnosis.js +466 -0
- package/lib/diagnosis.js.map +1 -0
- package/lib/dispatch.d.ts +100 -0
- package/lib/dispatch.d.ts.map +1 -0
- package/lib/dispatch.js +317 -0
- package/lib/dispatch.js.map +1 -0
- package/lib/engineering-context.d.ts +86 -0
- package/lib/engineering-context.d.ts.map +1 -0
- package/lib/engineering-context.js +263 -0
- package/lib/engineering-context.js.map +1 -0
- package/lib/evaluation.d.ts +125 -0
- package/lib/evaluation.d.ts.map +1 -0
- package/lib/evaluation.js +295 -0
- package/lib/evaluation.js.map +1 -0
- package/lib/event-follow.d.ts +93 -0
- package/lib/event-follow.d.ts.map +1 -0
- package/lib/event-follow.js +150 -0
- package/lib/event-follow.js.map +1 -0
- package/lib/feedback-targets.d.ts +91 -0
- package/lib/feedback-targets.d.ts.map +1 -0
- package/lib/feedback-targets.js +362 -0
- package/lib/feedback-targets.js.map +1 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +25 -0
- package/lib/index.js.map +1 -0
- package/lib/intake-admission.d.ts +49 -0
- package/lib/intake-admission.d.ts.map +1 -0
- package/lib/intake-admission.js +100 -0
- package/lib/intake-admission.js.map +1 -0
- package/lib/intake-evaluation-runtime.d.ts +74 -0
- package/lib/intake-evaluation-runtime.d.ts.map +1 -0
- package/lib/intake-evaluation-runtime.js +343 -0
- package/lib/intake-evaluation-runtime.js.map +1 -0
- package/lib/intake-evaluation.d.ts +180 -0
- package/lib/intake-evaluation.d.ts.map +1 -0
- package/lib/intake-evaluation.js +440 -0
- package/lib/intake-evaluation.js.map +1 -0
- package/lib/intake.d.ts +118 -0
- package/lib/intake.d.ts.map +1 -0
- package/lib/intake.js +137 -0
- package/lib/intake.js.map +1 -0
- package/lib/ledger.d.ts +86 -0
- package/lib/ledger.d.ts.map +1 -0
- package/lib/ledger.js +145 -0
- package/lib/ledger.js.map +1 -0
- package/lib/mandate.d.ts +45 -0
- package/lib/mandate.d.ts.map +1 -0
- package/lib/mandate.js +168 -0
- package/lib/mandate.js.map +1 -0
- package/lib/outcome.d.ts +137 -0
- package/lib/outcome.d.ts.map +1 -0
- package/lib/outcome.js +303 -0
- package/lib/outcome.js.map +1 -0
- package/lib/owner-access.d.ts +35 -0
- package/lib/owner-access.d.ts.map +1 -0
- package/lib/owner-access.js +152 -0
- package/lib/owner-access.js.map +1 -0
- package/lib/owner-continuation.d.ts +27 -0
- package/lib/owner-continuation.d.ts.map +1 -0
- package/lib/owner-continuation.js +131 -0
- package/lib/owner-continuation.js.map +1 -0
- package/lib/owner-recall.d.ts +85 -0
- package/lib/owner-recall.d.ts.map +1 -0
- package/lib/owner-recall.js +134 -0
- package/lib/owner-recall.js.map +1 -0
- package/lib/owner-replies.d.ts +44 -0
- package/lib/owner-replies.d.ts.map +1 -0
- package/lib/owner-replies.js +268 -0
- package/lib/owner-replies.js.map +1 -0
- package/lib/peers.d.ts +624 -0
- package/lib/peers.d.ts.map +1 -0
- package/lib/peers.js +10 -0
- package/lib/peers.js.map +1 -0
- package/lib/policy-notice.d.ts +31 -0
- package/lib/policy-notice.d.ts.map +1 -0
- package/lib/policy-notice.js +137 -0
- package/lib/policy-notice.js.map +1 -0
- package/lib/reply-contract.d.ts +67 -0
- package/lib/reply-contract.d.ts.map +1 -0
- package/lib/reply-contract.js +58 -0
- package/lib/reply-contract.js.map +1 -0
- package/lib/service.d.ts +632 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +2447 -0
- package/lib/service.js.map +1 -0
- package/lib/settlement.d.ts +74 -0
- package/lib/settlement.d.ts.map +1 -0
- package/lib/settlement.js +233 -0
- package/lib/settlement.js.map +1 -0
- package/lib/signals.d.ts +176 -0
- package/lib/signals.d.ts.map +1 -0
- package/lib/signals.js +497 -0
- package/lib/signals.js.map +1 -0
- package/lib/store.d.ts +791 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +3266 -0
- package/lib/store.js.map +1 -0
- package/lib/tools.d.ts +7 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +443 -0
- package/lib/tools.js.map +1 -0
- package/lib/types.d.ts +831 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +29 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +2 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +2 -0
- package/lib/version.js.map +1 -0
- package/lib/workspace.d.ts +43 -0
- package/lib/workspace.d.ts.map +1 -0
- package/lib/workspace.js +137 -0
- package/lib/workspace.js.map +1 -0
- package/package.json +101 -3
package/README.md
CHANGED
|
@@ -1,3 +1,686 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dsh-enhanced/assistant-work-items
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把外部信号沉淀为**有界、可恢复、可审计**的工作项账本,并用一次原生 automation occurrence 驱动 agent 认领、汇报和 owner 通知。本插件不创建第二套 agent loop、timer、outbox、预算系统、设置面板或 session store:调度复用 `assistant-automations` 的稳定 occurrence,执行复用原生 DSH Agent run,通知复用 `assistant-delivery.enqueueBackground()`,授权复用 `assistant-policy` 的默认拒绝。
|
|
4
|
+
|
|
5
|
+
账本提供认领与汇报工具 `work_item_claim`、`work_item_report`,状态查询 `work_item_status`,以及只读报告召回 `work_item_read_report`。可选的 `diagnosis` 模式将当前 owner 的真实前台终止观察接入固定 Host worker:原请求仍保留 unknown 的外部效果,仅用新预算读取原输入、写诊断报告。它不重放原请求,也不把诊断报告当作原目标完成或修复成功。通用来源注册与通用 worker 模式继续保留。
|
|
6
|
+
|
|
7
|
+
终态报告只记录 worker 自己的结论;账本另有一条 append-only 的**结果结算**(outcome settlement)修订链,用 `assistant-delivery` 已认证的 execution 记录独立验证 `ready` 是否真的对应成功执行,并在证据窗内保持 `pending`、到期保持 `unverified`,绝不把 `ready` 当作目标达成。GitHub、邮件、IM、任务日历等**仓库外**来源属于独立插件的职责:它们实现一个只读 `WorkItemSignalSource`,用 `registerSignalSource(...)` 注册进来,就走与本内置来源完全相同的扫描、provenance、去重、冷却与已结项围栏,而不是另开写路径。
|
|
8
|
+
|
|
9
|
+
## 安装与启用
|
|
10
|
+
|
|
11
|
+
本插件要求宿主已提供 `ctx.assistantPolicy`(patch 中 `inject: [assistantPolicy]`),运行中的 profile 还需有 agent loop、LLM、session 和 tools 服务。要让 queued 工作项真正被执行,还需要 `@dsh-enhanced/assistant-automations`;要让终态报告送达 owner,还需要 `@dsh-enhanced/assistant-delivery` 和已配置的 owner route。
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
dsh plugin --profile web add @dsh-enhanced/assistant-policy
|
|
15
|
+
dsh plugin --profile web add @dsh-enhanced/assistant-automations
|
|
16
|
+
dsh plugin --profile web add @dsh-enhanced/assistant-delivery
|
|
17
|
+
dsh plugin --profile web add @dsh-enhanced/assistant-work-items
|
|
18
|
+
dsh --profile web --dump-config
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Patch 默认 `enabled: false`、`mode: observe`,安装后不创建目录、不打开数据库、不调用模型、不发消息。启用 active 模式必须同时显式给出 `workerAutomationId`、`scope`(完整 owner 五元组)和至少一个 `admission.allowedKinds`,否则配置校验失败关闭。
|
|
22
|
+
|
|
23
|
+
### 只读前台失败诊断(显式启用)
|
|
24
|
+
|
|
25
|
+
`diagnosis` 默认关闭,只能在 `enabled: true`、`mode: active`、完整 `scope` 和 `sources.foregroundFailures.enabled: true` 下启用。在下面通用配置中加入:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
diagnosis:
|
|
29
|
+
enabled: true
|
|
30
|
+
budgetId: owner-diagnosis-budget
|
|
31
|
+
budgetAmount: 1
|
|
32
|
+
maxModelCalls: 5
|
|
33
|
+
maxToolCalls: 4
|
|
34
|
+
maxOutputTokens: 1024
|
|
35
|
+
timeoutMs: 30000
|
|
36
|
+
expiresAt: 1893456000000 # 必须在未来,且不晚于 scope.expiresAt
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
启用时所有额度字段都必填;这是 owner 独立批准的诊断额度,绝不推断原任务还剩多少预算。每次运行从同一 Policy 预算主体扣除 `budgetAmount`,重启不重置累计消耗。工具、prompt 版本、独立预算、owner、来源摘要、原 binding、供应商/model/实际 effort(包括未指定)均在首次准入时冻结;完整 canonical plan 与摘要和原 admitted 事件同事务保存。旧记录、重复信号、observe 记录或扩展来源不能事后补签计划。
|
|
40
|
+
|
|
41
|
+
此模式自动注册一个 system-owned、仅 external 触发的 Host definition,**不要按下一节手工创建同 ID 通用 automation**。首次创建需要 Policy 允许 background `assistant-work-items-diagnosis` 对该 automation `reconcile`;准入主体仍为 `intakeSourceId`(默认 `work-items/v1`)的 `admit`,外部事件需要该主体的 `ingest`,Host 执行需要 background `workerAutomationId` 的 `execute`,资源均为固定 automation。原生 worker 工具规则应精确包含 `scope.preset`、workspace 和 owner principal,通知继续按下文向原 conversation binding 授权 `send`。以上规则均须显式批准,不由本插件自动放开。
|
|
42
|
+
|
|
43
|
+
首次激活保存 intent/receipt;后续挂载只检查相同 definition/hash/version、nonce、owner 和配置,发现 paused、deleted、missing、配置或授权漂移即停,不自动 unpause、重新安装、换 nonce 或延长有效期。需要支持 `hostAgentAuthorizationVersion() === 1` 的 Automations;旧 peer 不能忽略执行 fence。每次模型与工具动作前重新核对当前注入、owner/source 和阶段权限,卸载立即撤销私有 executor 并 abort。模型可在 claim 前启动、在本人报告后给出有界最终摘要;只有本人活动 claim 可读取原输入。
|
|
44
|
+
|
|
45
|
+
诊断工具平面固定为 `work_item_claim`、`work_item_status`、`work_item_read_report`、`work_item_read_source`、`work_item_report`。新增 `work_item_read_source({offset?, max_bytes?})` 只读该 claim 的原输入,返回 UTF-8 有界正文、摘要、总字节和续读 offset;没有 item/inbox/路径选择器,内容标记为不可信。没有 Bash、文件读写、网络或任意执行工具。模型供应商配置变化(包括默认 effort 改变)不能静默换模型继续运行。
|
|
46
|
+
|
|
47
|
+
无需先手工唤醒 worker:已有原生 scheduler wake 会先处理可证明的 waiting 恢复,再按 `sweepIntervalMs`、候选页限额做一次非强制内置扫描;不新增 timer,也不扫描扩展来源。只接收当前 owner/binding 的真实 `error` 或启用的 `configured-timeout` 终止观察,且必须 `handleDisposed: true`。没有可信 termination 或供应商快照时保持不准入。`owner-cancelled` 只减少工作;终止观察不可改写,因此不能代表对旧 error 的任意后续纠正,可信 Host 仍可显式调用 `withdraw(...)` 减权。
|
|
48
|
+
|
|
49
|
+
本片诊断**串行**:任何 open item、尚在最终摘要的 live run 或待处置 unknown 都占用唯一诊断槽;未消费候选保留 cursor,避免 queue-one 合并丢掉任务。其他通用 worker 可继续承担独立任务。报告 ready/waiting 之后仍可能发生模型错误、卸载或进程崩溃;报告本身不证明外层 Host 已完成。waiting 只在完整报告 key 对齐原 released claim、同一 production occurrence 的外层 succeeded、已验证 scope/hash/version 且预算 finalized 后恢复。缺字段、不同 occurrence、未知结算或更晚不匹配 run 均停驻。
|
|
50
|
+
|
|
51
|
+
已开始/未知的运行不按过期 lease 自动 reclaim、不清零预算、不伪造 blocked 报告。unknown(包括 ready 后未知)跨重启保留,并在 health 的 `interventionRequired` 可见;需要可信 Host 显式 `withdraw(...)` 释放槽。该动作仅记录减权,不改写旧报告、原任务效果或 outcome。激活失败、peer 缺失时,没有可核对外层成功的已有报告/已发送 dispatch 也保守显示待核对。已有诊断历史的数据库不能通过关闭 diagnosis 切换成 active 通用模式重放;observe 可读,disabled 保持零打开。
|
|
52
|
+
|
|
53
|
+
诊断模式拒绝公共 `admit(...)` 与 `registerSignalSource(...)`,不接受来源替换或影子同名来源。以下注册示例、通用 worker 手工准备与 lease 自动恢复说明仅适用于未启用 diagnosis 的通用模式。
|
|
54
|
+
|
|
55
|
+
### 主人会话中的任务召回(显式启用)
|
|
56
|
+
|
|
57
|
+
在完整的 active 配置中加入 `ownerRecall: { enabled: true }`,并让主人的前台 preset 包含 `work_item_list`、`work_item_recall`,即可在新会话中查找后台工作项及其已有结论。默认关闭;开启时仍须配置完整的 `scope` 和 `workerAutomationId`。这两个工具供当前已认证主人使用,后台 worker 继续通过自己的 occurrence 认领、读报告和汇报。
|
|
58
|
+
|
|
59
|
+
跨渠道只读别名:须由主人在 `ownerRecall.linkedAliases` 中逐个列出允许的 Delivery `linked` principal id(Delivery 规范形式 `channel/account/tenant/user`,各段按 `encodeURIComponent` 编码,例如 `@` 写作 `%40`;按字节精确、区分大小写比较,空白、非 ASCII 或其他非规范写法在配置时即拒绝;最多 8 个、去重、不得为 owner 本人;未配置即不接受任何别名,也不会向 Delivery 查询别名回执或以别名调用 Policy)。Delivery 侧的 link 本身不构成授权。若 Delivery 提供 `inspectOwnerAliasRoute`,已 link 到当前 owner 且 principal id 在该名单中的 `linked` principal(例如 Lark 主人关联的 Telegram 账号)在精确 owner 校验失败时,可以只用 `work_item_list`、`work_item_recall`(`inspect-work-items`)读取同一主人的工作项;列表、正文、报告和游标都按根 owner 的 principal record/version 与 binding 代次过滤,与主人本人看到的一致。别名回执(`assistant-delivery/owner-alias-receipt/v1`)不进入 v2 owner 槽位,`receiptVersion` 存在、根身份或 scope 不符、alias 与 root 相同、Session 不符、别名 principal 不在名单内(`linked alias is not allowlisted by the owner`,在 Policy 求值之前拒绝)、或调用证明不是别名协议都拒绝。每次观察(Policy 评估前后、authorize 之后、回调前后)都重新向 Delivery 取回执并比对摘要,解除关联或 owner 版本变化会使进行中的读取以 `owner/call changed` 失败;owner 版本变化后旧版本工作项对主人和别名同样不可见。`work_item_continue`(`continue-work-items`)、审批、worker 认领/汇报等变更仍只接受精确 owner 或 worker 身份。Policy 以别名 Agent 为主体求值,别名与主人共享 preset 和 workspace:对已列入名单的别名,若主人的 `inspect-work-items` 规则未写 `principal` 匹配,别名也会命中该规则;只有主人规则限定了 `principal` 时,才须为别名 principal 单独授权。未列入名单的 linked 账号不再因此类规则获得读取。别名读取按 principal 单独计费:命中的 `inspect-work-items` 规则必须带 `budget`(否则 `linked alias read requires a budgeted Policy rule`,在扣费前拒绝),扣费改走 Policy `authorizeAgentForPrincipal`,落在该别名 principal 自己的预算分区(同一 budgetId 的 limit/period,独立计数),返回的 `budgetPrincipal` 须等于别名 principal;主人自己的额度不受别名读取影响,每个别名各自有界(名单至多 8 个)。不设跨分区总上限,同一 budget 的总消耗至多为 `(1 + 名单长度) × limit`(≤ 9 份);需要更紧总量时为别名单独写带 `principal` 的规则与更小的 budget。Policy 缺少该方法时别名读取一律拒绝(`linked alias reads require per-principal Policy budgets`),精确 owner 路径不变。撤销 owner 会使其所有 link 失效(Delivery 解除 `linked_to_id`);同一 owner record 重新激活后须重新 link,别名才能再次读取。原生 Web owner 以别名身份加入不在此范围。
|
|
60
|
+
|
|
61
|
+
跨信道报告路由:默认关闭。主人在 `ownerReportRouting.linkedFallback` 中按序列出 Delivery `linked` principal id(与 `linkedAliases` 同样的规范形式与限制:≤8、去重、不含 owner;要求 enabled active、完整 scope 与 worker),且 Delivery 提供 `routeWorkItemReport` 时,终端报告仍先发 origin;只有 origin 不可用时,Delivery 才把报告作为只读通知投到第一个仍合格的别名信道(首个通过 Policy `evaluate` 的别名即终点,见 Delivery README),报告的投递记录写入该 outbox。「origin 不可用」指 origin binding 非 active 且同一会话下没有同一主人、同一 workspace/preset 的后继 active binding;`/new` 轮换后 origin 仍算可用,不改道,沿用原有 origin 路径与失败语义。fallback 信道上的回复不会续办或反馈(续办仍需 v2 owner);别名要只读查看该工作项,还须列在 `ownerRecall.linkedAliases` 中。别名信道上对该通知的回复会被 Delivery WP22 结果事实计为 reaction,该事实带 `route: { kind: 'linked', originBindingId }` 标注,Evaluation 的注意力调参不计入改道记录。入队后若别名解除关联、binding 轮换、owner 撤销/重新 link 或 Policy 变化,Delivery 以 `binding-revoked` 或 `linked-report-route-revoked` 不发送。没有合格别名时与原来一样记为 failed,不回落到 origin 再试。未配置时不调用该接口,行为不变。可选 `ownerReportRouting.prefer`:默认 `'origin'`(行为与上述完全一致);`'recent'` 须同时配置非空 `linkedFallback`(否则配置被拒),此时 Delivery 改按主人最近一次入站消息(`inbox_messages.received_at`)选名单中的合格别名,origin 可用时仅当该别名入站严格晚于 origin 才改道,否则仍走 origin(该别名在入队提交前被 Policy/注意力拒绝时也回落 origin);其他取值被拒。改道后的报告同样只走普通 background lane,不经 typed 报告 lane,因此失去 `ownerReplyContinuation`/`ownerReportFeedback`:别名信道上的回复不能续办或反馈。
|
|
62
|
+
|
|
63
|
+
该入口沿用已能正常对话的 Delivery owner route;原生对话的身份与回复授权仍需有效。前台读取另外需要独立的 Policy 授权,例如:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
rules:
|
|
67
|
+
- id: owner-work-item-recall
|
|
68
|
+
effect: allow
|
|
69
|
+
subject:
|
|
70
|
+
kind: agent
|
|
71
|
+
id: primary
|
|
72
|
+
workspace: /absolute/workspace
|
|
73
|
+
principal: <owner principal id>
|
|
74
|
+
actions: [inspect-work-items]
|
|
75
|
+
resource: { kind: automation, id: work-item-worker }
|
|
76
|
+
context: { initiators: [external] }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
这里的 `external` 是 Delivery 消息入口的 Policy initiator;调用沿用真实入口绑定,不自行改写成 `foreground`。
|
|
80
|
+
|
|
81
|
+
`work_item_list({limit?, after?})` 默认返回 10 项、最多 20 项;用返回的续页引用继续查找。每项包含稳定任务 ID、状态与有界报告摘要。`work_item_recall({item_id, report_revision?, offset?, max_bytes?})` 沿该 ID 读取任务:不指定 `report_revision` 时分页读取 brief,指定时读取该修订的报告。正文默认 4096 字节,范围 4–8192;`offset` 是 UTF-8 字节位置,续读使用返回的 `nextOffset`。最近报告的修订号支持从摘要继续找回正文;报告页返回相邻修订号,可继续读取最近五条之外的历史。它不读取 artifact 文件。
|
|
82
|
+
|
|
83
|
+
每次读取由 Delivery 核对真实活跃 Agent、当前主人的人类 turn 以及这次原生工具调用,随后核对当前 owner lineage、workspace/preset、owner route 与 worker automation。SQL 在分页前按这些身份过滤;用户文本、任务 ID 或分页位置不能充当身份。相同主人通过 `/new` 进入较新的 Session 代次后,可以读原任务;owner 版本变化、代次倒退、同代 binding 不一致或当前读取授权失效时拒绝。
|
|
84
|
+
|
|
85
|
+
这是一项历史读取权限:同一主人可查看已结束、过期或撤回任务,但读取不会恢复任务、重派 occurrence、追加报告、修改预算期限或重置 worker 额度。Policy 可以为读取另设预算;预检查与结果复核不扣费,实际读取按真实工具调用的幂等键授权,并保留 Policy 审计。原生工具的显式拒绝、紧急停止、调用预算与撤权仍生效。旧 Policy/Delivery 缺少身份或只读复核接口时,这项能力失败关闭。
|
|
86
|
+
|
|
87
|
+
任务正文和报告均作为不可信材料展示;`ready` 仍是 worker 自报,独立 outcome 保留原状态。该入口提供跨会话任务召回,不自动关联不同 owner route,也不改变 `follows` 的关闭结算或 diagnosis 冻结的五工具合同。
|
|
88
|
+
|
|
89
|
+
### 一次性准备 worker automation
|
|
90
|
+
|
|
91
|
+
在同一个 profile 里,先由可信 owner 前台 Agent 创建一条普通 Agent automation(`automation_create` 只提交 ID、名称、prompt、schedule 和 `allowed_tools`,workspace/preset/owner 从当前 Agent 派生),例如:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
# 在 owner 前台会话里调用 automation_create
|
|
95
|
+
id: work-item-worker
|
|
96
|
+
name: Work item worker
|
|
97
|
+
schedule: { kind: cron, expression: '*/15 * * * *' }
|
|
98
|
+
prompt: |-
|
|
99
|
+
调用 work_item_claim。若没有工作项,直接结束,不要输出内容。
|
|
100
|
+
否则按工作项的 brief 和 acceptanceRef 完成工作,用 work_item_report
|
|
101
|
+
汇报 progress/ready/blocked/failed,并在结束前给出终态报告。
|
|
102
|
+
resume 摘要不足时,用 work_item_read_report 按 revision 读取报告,沿
|
|
103
|
+
nextOffset 继续正文,沿 previousRevision 找回更早报告。
|
|
104
|
+
allowed_tools: [work_item_claim, work_item_report, work_item_status, work_item_read_report]
|
|
105
|
+
budgetId: <owner 批准的 automation-runs 预算>
|
|
106
|
+
budgetAmount: 1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
要求:
|
|
110
|
+
|
|
111
|
+
- automation id 必须与配置里的 `workerAutomationId` 完全一致;
|
|
112
|
+
- automation 的 workspace 和 preset 必须与 `scope.workspace` / `scope.preset` 完全一致;
|
|
113
|
+
- worker preset 的最终工具平面必须包含 `work_item_*`(若 preset 自带 scoped tool 列表,需要把这些工具纳入);
|
|
114
|
+
- 每次 occurrence 都会创建新的原生 Agent;工作项身份来自 `assistant-automations` 注入的**冻结 production 执行上下文**,前台会话、preview 运行和其他 automation 一律失败关闭。
|
|
115
|
+
|
|
116
|
+
然后开启本插件:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
- id: dsh-enhanced-assistant-work-items
|
|
120
|
+
name: '@dsh-enhanced/assistant-work-items'
|
|
121
|
+
config:
|
|
122
|
+
enabled: true
|
|
123
|
+
mode: active
|
|
124
|
+
databasePath: !!js dshHomePath('assistant-work-items/state.sqlite')
|
|
125
|
+
workRoot: !!js dshHomePath('assistant-work-items/work')
|
|
126
|
+
intakeSourceId: work-items/v1
|
|
127
|
+
workerAutomationId: work-item-worker
|
|
128
|
+
scope:
|
|
129
|
+
workspace: /absolute/workspace
|
|
130
|
+
preset: primary
|
|
131
|
+
principalId: <owner principal id>
|
|
132
|
+
ownerRouteId: <assistant-delivery owner route id>
|
|
133
|
+
expiresAt: 1893456000000 # epoch ms,必须晚于当前时间
|
|
134
|
+
admission:
|
|
135
|
+
allowedKinds: [review, triage, foreground-repair]
|
|
136
|
+
sources:
|
|
137
|
+
foregroundFailures:
|
|
138
|
+
enabled: true
|
|
139
|
+
kind: foreground-repair
|
|
140
|
+
includeTimedOut: true
|
|
141
|
+
maxCandidatesPerSweep: 10
|
|
142
|
+
maxAdmitsPerSweep: 2
|
|
143
|
+
sweepIntervalMs: 3600000
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`sources.foregroundFailures.enabled: true` 时,`claim()` 会先做一次被间隔门控的来源扫描;因此 worker 每次被唤醒时都会顺带消费新出现的 owner 失败请求。也可以由可信 Host 显式调用 `sweepSignals(force)` 立即扫描一次。
|
|
147
|
+
|
|
148
|
+
### 注册自己的来源(外部信号插件)
|
|
149
|
+
|
|
150
|
+
Host 或同 profile 的其他插件可以把自己的只读来源接进同一套自动准入:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const dispose = ctx.assistantWorkItems.registerSignalSource({
|
|
154
|
+
// 只读契约(由来源实现遵守):sweep 只读外部系统,不写 delivery/automations
|
|
155
|
+
source: {
|
|
156
|
+
id: 'github-review-requests', // 稳定 lower-kebab-case,同时是 provenance.source
|
|
157
|
+
sourceVersion: 'github-review-requests/v1',
|
|
158
|
+
sweep({ now, limit, cursor }) {
|
|
159
|
+
// 用 cursor 做有界分页;degraded 表示本次跑不了(例如缺少凭据),不是异常
|
|
160
|
+
return { candidates, cursor: nextCursor }
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
// 可选:不填时沿用 sources.foregroundFailures 的间隔/预算默认值
|
|
164
|
+
intervalMs: 900_000,
|
|
165
|
+
maxCandidatesPerSweep: 20,
|
|
166
|
+
maxAdmitsPerSweep: 3,
|
|
167
|
+
})
|
|
168
|
+
// 插件卸载 / 配置切换时撤销注册(幂等)
|
|
169
|
+
ctx.effect(() => () => dispose())
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
约束:
|
|
173
|
+
|
|
174
|
+
- 每个候选必须自带 `provenance`(source、sourceVersion、signalDigest、observedAt、可选 evidence)、`externalId`、`originBindingId` 和 `disposition`;admission 会逐字段严格校验,缺失或超长直接拒绝而不是猜。
|
|
175
|
+
- `disposition: 'withdrawn'` 永远不会产生新工作项。若同一 canonical execution 已有在途工作项(`queued/claimed/working/waiting`),撤回会把该 dedup key 下**所有**在途行退役为 `dropped`(`stateReason: 'signal-withdrawn'`),包括去重窗口滚动前遗留的旧行;已结项的同一信号仍由跨窗口围栏挡住,来源不能复活它。每次扫描报告新增 `retired`,只统计本次真正被退役的工作项(可能一次多条);`withdrawn` 仍只统计观察到的撤回候选。
|
|
176
|
+
- 撤回的边界:退役会释放活动 claim、把仍处于 `pending` 的 dispatch 行标为 `failed`(`reason: 'signal-withdrawn'`,不再重新武装唤醒),但**不能召回**已经交给 `assistant-delivery` 的报告/通知;随后 `claim`/`report` 会以 `claim-conflict` 失败(claim 已释放;若 owner route 恰好变化也可能先报 `owner-route-*`),status 工具会显示 `dropped`。撤回只读取规范化的 `externalId` 与来源归属,不重跑 owner route、Policy、cooldown、kind/rate/capacity 闸门,因为它只会减少待办工作;provenance 的来源与本次扫描来源不一致的候选会被直接拒绝。来源归属只保证「来源为自己发信号」:dedup identity 仍是 `intakeSourceId + kind + externalId`,扩展来源必须给 `externalId` 加自己的前缀,否则两个来源可能碰撞到同一工作项。
|
|
177
|
+
- 注册在插件 disabled 或已 dispose 时失败关闭(`disabled` / `storage`),不会先排队再补扫;同一 id 重复注册或格式非法抛 `invalid-input`,且不影响已注册来源。
|
|
178
|
+
- 来源仍然只依赖冻结的 owner route(`assistant-delivery` 的 `validateOwnerRoute`);注册不绕过 Policy `admit` 授权。
|
|
179
|
+
- 没有新定时器:来源只在 `claim()` 或显式 `sweepSignals(force)` 时被扫一次,受各自的持久化 `intervalMs` 门控。
|
|
180
|
+
- 内置适配器的 id 是 `owner-foreground-failures`;用同一 id 注册表示**替换**内置适配器(不会双重扫描),该来源沿用内置适配器已有的 `signal.cursor:owner-foreground-failures` / `signal.sweptAt:owner-foreground-failures` 键,因此首次扫描可能被判为 `not-due`。
|
|
181
|
+
|
|
182
|
+
### Policy 最小规则
|
|
183
|
+
|
|
184
|
+
默认无规则即全部拒绝。需要三条彼此独立的授权:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
rules:
|
|
188
|
+
# ① 允许本插件把外部信号提升为 active 工作项并唤醒 worker occurrence
|
|
189
|
+
- id: allow-work-item-admission
|
|
190
|
+
effect: allow
|
|
191
|
+
subject: { kind: background, id: work-items/v1, workspace: /absolute/workspace, principal: <principal id> }
|
|
192
|
+
actions: [admit]
|
|
193
|
+
resource: { kind: automation, id: work-item-worker }
|
|
194
|
+
|
|
195
|
+
# ② 允许 worker Agent 调用本插件的 CAS 工具(tool guard 默认拒绝)。
|
|
196
|
+
# subject.id 是 worker 会话的 Agent preset id(即 definition.agentPreset /
|
|
197
|
+
# scope.preset),不是 automation id,也不是 "primary":assistant-policy 在
|
|
198
|
+
# authorizeToolExecution 里用 session.header.agentPreset 作 subject.id。
|
|
199
|
+
- id: allow-work-item-tools
|
|
200
|
+
effect: allow
|
|
201
|
+
subject: { kind: agent, id: <worker preset id>, workspace: /absolute/workspace }
|
|
202
|
+
actions: [execute]
|
|
203
|
+
resource: { kind: tool, id: "work_item_*" }
|
|
204
|
+
context: { initiators: [background] }
|
|
205
|
+
|
|
206
|
+
# ③ 允许本插件经 Delivery 向固定 owner route 发通知。
|
|
207
|
+
# resource.id 是 assistant-delivery 的 conversation binding id(通知真正
|
|
208
|
+
# 投递所绑定的会话),不是 ownerRouteId:Delivery 的
|
|
209
|
+
# enqueueBackgroundAuthorized 以 binding.id 作 resource.id 再次授权。
|
|
210
|
+
- id: allow-work-item-owner-notify
|
|
211
|
+
effect: allow
|
|
212
|
+
subject: { kind: background, id: work-items/v1, workspace: /absolute/workspace, principal: <principal id> }
|
|
213
|
+
actions: [send]
|
|
214
|
+
resource: { kind: message, id: <conversation binding id> }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
worker preset 需要与 definition 的 `agentPreset` 一致;`subject.id` 写成 automation id、`primary` 或 route id 都会被判 `default-deny`(默认拒绝,不会报“配置错误”),这是最容易踩空的一处。同理 ③ 写成 `route:<ownerRouteId>` 会因为 binding 查不到而抛 `missing-binding`。
|
|
218
|
+
|
|
219
|
+
注意:当前 `assistant-policy` 的资源类型词汇表没有 work-item/ticket kind,因此准入授权复用 `resource: { kind: automation, id: <workerAutomationId> }`。这是契约层面的错配,不是可配选项;后续若 Policy 增加专用 kind,应同步调整实现与规则。
|
|
220
|
+
|
|
221
|
+
## 工具
|
|
222
|
+
|
|
223
|
+
通用模式的四条工具都要求调用者是配置中的 worker automation 的 exact production occurrence;工具调用落到前台会话或非 production 运行会被拒绝。调用方 Agent 的 workspace 与 preset 还必须与 `scope` 一致;claim 与 read_report 会复核 `scope.expiresAt`,过期即失败关闭。
|
|
224
|
+
|
|
225
|
+
- `work_item_claim`:按 occurrence 找到唯一绑定的工作项,用 CAS 取得 claim,返回 `itemId`、kind、title、brief、`workDir`、attempt 等。claim 没有独立续租动作:`progress` 报告会顺延租约;同一 run 的重复认领只在租约仍有效时幂等重放,租约过期则生成新的 claim 行与新的 attempt(受 `maxAttempts` 约束)。
|
|
226
|
+
- `work_item_report`:在活动 claim 下追加一条 append-only 报告修订(`progress` / `waiting` / `ready` / `blocked` / `failed`),可带 artifact 绝对路径;idempotency key 由 `status+summary+artifacts` 的 SHA-256 再按 item 与 claim 行限定,同一 claim 的重试返回已存修订而不重复写入,不同 claim 的同样文本则视为新修订。对已过期 claim 的报告会以 `claim-expired` 失败关闭。`waiting` 报告释放 claim 并按 `limits.waitingResumeMs` 安排一次**有界**恢复(受 `maxAttempts` 约束,见“生命周期与可靠性”)。终态报告会触发一次 owner 通知,通知失败只记录在报告行上,不删除报告证据。
|
|
227
|
+
- `work_item_status`:查询当前 occurrence 绑定的工作项、活动 claim 和报告列表;沿用既有租约恢复,不回显数据库路径、principal 或 Host 内部路径。
|
|
228
|
+
- `work_item_read_report`:只读召回当前 occurrence 所属工作项的指定报告正文与 artifact 引用,支持 UTF-8 字节分页及相邻 revision 导航;不接受 item ID 或任意文件路径。
|
|
229
|
+
|
|
230
|
+
报告重放在 SQLite 内精确比较完整幂等键、item、状态和实际持久化内容:摘要沿用去除首尾空白及 UTF-8 字节上限,保留截断标记,artifact 沿用路径规范化与去重。同键但持久化内容不同会拒绝;相同规范化报告的重放不增加修订、额度或通知。预授权与实际写入使用同一比较规则,既有键和账本 schema 保持不变。
|
|
231
|
+
|
|
232
|
+
`work_item_report` 的 `summary` 有字节上限(默认 16 KiB),超长会被截断并标记 `truncated`;artifact 路径必须是绝对路径,且位于该工作项的 scratch 目录或 `scope.workspace` 之内,相对路径与 `..` 逃逸一律拒绝。校验会逐级 realpath 每个已存在的路径组件,任何逃出根目录的符号链接都会失败关闭;scratch 目录仍应由部署者保持私有。
|
|
233
|
+
|
|
234
|
+
### 断点续办简报(resume)
|
|
235
|
+
|
|
236
|
+
`work_item_claim` 的返回值里带一个**只读、有界**的 `resume` 对象,让新 occurrence 在没有上一次模型记忆的情况下继续:`resumed` / `priorAttempts`、最近 5 次**先前** attempt(attempt 号、可得的 run id、开始时间、结束方式 `expired` / `reclaimed` / `attempt-exhausted` / `reported` / `settled`、以及终态报告的状态)、最近 5 条先前报告(revision、status、summary、`summaryTruncated`、记录时间;按时间从旧到新排列)与真实的 `reportsTotal`、最近一条已结算结果(revision、`settledBy`、execution/objective/evidence 状态、结算时间),以及 `truncated` 表示上游历史是否被上述上限截断。
|
|
237
|
+
|
|
238
|
+
- 简报只从本插件的 append-only 本地账本派生:`work_item_event` 与 `work_item_report` 按 `(item_id, …)` 主键前缀做有界扫描(`LIMIT`),不引入新表或 timer,也不调用模型或 peer。该功能引入时 schema 为 8,新增内容仅服务于下述有界续办索引;当前 schema 为 10。
|
|
239
|
+
- 报告 summary 在简报里最多保留 512 UTF-8 字节,超长按 UTF-8 边界截断并置 `summaryTruncated`;当前 attempt 自身不计入 `attempts`/`priorAttempts`。
|
|
240
|
+
- 简报是派生展示,不改变 claim/report/结算语义;它不含数据库路径、delivery id 或 evidence blob。首次认领与旧账本行可能得到空简报(`resumed: false`),重放同一 claim 会重新计算并返回一致的简报,不会追加新的 claim/attempt/事件。
|
|
241
|
+
|
|
242
|
+
### 摘要后的报告召回
|
|
243
|
+
|
|
244
|
+
`work_item_claim` 现在也会把非空 `acceptanceRef` 和非空 `leaseUntil` 投影给模型;缺失值省略。`resume.reports[].revision` 是稳定的报告引用,摘要超过 512 字节或较早报告被最近 5 条上限排除时,可调用:
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
work_item_read_report:
|
|
248
|
+
revision: 3
|
|
249
|
+
offset: 0
|
|
250
|
+
max_bytes: 4096
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- `revision` 为必填正安全整数;`offset` 是已存正文的 UTF-8 **字节**偏移,默认 `0`;`max_bytes` 默认 `4096`,取值 `4–8192`。返回正文总字节数 `summaryBytes`;还有后文时返回 `nextOffset`,原样传回即可续读;末页省略该字段。偏移超出正文或落在多字节字符中间会拒绝,不会跳字或产生半个字符。
|
|
254
|
+
- 返回实际存在的 `previousRevision` / `nextRevision`,首尾省略对应字段;因此可以从简报中的任一报告逐条找到早于最近 5 条的报告。`storedTruncated` 表示**原报告入库前**已被 `limits.maxReportBytes` 截断;召回只能恢复已存正文,不能恢复被丢弃的原输入。
|
|
255
|
+
- 读取只访问当前 occurrence 绑定的 item/report,重查 owner route 的 principal、版本及 binding 代次,并重跑原准入授权;缺依赖、撤权、owner/代次变化或到期均失败关闭。拒绝读取不 park 工作项,不触发恢复、结算或新增账本行,也不改变 schema。
|
|
256
|
+
- `artifactPaths` 只是原报告登记的引用,不读取文件。正文及引用都按不可信数据渲染。`work_item_report` 显示通知已入队仅证明 Delivery 接受了 outbox 请求,不证明消息已发送、送达或已读;历史 `delivery: sent` 仍保留原枚举语义。
|
|
257
|
+
- 新 automation 的不可变工具白名单和 preset 工具平面都须包含 `work_item_read_report`;已存在的 automation 不会因插件升级自动取得该工具。
|
|
258
|
+
|
|
259
|
+
### 已准入工作项的工具预授权
|
|
260
|
+
|
|
261
|
+
支持 `workItemToolPreauthorizationVersion() === 1` 的 Policy 可让**已准入的正确 worker 调用**继续执行,无需为四条工作项工具逐次请求交互审查。注册只认当前真实工具 definition 和 `AssistantWorkItemsService` owning Fiber;同名工具、别的插件或别的 occurrence 不能借用。
|
|
262
|
+
|
|
263
|
+
- 有界预检从冻结的 production occurrence 找到持久工作项,核对运行中的 workspace/preset、准入 scope、owner record/version、binding 版本/代次及有效期,并用原幂等键重查准入 Policy;只有仍为 `allow` 的现代账本项可自动通过。工具参数必须为普通 JSON 数据,隐藏字段、accessor、symbol、稀疏 artifact 数组及任意 item/path selector 均不通过。
|
|
264
|
+
- claim 还核对剩余尝试和租约状态;可以只读证明租约已到期、让原 claim 执行路径回收,预检自身不回收。report 只接受有效认领内的新报告或同 claim 的精确幂等重放;status/read_report 可查询当前 occurrence 的 waiting/终态项,不要求活动租约。
|
|
265
|
+
- 预检不调用 claim/status/report,不改工作项账本、不派发、不结算、不通知、不调用模型;准入 Policy 自己的审计仍按原规则保存。执行前会再次检查真实 owner、occurrence、原授权和本次参数,审批期间发生撤权或代次变化也不能越过这些边界。Policy 的独立 execute deny、急停和预算门继续生效。
|
|
266
|
+
- legacy 行、未证明的调用及旧 Policy 保留原交互审批路径;这不授予额外工作项权限,实际执行仍须通过上述守卫。旧 Policy 缺版本能力时会记录可见 warning。工具按其 owning Fiber 注册,预授权另由嵌套 Policy injection 持有;Policy 替换/卸载会撤销旧注册,不会把旧能力留给新实例。
|
|
267
|
+
|
|
268
|
+
## 生命周期与可靠性
|
|
269
|
+
|
|
270
|
+
以下自动来源、租约回收、授权恢复和未知 dispatch 重试描述通用模式;diagnosis 使用前节的串行准入、禁止未知重放和精确外层成功证明,不继承通用自动重试。
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
外部信号 --admit--> 私有 SQLite 工作项账本
|
|
274
|
+
├─ observe: 只写 observed/dropped 行,绝不唤醒、绝不通知
|
|
275
|
+
└─ active: queued + dispatch 行
|
|
276
|
+
└─ automations.ingestExternal({ sourceId: intakeSourceId, automationId: workerAutomationId,
|
|
277
|
+
eventId: 工作项 ID 或 工作项 ID#代次, occurredAt: dispatch 行 })
|
|
278
|
+
└─ 原生 Agent occurrence
|
|
279
|
+
├─ work_item_claim (CAS,租约默认 15 分钟)
|
|
280
|
+
├─ work_item_report (append-only,终态通知)
|
|
281
|
+
└─ delivery.enqueueBackground({ bindingId: 工作项创建时的 originBindingId })
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
- 状态机:`queued / claimed / working / waiting / ready / blocked / failed / dropped`。`ready`、`blocked`、`failed`、`dropped` 为终态;`queued`、`waiting` 可被认领。`ready` 表示 worker 汇报已完成,不代表目标已达成;独立结果证据见下节。
|
|
285
|
+
- 准入闸门顺序:enabled → 输入规范化 → owner route 校验(失败时零写入、零唤醒)→ 去重窗口重放 → **跨窗口已结项围栏**(`ready/blocked/failed/dropped` 的旧项不会被同一 canonical signal 复活)→ kind allowlist → cooldown → 每小时准入上限 → 未结工作项上限 → observe 记录或 active 入账 + dispatch 行。active 路径还会在写库前调用一次 `assistant-policy` 的 `admit` 授权。
|
|
286
|
+
- 自动准入(`sources`):在 `claim()` 之前同步扫一遍已武装的来源,并按持久化的 `sweepIntervalMs` 做**运行间隔门控**(没有新 timer)。每个来源的游标 `signal.cursor:<source>` 和上次扫描时间 `signal.sweptAt:<source>` 都存在账本同一 SQLite 的 `work_item_schema_meta` 表中;adapter 抛错会把游标重置为空(下次从最旧信号重扫,去重保证重叠无害),并只记一条 warn,绝不让 claim 失败。每次扫描的准入数量由 `maxAdmitsPerSweep` 有界。
|
|
287
|
+
- 信号以**规范化 canonical key**(对持久身份做 SHA-256,不是原文)去重;工作项上通过 `admitted` 事件 detail 的 `provenance`(source、sourceVersion、signalDigest、observedAt、evidence)记录来源、来源版本与观察时间。provenance 复用事件 detail,因此 schema 版本不变。
|
|
288
|
+
- 已结项修复不会被同一信号再次唤醒:owner 的失败前台请求在 owner 重试前一直是 `failed`,语义上必须由跨窗口围栏挡住,否则每个去重窗口都会重开同一修复。
|
|
289
|
+
- 去重键 `sha256(sourceId\0kind\0externalId)` 加时间桶 UNIQUE;同一外部信号在 `dedupWindowMs` 内只入账一次,重放返回既有工作项。
|
|
290
|
+
- 唤醒身份按**代次**限定:`work_item_dispatch` 保存 `generation`、`event_id`、`occurred_at`,代次 0 就是准入事件(`event_id` = 工作项 ID、`occurred_at` = 准入时间);每次重新武装都把 `generation + 1`、`event_id` 改写为确定性的 `工作项 ID#代次` 并写入新的 `occurred_at`。`wake()` 只使用 dispatch 行持久化的身份,绝不从工作项 ID 重新构造,因此重新武装一定对应 automation 侧的新 occurrence,而同一代次的重复唤醒仍解析到同一个 occurrence。
|
|
291
|
+
- 租约到期不靠 timer 回收:admit/claim/status/list/health 以及启动恢复时惰性调用 `reclaimExpired()`;到期 claim 被显式置为 `expired` 并记事件,工作项回到 `queued`,dispatch 行在同一事务里进入下一个代次并回到 `pending`,由后续普通 worker 流量中的有界 `wakePending()` 扫到(`occurred_at <= now` 才唤醒,绝不提前);超过 `maxAttempts` 的项标记 `blocked`。
|
|
292
|
+
- `waiting` 报告会释放 claim 并**安排一次延迟恢复**:dispatch 行进入下一个代次、`reason = waiting-resume`、到期时间为报告时间 + `limits.waitingResumeMs`;只有 `attempt < maxAttempts`、未进入终态且未被观察时才安排,每次 waiting 报告最多一代。active intake 会通过 owning Fiber 的嵌套 injection 订阅原生 Automations `onSchedulerWake`,在已有 scheduler tick 到期时入队新 occurrence,无需用户再调用 claim;执行仍由原生 scheduler 负责,成功认领后才增加 attempt。
|
|
293
|
+
- 原生唤醒每次先从部分 due 索引按 `(occurred_at, item_id)` 取最多 **10 个候选**,再逐项检查仍为 pending waiting-resume、item waiting 且最后报告 waiting、未观察、无活动 lease/claim、有剩余尝试、现代准入 mandate 和有效的精确 owner/scope;沿用 `dispatch.wake` 的原准入授权、稳定事件身份与 Automations ingest Policy。通用模式的回调不调用全量租约回收、来源扫描、结算、tick/start、模型或预算 reserve;diagnosis 模式会在 waiting 恢复之后追加前节所述的有界内置来源扫描,执行和预算仍由原生 Host 路径负责。schema 8 只新增 dispatch due 与 active-claim 两个部分索引;历史行不改写,公平游标存于既有 meta 表,经过拒绝或失效项也会推进并轮转,避免后续项饥饿。
|
|
294
|
+
- Automations peer 按调用时解析,晚加载或替换后由新 owning injection 重新订阅;旧代次 callback 被撤销,close 先停监听再关账本,同步重入会被隔离。disabled/observe 不订阅,也不因 tick 打开账本。旧 peer 缺 `onSchedulerWake` 时会 warn 并保留启动/claim 惰性恢复路径。要在空闲 Host 自动续办,仍须启用原生 Automations `schedulerEnabled`;`health().schedulerWake.subscribed` 只证明监听已安装,不证明调度器、duty 或任务执行已获授权。
|
|
295
|
+
- 崩溃恢复:`ingestExternal` 已成功但 dispatch 行仍未记账时,该行保持 `pending` 且身份不变,下一次有界恢复用**同一代次身份**重新唤醒,automation store 以相同 `(automationId, eventId, occurredAt)` 幂等返回既有 occurrence,绝不重复运行、绝不重复报告。
|
|
296
|
+
- 唤醒记账诚实:peer 抛出的错误若表示「不知道是否已经提交」(非 `policy-denied`/`policy`、非去重冲突),dispatch 行记为 `state='failed'`、`reason='unknown_after_send'`,`health().dispatchUnknown` 因此只统计仍然**开放且未被观察**的 item 上真实存在的这种行:item 一旦被撤回、终结或观察,就不再计入,也不会再被恢复扫描。这样的行在同一代次身份下最多被惰性恢复重试 **1 次**(`wake_attempts` 持久计数);重试成功则转为 `dispatched`,再次失败则保持 `failed`/`unknown_after_send`,把无法消除的缺口留给 owner,而不是无限重试或假装成功。持久身份冲突(`idempotency-conflict`)是确定性错误,记为独立原因 `idempotency-conflict` 并抛 typed `WorkItemError`(`dispatch-failed`);没有 occurrence 查询接口时无法自动收敛,因此不静默吞掉。
|
|
297
|
+
- SQLite 使用 `WAL`、`synchronous=FULL`、`BEGIN IMMEDIATE` 事务和 forward-only schema;`PRAGMA user_version` 只前进。版本与对象清单在同一读快照里读取;每个迁移步骤在自己的 `BEGIN IMMEDIATE` 内重读 `user_version`,若并发打开者已推进版本,则回滚本步、按其留下的文件重新校验,不重复执行 DDL(仅限已有版本 ≥1 的库;两个进程同时首次创建新库仍可能抛原始 `SQLITE_BUSY` 或 `EEXIST`)。升级仍建议单 writer:升级前停掉 DSH 进程。
|
|
298
|
+
|
|
299
|
+
### inferred 候选的模型评估合同(WP20)
|
|
300
|
+
|
|
301
|
+
`InferredIntakeEvaluationQueue`(`src/intake-evaluation.ts`)是 WP20 inferred 候选模型评估的纯内存合同:唯一外部调用是注入的 `evaluator`,不读写账本、不投递、不准入工作项,决策只作为返回值交给调用方持久化与执行。运行时接线(第 3 阶段)见「入站接线」下的「inferred 评估运行时」。
|
|
302
|
+
|
|
303
|
+
- 与 intake 账本强度分级的组合:只有 `strength: 'inferred'` 的候选入队(否则 `invalid-candidate`);`explicit` 由确定性 gate 直接处理、不调模型,`ignore` 已被过滤。
|
|
304
|
+
- 候选携带 Host 匹配规则与计算 `ceiling` 时的 `policyVersion`,不等于当前版本时入队返回 `stale-policy`。
|
|
305
|
+
- 逐 `(candidateId, ruleId)` 在每个 Policy 版本下只评估一次;同键再入队返回 `duplicate`,原文、摘要或上限不同返回 `conflict`。`ceiling` 为 `ignore` 时直接落定 `ceiling-ignore`,不耗预算。
|
|
306
|
+
- 调用方可用 `enqueue(candidate, prior)` 回填已持久化决策,避免重启后重复评估。回填不做认证(调用方账本是信任根),但须与候选自洽:键、摘要、上限、Policy 版本一致,等级合法且不高于上限,引文逐条复核,状态/原因码一致;派生字段(`wouldAccept`、`effect`)按复核后的字段重算。结果报告 `prior: 'adopted' | 'rejected'`,被拒时照常评估。
|
|
307
|
+
- 等级 `ignore < digest < notify < accept`,Host 给出 `ceiling`,模型只能降级;高于上限时钳到上限并记 `upgrade-clamped`。
|
|
308
|
+
- `sourceDigest` 必须是 `sourceText` 的 SHA-256,否则不调用模型直接 `ignore`/`source-unauthenticated`,且不占用该键(随后的真实原文仍可入队)。每条引文必须是认证原文的**精确子串**(UTF-16 比较,不做任何规范化,NFC/NFD 不互认),否则 `ignore`/`quote-not-in-source`;非 ignore 无引文为 `quote-missing`;输出形状不符、稀疏数组、含孤立代理项或仅空白的引文为 `malformed-output`;引文条数/长度(UTF-16 码元)越界为 `quote-bound-exceeded`。模型输出每个字段只读一次。降级时不保留任何模型引文。
|
|
309
|
+
- 预算:滑动窗口 `budget.maxCalls`/`windowMs`(在派发时计数,超时与失败也计入;超时后迟到的结果直接丢弃),每轮 `maxPerRun`;`maxPending` 只限制当前版本尚未落定的项,`maxRetained` 限制总保留量,满时先淘汰最早的已落定或过时项。预算耗尽时剩余项保持排队。
|
|
310
|
+
- 超时(`timeoutMs`,上限 2^31−1 ms,同时 abort 传给 evaluator 的 signal)或抛错按 `backoffBaseMs·2^(n-1)` 退避、上限 `backoffMaxMs` 重排;连续 `maxConsecutiveFailures` 次失败落为 `unavailable`(等级 ignore、无效果)。Host 回调抛错时条目回到排队,不会卡在 in-flight。
|
|
311
|
+
- 调用期间 Policy 版本变化的结果丢弃;旧版本上的条目(已落定或排队)不再评估、`decision()` 不返回,计入 `inspect().awaitingRematch`,等待 Host 按新版本重新匹配后再入队替换——这样新版本的规则与上限不会被旧值顶替。
|
|
312
|
+
- observe 模式下 `effect` 恒为 `none`,仅记录 `wouldAccept`;auto 模式下已决定且非 ignore 才为 `apply`。`decision()` 按当前模式投影:切回 observe 后查到的决策 `effect` 为 `none`。
|
|
313
|
+
|
|
314
|
+
### 准入授权的复核(依次 claim / wake 之前)
|
|
315
|
+
|
|
316
|
+
准入时的 `assistant-policy` allow 是**那一刻**的决定,不是永久的授权。工作项因此在真正派发之前会重新验证自己的“mandate”:同一个 `authorize(request, { idempotencyKey })` 请求在 claim 前和 wake 前各重跑一次,policy 仍是唯一权威。
|
|
317
|
+
|
|
318
|
+
- 准入把 allow 决定持久化为工作项上的 `authority_basis`(**仅供审计/复核,不授予任何新权限**):协议 `work-items/authority/v1`、`action: admit`、background 主体(来源 ID、workspace、principal)、resource(automation id)、`effect: allow`、`reasonCode`、可选 `ruleId`、确定性的 idempotency key 与 `checkedAt`。复核时用这份**冻结**的 basis 重建请求,不从当前配置重算,因此覆盖不了 admission 之后被替换的配置。
|
|
319
|
+
- 复核结果只有两种:allow → 正常继续;deny / policy 抛错 / peer 缺失 / 决定畸形 → **fail closed**:把工作项停在 `waiting`(`stateReason` 为 `authority-revoked` 或 `authority-unavailable`,事件 detail 记 `reason` 与 `phase`),不 dispatch、不 claim、不标 `failed`,也绝不授予新 authority。`emergency-stop` 等 deny 原因原样上报。
|
|
320
|
+
- claim 阶段的拒绝会额外重新武装 dispatch(新一代次、`reason: 'authority-resume'`):occurrence 已存在、单靠 park 会让工作项永远没有下一次唤醒。重新武装只产生一次普通唤醒机会,真正的派发仍要先过同一道复核,因此不构成新的授权。
|
|
321
|
+
- 之后 policy 重新 allow 时,同一工作项**原样恢复**(回到 `queued`,记 `authority-restored` 事件,不重新准入、不重置 attempt),下一次正常唤醒/认领即继续,这就是“在不变 mandate 下恢复”的原型。
|
|
322
|
+
- 不可读/畸形的已存 basis 视为 `revoked`(`malformed-basis`),绝不按 legacy 放行;`null` basis 只对应升级前的旧行(schema v7 之前),按既有 legacy 语义保持可认领。observe 模式、`following-thread` 等待和既有 `waiting-resume` 路径不受影响。
|
|
323
|
+
- 不带 basis 的内部构造(测试或旧行)按 legacy 处理;生产服务总是装配复核器。`health().authority` 暴露 `legacy`/`parked` 计数与最近一次拒绝的时间和原因,`stats()` 暴露 `authorityLegacy`/`authorityParked`;不暴露 basis 内容。
|
|
324
|
+
|
|
325
|
+
### execute 决策准入(冻结 Mandate,WP21 第 1 片)
|
|
326
|
+
|
|
327
|
+
host-only `admitDecision(decision)`(service 与 `WorkItemAdmission` 同名方法)把一条 `work-items/execute-decision/v1` 决策准入为工作项。确定性、无模型调用、不依赖 intake 实现;输入只接受 `{ protocol, decisionId, verdict: 'execute', origin: 'explicit'|'inferred', mode: 'observe'|'auto', policyRuleId?, budgetRemaining, mandate: { kind, title, brief, originBindingId, acceptanceRef } }`,未知字段、畸形 id/预算一律 `invalid-input`。
|
|
328
|
+
|
|
329
|
+
- 身份:`externalId = decision:<decisionId>`,dedup 键只取 intake 源与该 externalId(独立键域 `execute-decision:v1`,不含 kind),普通信号无法占用、重放或撤回某个 decision id;mandate 摘要 `digest` = sha256(协议、decisionId、origin、ruleId、规范化后的 kind/title/brief/originBindingId/acceptanceRef),mode 与预算是准入时事实,不进身份。
|
|
330
|
+
- 重放不开第二项:同一 decisionId 跨所有 dedup 窗口、任何状态(含已结项)、跨重启都回到原工作项(`outcome: 'replayed'`,不再 wake);同 id 不同 digest(含换 kind)为 `identity-mismatch`,不写入。查重与插入在同一同步调用内完成,单进程内无交错;多进程共用同一库时跨窗口的并发首次准入不在本片保证范围(同窗口由 `UNIQUE(dedup_key, dedup_bucket)` 拒绝)。
|
|
331
|
+
- inferred 闸门(在 Policy 调用与任何写入之前):须 `mode: 'auto'`(否则 `inferred-requires-auto`)、命名 `policyRuleId`(`inferred-requires-rule`)、`budgetRemaining >= 1`(`budget-exhausted`);返回 `{ outcome: 'refused', reasonCode }`。explicit 不受 mode/预算约束,但仍过 kind allowlist、绑定校验与 Policy。
|
|
332
|
+
- 命名规则须由 Policy 以**同一** `ruleId` allow,否则 `policy-rule-mismatch`(不写入);deny 仍为 `source-rejected`。规则先用无副作用的 `evaluate` 比对,不一致时不调用 `authorize`,不扣该规则授权预算、不写审计;仅缺少 `evaluate` 的旧 Policy 对端退回到 `authorize` 后比对,此时每个(decision、dedup 窗口)至多扣一次。
|
|
333
|
+
- 冻结:mandate(`work-items/mandate/v1`:decisionId、origin、mode、ruleId?、budgetRemaining、digest)存入既有 `authority_basis` JSON,`admitted` 事件 detail 记 `mandate: { decisionId, origin, digest }`。**无 schema 变更。**
|
|
334
|
+
- 派发复核:带 ruleId 的 mandate 在 claim/wake 前要求 Policy 仍由同一规则 allow,否则 park 为 `waiting`(`mandate-rule-changed`),不取消;畸形、不可能通过闸门或 decisionId 与工作项 externalId 不符的已存 mandate 视为 `malformed-basis`。
|
|
335
|
+
- 未覆盖:`budgetRemaining` 是决策方上报的事实,不是 Host 持有的预算;observe 决策记录未实现(Policy 变更通知与跟进归属见下节,独立验收绑定见第 4 片)。
|
|
336
|
+
|
|
337
|
+
### 独立验收绑定(WP21 第 4 片)
|
|
338
|
+
|
|
339
|
+
- 决策可选带 `acceptance: { contractId, contractDigest }`,只是句柄,不接受契约正文或判定。准入时 WorkItems 自己经 `assistantVerifier.inspectAcceptedTask(contractId)` 读回契约:须通过 `validateTaskAcceptanceContract`、id/digest 与句柄一致、`scope.workspace/preset` 与 owner 精确一致、`owner.principalRecordId/principalVersion` 一致、未过期、尚无回执且状态恰为 `awaiting-execution`(pending/verifying 可能用本项运行前采集的证据结算,拒为 `acceptance-not-awaiting`)、且该 contractId 尚未被任何工作项绑定(一契约只绑一项,`acceptance-contract-bound`;同一 decisionId 重放先于此检查,仍回原项);否则在 Policy 与任何写入之前拒绝(`acceptance-unavailable`/`-mismatch`/`-expired`/`-already-settled`/`-not-awaiting`/`-contract-bound`/`-not-independent`)。
|
|
340
|
+
- 冻结:mandate 新增 `acceptance`(`work-items/acceptance-binding/v1`:verifier=`assistant-verifier`、author、contractId/Digest、taskKind、task/objective/profile 摘要、criteria 数、expiresAt)。author 由契约 task kind 推出(automation-run→automations、foreground-turn→delivery、goal-*→goals),须与 verifier、执行方 `assistant-work-items` 三方互异;已存绑定结构不符即 `malformed-basis`。句柄进入 mandate digest(无句柄时 digest 与第 1 片完全一致),同 decisionId 换句柄为 `identity-mismatch`。
|
|
341
|
+
- 完成判定:host-only `settleAcceptance(itemId)` 不接受判定输入,只从 verifier 读回同一契约的 `done` 回执并以 `validateTaskVerificationReceipt` 校验,objectiveStatus 取回执由准则结果推出的值;回执 `startedAt` 早于工作项创建时间、契约漂移、回执无效或缺 verifier 一律不写(`drift`/`unavailable`)。结算前先做本地无副作用门:已存依据须完整且指向本项(不调用 Policy,不写审计),且工作项须已被认领执行并处于 `ready`;从未认领、park(含授权撤销)、blocked、failed、dropped 均为 `invalid-state`。每项至多一条 `acceptance-verified` 事件,先到回执胜出:同回执重放幂等(`already-verified`),已记判定与当前绑定契约不符为 `drift`,竞争中落败的不同回执返回 `{ outcome: 'identity-mismatch', verdict, receiptDigest }` 而非抛错,绝不覆盖;`acceptanceVerdict(itemId)` 读回。worker 的 `ready` 报告不产生任何判定。仅追加事件行,**无 schema 变更**。
|
|
342
|
+
- 独立性依赖宿主注入的 `assistantVerifier` 服务槽:WorkItems 只信任该槽提供的契约与回执,宿主须保证注入的是真正独立的 verifier,而非执行方自身或其代理。
|
|
343
|
+
- 自动结算(WP21 第 5 片):`acceptance: { autoSettle: { enabled: true, maxPerTick?: 1–10,默认 5 } }` 默认关,开启须 enabled + active 部署;关闭时零行为变化。不新增定时器,只搭既有 Automations scheduler wake:每次 wake 在等待恢复扫描之后,按 (created_at, id) 内存游标轮转选出至多 `maxPerTick` 个 `ready`、attempt≥1、绑定了 acceptance 合同且尚无 `acceptance-verified` 事件的工作项,逐个走与 host-only `settleAcceptance(itemId)` 完全相同的路径(门禁、首个回执胜出、漂移判定语义不变)。verifier 槽缺失时整轮跳过不扫描;verifier 抛错、`pending`、`drift`、`needs-attention` 均不写判定行,下次 wake 重读(开启裁决门时 `needs-attention` 会把项转入 `blocked`,见第 7 片);单项异常只记 debug 日志,不影响 wake 其余工作。已记判定的项离开候选集,不再读 verifier。游标仅在内存,重启从头轮转。
|
|
344
|
+
- 晚绑定 worker run 合同(WP21 第 6 片):`acceptance: { bindWorkerRun: { enabled: true, maxPerTick?: 1–10,默认 5 } }` 默认关,开启须 enabled + active 部署;关闭时零行为变化(不读 Automations 句柄、不写行)。生产中准入时尚无合同,真正验收工作项执行的是 worker 运行开始时由 Automations 冻结的 `automation-run` 合同,因此改为 wake 后绑定:同一 scheduler wake 中(先于自动结算)按内存游标轮转选出至多 `maxPerTick` 个 dispatch 为 `dispatched`、发往 `workerAutomationId`、occurrence 与工作项当前 occurrence 一致、mandate 未绑定合同、尚无判定且该 occurrence 尚无绑定行的工作项(failed/dropped 除外);经 Automations host-only `inspectOccurrenceAcceptance` 按精确 (automation, occurrence, workspace, preset) 取 production worker task 的 `{ contractId, contractDigest }` 句柄及其冻结时的 Delivery binding(`bindingId/bindingVersion/bindingGeneration`),再自己从 verifier 读回契约,走与准入相同的 `resolveAcceptanceBinding` 校验(owner/scope、未过期、三方独立),差异在于:契约 `task.ref` 必须恰为 `run-task-<occurrenceId>`(与 Automations 的 task id 推导刻意耦合);状态门放宽为 `awaiting-execution`、`pending`,以及 run 在两次 wake 之间已结束的 `verifying`/`done`——后两者另要求契约 `issuedAt` 与(`done` 时经 `validateTaskVerificationReceipt` 校验的)回执 `startedAt` 均不早于本代 dispatch 时间 `max(item createdAt, dispatch occurredAt)`(re-arm 把 occurredAt 设为 re-arm/恢复到期时间,wake 不早于它);`needs-attention` 从不绑定。准入路径的状态门不变。绑定前另核对实时 owner route 与工作项准入时的 principal/binding 版本一致,且句柄的 bindingId/bindingVersion/bindingGeneration 须等于该 route 当前 active binding 的 id/version/generation。binding id 经 Delivery host-only `inspectOwnerRouteBinding` 读取(v2 owner route 回执本身仍不含 binding id),其内嵌回执须与刚校验的 `validateOwnerRoute` 回执逐字段相等;三种情况都失败关闭,不退回只比对 version/generation:Delivery 缺该方法(旧版本)判 `owner-route-binding-unsupported`;内嵌回执与刚读的回执不符(两次读取之间 route 变了)判 `owner-route-drift`。这两种都不停放该 (item, occurrence),下次 wake 重读,所以 Delivery 升级或瞬态漂移后可以自行恢复。应答格式不对(协议不符、内嵌回执缺字段或字段类型不符,或 binding id 为空、超过 256 字符)判 `owner-route-binding-unknown`,确定性停放。缺方法时每进程 warn 一次;方法恢复后再次缺失会重新 warn。同一 (item, occurrence) 连续 5 次 `owner-route-drift` 时 warn 一次(内存计数,其他任何结果含绑定成功、或 occurrence 变化即清零,进程重启也从零开始),只提供可见性,重读行为不变。因此开启 `bindWorkerRun` 需配套提供 `inspectOwnerRouteBinding` 的 Delivery。
|
|
345
|
+
确定性拒绝(契约过期、ref/owner/摘要不符、已越过可绑定状态或早于本代 dispatch、非独立、owner 变更、route 过期、binding 不符、binding 应答畸形(`owner-route-binding-unknown`:协议不符、回执缺字段或字段类型不符、binding id 非法)、合同已被他项绑定或本 occurrence 已绑他合同的 `identity-mismatch`、最新绑定行畸形)在内存中记 (itemId, occurrenceId) 跳过集,直到该项 occurrence 变化才重读,且跳过项不占每 wake 的 `maxPerTick` 额度(每 wake 至多扫描 100 行,跳过集至多 1000 对,满时最早的一对被遗忘并重读一次);暂时性情况(尚无合同,如 Automations 未接 verifier;verifier/Automations 缺失或抛错;owner route 暂不可用;Delivery 缺 `inspectOwnerRouteBinding`;两次读取之间回执漂移)继续有界重读。Delivery 的 `missing-binding`(route 无 active binding、owner 撤销、scope 不符等)按确定性处理:被撤销的 binding 不会复活,重新 link 或 `/new` 都产生更高的 generation,准入时冻结的 generation 不可能恢复;Delivery 的其他失败(`owner-route-unavailable`)仍按暂时性重读。跳过集仅在内存,重启后每对至多重读一次。拒绝只记 debug。最新一条 `acceptance-bound` 行畸形时保持 fail-closed(不绑定、`settleAcceptance` 抛 `storage`),每个 (itemId, 行序号) 只记一次 warn。结果只追加一条 `acceptance-bound` 事件行(`{ occurrenceId, binding }`),**不进 mandate digest、无 schema 变更**;一契约仍只绑一项(准入与晚绑定共用 `acceptance-contract-bound` 判定)。mandate 已绑定的项从不晚绑定。`settleAcceptance` 与自动结算同时认 mandate 绑定与事件绑定;事件绑定以最新一行为准,且只在其 occurrence 仍是工作项当前 occurrence 时可结算(否则 `invalid-state`)。re-arm(如 waiting 恢复)后,**仅限尚无判定的项**,新 occurrence 的合同追加新行取代未结算的旧绑定(提交说明中"a re-armed item follows its newest run"即此义)。已有判定的项被 re-arm 后不再晚绑定新 run,也不再读其句柄:每项至多一条 `acceptance-verified` 判定,且判定先到者胜、从不覆盖,因此新 run 无处记录自己的判定;为它再绑合同只会产生永远无法结算的绑定。旧代判定保留,描述的是该判定所属的那次 run,而非 re-arm 后的执行。Automations 槽缺 `inspectOccurrenceAcceptance`、verifier 缺失时整轮跳过;单项异常与 autoSettle 一样在 finally 内隔离,不抛进 wake。
|
|
346
|
+
局限:worker 合同的验收准则来自 Automations 侧共用的 worker prompt 与 verifier profile,而非每个工作项的 brief/acceptanceRef,因此判定的是"这次 worker 运行是否满足共用准则",不是逐项定制验收;`issuedAt`/`startedAt` 下限按 dispatch 的 occurredAt 计,而非 Automations 实际 wake 时间,工作项侧拿不到更晚且可靠的时间点;从未产出合同的 occurrence(如 Automations 未接 verifier)会在每次 wake 被有界重读;Delivery 缺 `inspectOwnerRouteBinding` 时,每个候选项每次 wake 也会重读(每 wake 至多 `maxPerTick` 次句柄与 route 读取),不再读一次就停放;owner continuation 等非 worker dispatch 的 occurrence 不晚绑定;游标仅在内存。
|
|
347
|
+
- 裁决门(WP21 第 7 片):`acceptance: { verdictGate: { enabled: true } }` 默认关,开启须 enabled + active 部署;关闭时判定只记事件、零行为变化。开启后 `settleAcceptance`(含自动结算)在判定写入的**同一事务**内:objective `not-achieved` 或 `unknown` 把已执行(attempt ≥ 1)的 `ready` 项转入既有终态 `blocked`,`stateReason` 为 `acceptance-not-achieved` / `acceptance-unknown`;verifier 返回 `needs-attention`(终态,不写判定)时同样转 `blocked`,原因 `acceptance-needs-attention`;`achieved` 不改状态,项保持 `ready`,即既有完成路径。每次转换追加一条 actor 为 `verifier` 的 `blocked` 事件(`reason`、`attempt`、`contractId`,以及 `receiptDigest` 或截断到 256 字节的 verifier 原因)。判定事件同时记下结算时的 attempt 与 occurrence(不论门是否开启都记录,因此门后开时仍能补做;门关闭时工作项状态零变化,但判定事件与 `acceptanceVerdict()` 结果多出这两个字段),门只移动该判定所属的那一代:按 (state `ready`、attempt、occurrence) 比较并交换,项已 re-arm、撤回、已被阻塞或判定属于另一代时不写任何行;第 7 片之前记录、不带代次的判定从不触发移动。「代」指结算时的代次,不是产出回执的那次运行:晚绑定合同按 occurrence 绑定,代次与运行一致;准入时绑定的合同跨所有 attempt 共用,第 1 次 attempt(如租约过期后被回收)产出的回执或 `needs-attention` 仍可能阻塞第 2 次 attempt 的 `ready`。门在判定之后才开启时,下一次 Host 调用 `settleAcceptance` 以同一回执幂等补做(自动结算只扫无判定的项,不会补做)。终态项(含被门阻塞的项)在 claim 时不再经 Policy 复查被改回 `waiting` 并 re-arm,由账本直接以 `claim-conflict` 拒绝。真实 verifier 只对已知结论落 `done`,objective `unknown` 的回执会重试或进入 `needs-attention`,因此实际经 `acceptance-needs-attention` 阻塞;`acceptance-unknown` 仅在 `done` 回执携带 unknown 时出现。这是事后降级而非前置门禁:判定到达之前项已是 `ready`,主人通知、续办与 Evaluation 已按 worker 自报的完成处理,verifier 不可用或尚在验证时项保持 `ready`。结果的 `blockedReason` 仅在本次调用确实转入 `blocked` 时出现。无 schema 变更;outcome 表不改写,worker 自报的 `ready` 修订保留为历史,Evaluation 导出仍按 outcome 表;转入 `blocked` 不另发 owner 通知,开启 `ownerRecall` 时主人可经 owner recall 看到 `blocked` 与原因。
|
|
348
|
+
- 未覆盖:裁决门关闭时判定只记事件;开启时也只改工作项状态,不追加 outcome 修订;漂移项不会被永久排除,每轮仍占一个读额度;尚无调用方提供句柄;contractId 查重与自动结算候选查询按已存依据 JSON 全表扫描(无索引)。
|
|
349
|
+
- 报告持有(WP21 第 8 片):`acceptance: { holdReport: { enabled: true, holdTimeoutMs?: 60_000–86_400_000,默认 1_800_000 } }` 默认关,开启须 enabled + active、无 diagnosis,并同时开启 `acceptance.bindWorkerRun` 与 `acceptance.autoSettle`;关闭时零行为变化(报告照常在 `report()` 内立即进 Delivery)。开启后,已绑定验收合同(mandate,或属于当前 occurrence 的最新晚绑定事件行)的项在 worker 报出可通知的 `ready` 终态报告时不再当场派发:`report()` 只追加报告与状态,`delivery_state` 保持 `null`,返回 `notified: false`。释放不是新定时器,而是同一条既有 Automations scheduler wake:每次 wake 在 bindWorkerRun 与 autoSettle 之后,按 `(报告 recorded_at, item id)` 内存游标轮转、至多检查 10 条“最新报告为 `ready`、尚无投递状态、项为 `ready` 或已被验收门转 `blocked`”的报告(整页 10 条就把游标推进到本页最后一行,短页或空页则清空游标、下一次 wake 从最旧重新开始;因此仍等待判定的行最多占用一轮窗口,不会饿死其后的可释放报告,游标仅在内存、重启从头轮转)——已记 `achieved` 判定则按普通 `ready` 报告发送;`not-achieved`/`unknown` 判定或 `needs-attention` 造成的 `acceptance-* blocked` 项改发 `blocked` 报告(正文含 `acceptance-*` 原因,投递键为 `<deliveryKey>:blocked`,仍记在被打断的 `ready` 修订上);无判定且已到 `recorded_at + holdTimeoutMs` 则按普通 `ready` 报告发送,正文追加 `_Unverified: no verifier verdict arrived within the N-minute hold window..._` 表明未经独立验收;未到时限且无判定则继续持有。扫描到的行若已无验收绑定(报告提交后绑定丢失或被 re-arm 取代、报告时 Delivery 缺失或崩溃在 pending 投递前、或当时 `notifyOn` 不含 `ready`),按普通 `ready` 报告恢复发送(不附 unverified 说明、沿用原 `deliveryKey`),不再无限期滞留。持有项**隐含裁决门**:即使 `acceptance.verdictGate` 关闭,`settleAcceptance`(含自动结算)也按该判定所属代次把持有项从 `ready` 转入 `blocked`,否则 blocked 报告无处产生;这是对 `verdictGate` 的逐项应用,不是另开旁路。释放不新增 schema 或状态:判定、晚绑定与报告行都是既有持久事实,`delivery_state` 保证同一报告至多重试一次投递;scheduler wake 被跳过、进程重启或第二次 wake 都不会重发。单行读取异常(畸形 `acceptance-bound` 或 `acceptance-verified` 事件)只告警该行并跳过,不中断同一轮其余释放;该行自身维持 fail-closed 持有(畸形绑定行按 (item, 行序号) 只告警一次)。
|
|
350
|
+
失败模式(均为实测语义):verifier 槽缺失或抛错时 autoSettle 整轮/单项跳过,持有项不因此永久挂起,超过 `holdTimeoutMs` 的下一次 wake 仍以 unverified 释放;报告发生时尚未绑定(例如 worker run 极快、绑定 wake 还没发生)的项按未绑定处理**立即正常发送**,之后的晚绑定不会追溯持有或撤回该报告;报告时若工作项已存在属于更早代次的判定(其 attempt/occurrence 与当前代不符;每项只保留一条判定且已判定项不再自动结算),该 `ready` 报告不持有、按关闭开关时立即派发——否则它必然等满 `holdTimeoutMs` 才以 unverified 释放;判定在报告之后才到达的旧代次仍不匹配新报告,只能等超时;判定读取抛错时 fail closed 继续持有;释放时 owner route 已撤销(Delivery `missing-binding`)则记录 `failed` 并告警,报告不发出也不再重试,项保持原状态;`ready`/`blocked` 项被撤回或转为 observed 后不再出现在持有扫描中,不再发送;重启只重建内存游标,持有的判定来自持久报告行、`recorded_at`、判定事件与绑定事件,因此第一次 wake 即可继续,且不会重复发送。
|
|
351
|
+
局限:`holdTimeoutMs` 只在既有 scheduler wake 上检查,没有独立定时器,因此实际释放时刻可能晚于截止时间一个 tick;持有只覆盖 `ready` 报告(`blocked`/`failed` 报告与 waiting/progress 不持有);`delivery.notifyOn` 不含 `ready` 时不会持有,不含 `blocked` 时非 achieved 结算只把项转 `blocked` 而不发 blocked 报告,并把该报告修订记为终端 `failed` 以免每次 wake 重扫;仍跟随打开线程的项会把 `ready` 报告暂存为 `waiting`,此类报告不在持有范围(按普通路径立即通知);判定只在其代次与工作项当前 attempt/occurrence 一致时才决定该报告:报告时已存在旧代判定则报告不持有、立即正常派发;报告之后才到的旧代判定既不移动项也不决定新报告,持有项等本代判定或超时;outcome/Eval 仍在报告时按自报 `ready` 记 `report-ready`,持有只推迟主通知,不推迟结果修订与评估导出;投递失败/未知与普通报告一致,不做无界重试;持有扫描每 wake 至多检查 10 行,最后一条可释放行最坏要等一轮轮转(约一个 wake tick × ceil(行数/10))才被看到;畸形判定/绑定行保持 fail-closed 持有,只能靠修复事件数据解除(畸形判定行每次 wake 告警)。
|
|
352
|
+
### Policy 变更通知与跟进归属(WP21 第 2 片)
|
|
353
|
+
|
|
354
|
+
两者均为确定性、无模型调用的 host-only 接口,只追加 `work_item_event` 行(`policy-change-noticed`、`follow-up-owned`、`follow-up-processed`、`follow-up-released`),**无 schema 变更**;幂等靠在同一事务内按 `json_extract(detail, …)` 查重。
|
|
355
|
+
|
|
356
|
+
- `noticePolicyChange({ protocol: 'work-items/policy-change/v1', policyVersion, after? })`:`policyVersion` 是宿主给的不透明令牌(配置摘要、修订号)。按准入顺序对活工作项(queued/claimed/working/waiting,非 observed)用 Policy `evaluate` 重算准入(无审计、不扣预算),与 `check()` 同一比较与同一本地完整性判断:deny(记其 reasonCode)、命名规则改变(`mandate-rule-changed`)、已存依据畸形或 decisionId 不指向本项(`malformed-basis`)即"受影响";单次最多评估 1000 项,不静默截断:返回 `remaining`(本页之后的活项数)与 `next` 游标,以 `after: next` 续跑同一版本,每个 (item, policyVersion) 记一条通知(frozenRuleId、currentRuleId、decisionId)。**不取消、不 park、不 wake、不改租约**;派发复核仍是唯一 park 门。重放同一版本(含重启后)只返回 `alreadyNotified`。先全部评估、再在一个事务内写入全部受影响通知(写失败则本次一条不写,重试同版本重新推导)。返回 `notified/alreadyNotified/unaffected/legacy/unavailable/remaining/next`;单项 evaluate 抛错列入 `unavailable`,下次同版本重试;缺 Policy 或 Policy 无 `evaluate` 时在评估前即整体 `{ outcome: 'unavailable' }`,不写任何行(含本地即可判定的畸形依据)。`policyNotices(itemId)` 读回。
|
|
357
|
+
- `ownFollowUp({ protocol: 'work-items/follow-up/v1', itemId, bindingId, messageId, sourceDigest })`:工作线程(`bindingId` 必须等于 `originBindingId`)上的人类跟进归该活工作项;不存消息正文。跟进身份是(工作线程 bindingId、messageId):同 (item, messageId, digest) 幂等(`already-owned`),同一线程上该 messageId 已被别的项或别的 digest 占用为 `identity-mismatch`(先到者占有,宿主只应在观测到真实消息后调用);不同线程上的同名 messageId 互不冲突。项已结束或为 observed 影子项返回 `item-ended`,不写入,消息留给判断。
|
|
358
|
+
- `markFollowUpProcessed(itemId, messageId)`:仅活项可标记;项一结束,未处理的跟进即属于判断,再标记为 `invalid-state`(无论释放扫描是否已跑)。
|
|
359
|
+
- `releaseFollowUps(limit?)`:把已结束项(ready/blocked/failed/dropped,含报告、撤回、重试耗尽、线程关闭等任一终态路径)上未处理的跟进物化为 `released`(记 `releasedFrom`)并返回新释放的列表(每次至多 `limit`,默认 100、上限 1000,调用至返回空即清完);只由持久状态推导,重启安全,二次调用为空。不发送任何消息。`listReleasedFollowUps({ limit, after })` 按释放顺序分页重读;`followUps(itemId)` 读单项。
|
|
360
|
+
- 未覆盖:没有 intake/Delivery 调用方(宿主需在 Policy 配置变化时调用 `noticePolicyChange`、在观测到工作线程消息时调用 `ownFollowUp`);通知不预测 claim 时 `authorize` 的预算扣减;释放是惰性扫描而非终态钩子;查重无表达式索引:通知查重按单项事件主键前缀扫描;跟进占有查重只扫同一 originBindingId 的项的事件;释放扫描会走全部 `follow-up-owned` 事件(含历史已结束项),代价随历史增长。
|
|
361
|
+
|
|
362
|
+
## 结果证据与结算(`ready` 只是报告)
|
|
363
|
+
|
|
364
|
+
- 终态与客观结果分离:`ready` 只表示 worker 自报完成(`trust: agent-self-report`);`objectiveStatus` 目前只能从独立证据推导为 `unknown`,不会出现 `achieved`/`partial`。
|
|
365
|
+
- 每条终态报告在同一事务里追加一条 append-only 结果修订(`work_item_outcome`,主键 `item_id + revision`):`report-ready`、`report-blocked`、`report-failed`、撤回 `cancelled/external-receipt`、重试耗尽 `retry-exhausted`。已结项的历史修订永不改写。
|
|
366
|
+
- 只有带可结算 provenance(source=`owner-foreground-failures`、`evidence.inboxId`、`evidence.completionSequence`)的准入才打开证据窗:`evidenceState: pending`,`deadlineAt = 报告时间 + settlement.windowMs`,基线取准入 `admitted` 事件 detail 里的 provenance。没有独立来源时直接 `not-applicable`(`no-independent-source`),不制造假的“待验证”。
|
|
367
|
+
- 结算不新增 timer:只在 `claim()` 或宿主显式 `settleOutcomes(force)` 时被动运行,受持久化键 `settlement.sweptAt` 门控;分页游标 `settlement.cursor` 仅在 peer 同时返回 `hasMore` 和非空 `nextCursor` 时保留,否则清除并从下一页顶部重新开始,避免把旧游标当成新证据。单次 pass 只读一页;`maxSourcesPerSweep` 是该页的**条数**(作为 `limit`),`maxPendingPerSweep` 是本 pass 检查的窗口数上限;待检查窗口按 `deadlineAt` 升序取前 N 个,超出部分顺延到后续 pass。`maxSettlementsPerSweep` 同时约束 observation 与 expiry 的追加总数。
|
|
368
|
+
- 证据匹配是严格相等而非“看起来像”:同一 `inboxId`、scope/principal/authorityHash 与准入时持久化的 owner route 一致(不是本次 pass 重算的值)、`source.protocol` 必须等于本插件约定的证据协议、`execution.status === 'succeeded'`、`quiescent === true`,且 `completionSequence > baselineSequence`。命中后追加 revision N+1:`observed-evidence`、`executionStatus: succeeded`、`trust: external-receipt`、`evidenceState: observed`;observation 只扩大信任面,`objectiveStatus` 仍是 `unknown`。
|
|
369
|
+
- 窗口到期仍无证据时追加 `window-expired`:保留原执行状态,`evidenceState: unverified`。channel 健康但安静记 `window-expired`;channel 不可用/路由撤销、或到期时本页读取抛错记 `channel-unavailable`(degraded)。两者都不会把结果写成成功;`channel-unavailable` 是终态,窗口关闭后不会因为之后 channel 恢复而重试成 observation。
|
|
370
|
+
- 证据只保留 allowlist 字段(`inboxId`、`completionSequence`、`completedAt`、`executionStatus`、`sourceDigest`、`bindingVersion`、`bindingGeneration`、`scope`),总长不超过 2048 字节,不读取 owner 原文。超限负载**整体丢弃**、绝不截断:报告 ready 路径记为 `not-applicable` / `evidenceReason: 'evidence-too-large'`,结算路径记为 `unverified` / `evidence-too-large`。
|
|
371
|
+
- host-only 接口:`settleOutcomes(force)`、`outcomes({ limit, evidenceState })`(没有对应的模型工具);`status()` 增加 `outcome` 视图,`health().settlement` 与 `stats()` 暴露 pending/observed/unverified 计数。`settlement.enabled: false` 时报告 `skipped: 'disabled'` 且不扫描。
|
|
372
|
+
- 缺 `assistant-delivery`、缺 `listOwnerForegroundTaskSources` 或 owner route 撤销时只返回 `degraded`/`failed` 并 warn,不抛错、不写假结算,也不会让 `claim()` 失败。
|
|
373
|
+
|
|
374
|
+
### 跟随线程的提前收束(follows)
|
|
375
|
+
|
|
376
|
+
外部线程(ticket、PR 等)的关闭是**完成信号**,不是目标达成证据;它可以让一项进入终态报告后的等待提前结束,但不能替 owner 判断 objective 已实现。
|
|
377
|
+
|
|
378
|
+
- 准入时可选 `follows: WorkItemThreadRef[]`(`{ kind, ref }`,最多 8 条,按 `kind + ref` 去重;非法/超限/伪造来源整条准入失败),持久化到 `work_item_follow`(schema v6,`STRICT` + `WITHOUT ROWID`,主键 `item_id + position`,并有 open 状态查询索引)。信号源候选也可带 `follows`,原样进入准入。
|
|
379
|
+
- 只有 host 能注册解析器:`registerThreadSource({ source, generation, resolve(ref) })`。`resolve` **同步、只读、无 Promise**;每个 `source` 只能注册一次,重复/畸形注册 fail closed,返回的 disposer 撤销当前注册。注册的 `source` 必须与 follow 的 `kind` 同名:准入只把 `kind` 解析到对应注册,并把该注册的 `source`/`generation` 冻结在 follow 行上。
|
|
380
|
+
- 终态报告遇到仍 open 的 follow 时不追加 outcome,而是把 item 停在 `waiting`(`stateReason: 'following-thread'`,`last_report_status` 记下报告状态,claim 行收尾)——此时仍可被认领,且 `settleOutcomes` 报告 `deferred`。
|
|
381
|
+
- 结算 pass 在原有窗口检查之后、受同一 `maxSettlementsPerSweep` 预算约束地惰性解析 open follow:`open` 继续等待;`closed` 写回 `outcome`/`digest`/`closed_at`(CAS `state='open'`,重复或乱序的关闭只会命中一次)。当且仅当该 item 已无 open follow 时,恰好追加一条 `settledBy: 'closed-by-thread'`、`evidenceState: 'unverified'` 的修订;窗口到期仍是 `window-expired`。关闭**不会**设置 `objectiveStatus: achieved`,`observed` 仍只由交付 provenance 规则产生。结算候选同时要求 `waiting` 且没有 lease:被新 worker 重新 claim 的 item 不会被线程关闭在其运行期间收束。
|
|
382
|
+
- 无法解析时保持 `waiting` 并显式降级,绝不把缺失当成关闭:`source-missing`(注册已撤销/从未注册)、`generation-mismatch`(注册代际与准入时冻结的代际不一致)、`resolver-failed`(resolver 抛错、返回 undefined 或畸形 closure)。`health().follows` 暴露 `open`/`closed`/`unresolved` 计数与 `degraded` 原因(`source-missing` > `generation-mismatch` > 最近一次结算的降级),`stats().follows` 暴露总数;不暴露 ref 或 digest。open/unresolved 只统计仍活跃的 item,撤回或被其他路径结项的 item 留下的 follow 行不会造成永久降级。
|
|
383
|
+
- 没有 timer:只有 `claim()` 或宿主显式 `settleOutcomes(force)` 触发解析;无 follow 的 item 走原有窗口路径,行为不变。follow 不是新的 authority store,也不产生 objective-achievement 推断。
|
|
384
|
+
- 与 Evaluation 的衔接:`closed-by-thread` 修订按 unverified 结算投影(`executionStatus: 'unknown'`、`evaluator.version: 'unverified-v1'`、`objectiveStatus`/`deliveryStatus` 为 `unknown`、`trust: 'trusted'`),幂等键仍为 `work-items/<itemId>/<revision>/<evidenceState>`。协议 `assistant-work-items/evaluation/v1` 原生接受该原因,不做 `window-expired` 之类的别名替换。
|
|
385
|
+
- 注册形状额外接受可选 `validate(ref): boolean`(同步、只读);返回 false 或抛错时整条准入以 `invalid-input` 失败,不写任何行。未提供 `validate` 的既有来源行为不变。
|
|
386
|
+
- 注册形状还接受可选 `canonicalize(ref): string | undefined`(同步、只读),只对新建项在 `validate` 之前运行:返回值替换原 ref 交给 `validate` 并入库,返回 undefined 或抛错时以同一 `invalid-input` 失败;改写后相同的 ref 按重复折叠。
|
|
387
|
+
|
|
388
|
+
#### 订阅直通:event-triggers 来源(slice 1)
|
|
389
|
+
|
|
390
|
+
配置 `follows: { eventTriggers: { enabled: true } }`(默认 `false`;要求 enabled intake 且有完整 `scope`)且宿主提供 `eventTriggers` 服务时,本插件在启动时自行注册 `source: 'event-triggers'`,让工作项跟随一个事件订阅:
|
|
391
|
+
|
|
392
|
+
- ref 形如 `<triggerId>@<afterSequence>@<configDigest>`(`configDigest` 为该 trigger 当时的 64 位小写 hex 配置摘要)。准入时 `validate` 要求 trigger 属于当前 owner(经 Delivery `validateOwnerRoute` 取得的 principal record/version、workspace、preset,与 `inspectOwnerSources` 范围一致)、`configDigest` 等于该 trigger 当前配置摘要,且 `afterSequence` **等于**当前 `highWaterSequence`。`highWaterSequence` 是 event-triggers store 的全局序号,不分 trigger。更低的序号(伪造的旧序号,可能借旧事件关闭)、更高的序号、过期摘要、他人 trigger、格式错误一律拒绝。安全性依赖这一次准入时检查:resolve 只是回放已存 ref 中的序号。
|
|
393
|
+
- `follows.eventTriggers.acceptHeadRef: true`(默认 `false`)时,Host 与 owner 准入入口还接受 `<triggerId>@head`:准入在同一同步段内(读取与写入之间没有 await)检查 owner 归属、读 `sourceSnapshot` 取当时的 `highWaterSequence` 与 `configDigest`,改写为三段式 canonical ref,再走上面的 `validate` 并入库。存储、`follows()`、health 与报告里只出现 canonical 形式。未知或他人 trigger 与三段式一样以 `invalid-input` 拒绝;显式三段式 ref 的校验不变(伪造/过期序号或摘要仍拒绝)。开关关闭时 `@head` 不是合法 ref,以 `invalid-input` 拒绝。
|
|
394
|
+
- `validate` 只对新建项运行:同一信号在 dedup 窗口内重试、已结算项的 fence、decision/continuation 重放都先于它返回原项,因此新事件到达后重试会得到 `duplicate` 而不是 `invalid-input`。
|
|
395
|
+
- generation 为 `event-triggers/v1/<sha256(principalRecordId, principalVersion, workspace, preset)>`,只取决于 owner 身份,不随 trigger 运行时状态(observer 到期、goal 退役、automation 暂停、配置编辑)或重新 provide 变化。`v1` 是 follow 协议常量:Event Triggers 只在单个 trigger 的 snapshot 上暴露版本号;升级后 `firstEventAfter` 只匹配同版本 envelope,旧 follow 保持 open。
|
|
396
|
+
- resolve 同步调用 `firstEventAfter`:命中已认证事件 → `closed`,`outcome: 'unknown'`,`digest` 为该事件 envelope 的 `externalEventDigest`(事件到达不等于目标达成,结算仍为 unverified);未命中 → `open`。
|
|
397
|
+
- 降级按 follow 隔离:某个 trigger 被停用、observer 到期、不再属于 owner 或配置摘要变化时,只有跟随它的 follow 记为 `resolver-failed` 并保持 open(不会被当作关闭),其他 trigger 的 follow 与新准入不受影响。owner 身份变化(principal version 等)会让全部 event follow 在重新注册前为 `resolver-failed`,重新注册后为 `generation-mismatch`。
|
|
398
|
+
- 仅被动:沿用上文结算 pass 与 CAS,无 timer、不订阅 `subscribeSourceChanges`;无 schema 变更。开关关闭时不注册、不读 event-triggers,宿主仍可自行注册同名来源。
|
|
399
|
+
- 限制:显式三段式 ref 的序号须精确等于全局 high water,调用方要在读到 snapshot 后立即准入,期间若有任何 trigger 的新事件写入,准入会被拒绝,需重读后重试;开启 `acceptHeadRef` 后改用 `<triggerId>@head` 可免去这一竞争(游标取准入时刻的 high water,调用方读 snapshot 之后、准入之前到达的事件不会关闭该 follow);trigger 配置一经编辑,旧 follow 永久 `resolver-failed`(不会自动迁移到新摘要);owner principal version 变化后旧 follow 无法解析。
|
|
400
|
+
|
|
401
|
+
### 可信 Evaluation 导出(第四个 trusted producer)
|
|
402
|
+
|
|
403
|
+
已结算的结果修订(`evidenceState != 'pending'`)会投影到 `@dsh-enhanced/assistant-evaluation` 的 trusted lane,作为该账本与交付/自动化的 latter 之外的**证据**,而不是目标判断:
|
|
404
|
+
|
|
405
|
+
- 每个结算修订最多投影一条 Evaluation 记录,幂等键为确定性的 `work-items/<itemId>/<revision>/<evidenceState>`;相同重放安全,内容变化由 Evaluation 以 `idempotency-conflict` 失败关闭。
|
|
406
|
+
- `evidenceState: observed` → `executionStatus: succeeded` 且 `evaluator.version: settlement-v1`;`unverified` / `not-applicable` → `executionStatus: unknown` 且 `unverified-v1`。投影的 `objectiveStatus` 与 `deliveryStatus` 在类型和运行时都只能是 `unknown`:work-item 结算永远不能把自身升级为目标达成证据,也不会因报告过 `ready` 而被 Evaluation 当作 achieved。
|
|
407
|
+
- 复用与 verifier 相同的私有 producer registration seam:`assistantEvaluation` 通过 `ctx` 发现本服务的 `trustedWorkItemProducerGeneration()` 与 `registerTrustedWorkItemEvaluationSink(registration)`,并校验 protocol(`assistant-work-items/evaluation/v1`)、generation、ownership 和单注册不变量;本插件不直接写 Evaluation 数据库。
|
|
408
|
+
- `assistant-evaluation` 是可选 peer:缺席时结算行保持 `pending`,不阻塞 claim/report;绑定后下一次 drain 会补导既有 pending 行。
|
|
409
|
+
- 有界且诚实降级:每 sweep 最多 10 条,失败最多重试 5 次后标记 `export-attempts-exhausted`;部署没有精确 workspace+preset scope 时标记 `skipped`(`owner-route-unavailable`);observe 模式不导出。状态写入 `work_item_outcome.evaluation_*`(schema v5 引入,后续版本保持),`health().evaluation` 与 `stats()` 暴露 `evaluationPending/exported/skipped`。
|
|
410
|
+
- 依赖方向:本插件只以 **type-only import** 引用 `@dsh-enhanced/assistant-evaluation` 的 claims/registration 契约类型,并把该包声明为 optional peer + devDependency;Evaluation 保持 producer-agnostic,不依赖任何 producer 运行时。
|
|
411
|
+
|
|
412
|
+
#### 工作项执行指标(metrics)
|
|
413
|
+
|
|
414
|
+
投影的 `metrics` 不再只有 live 记录推算的 `latencyMs`/`retries`,而是同一账本里**可独立复算**的执行事实(`store.workItemExecutionMetrics(itemId, settledAt)`,只读、单条 SELECT、复用 prepared statement):
|
|
415
|
+
|
|
416
|
+
- `latencyMs`(存在时总是非负):`settledAt − item.createdAt`,即从准入到该结算修订的墙钟时间;时间戳非安全整数时箝位为 0。item 行缺失时 `workItemExecutionMetrics` 返回 `undefined`,drain 路径回退到 `metricsFor(undefined)`(即 `{}`,**没有** `latencyMs`)。
|
|
417
|
+
- `claims`/`retries`:`claims` 统计结算上界内的 `claimed` 事件数(每次成功 claim 在同一事务里恰好追加一条事件并原子递增 `attempt`,所以它就是 `settledAt` 时刻的尝试数,不会像可变的 `item.attempt` 那样吸收结算之后的重试;`work_item_event` 以 `(item_id, sequence)` 为主键,可按前缀索引搜索,避免扫全部历史 claim 行);`retries = claims − 1`。`reports`/`waitingReports` 来自 `work_item_report`(按 `recorded_at`,其中 `waitingReports` 按 `status = 'waiting'`),`unknownDispatches`/`claimExpiries` 来自 `work_item_event` 的 `dispatch-unknown`/`claim-expired` 事件(按 `recorded_at`)。
|
|
418
|
+
- 所有计数都以 **`settledAt` 为上界**(`recorded_at/created_at <= settledAt`):结算之后才落库的行属于后续观察,不会被已结算修订吸收;同一 item 的新修订重新计算即可看到它们。边界是 1 ms 粒度、含等号的(`<=`),因此同一毫秒内、结算之后才提交的行仍会被计入;这里刻意不用严格 `<`,否则会把合法的同毫秒行漏掉。
|
|
419
|
+
- 零/缺失计数直接省略(不写 `0`),值域是无符号安全整数;这些指标不新增表或列。`workItemEvaluationClaims` 增加可选的第四个 `execution` 参数:drain 路径传入上面的只读计数包,直调三参数形式继续使用 live 记录回退,行为与旧版本一致(`{ latencyMs, retries? }`)。
|
|
420
|
+
- 刻意**不采集 provider token/成本指标**:本插件与 `assistant-automations` 之间没有 run-usage 契约,无法区分「本工作项消耗」与「同一 run 的其他开销」,复制过去的数字会把成本重复计入 Evaluation;需要成本归因时应由拥有 provider 账本的 producer 自己投影。执行指标只回答「做过多少事、等多久、重试几次」,不回答 objective 是否达成。
|
|
421
|
+
|
|
422
|
+
## 主人续办旧任务
|
|
423
|
+
|
|
424
|
+
显式配置 `ownerContinuation` 后,前台可先通过 `work_item_list` / `work_item_recall` 找回旧项,再调用 `work_item_continue({ item_id })`。新指令直接取自 Delivery 保存的当前真实人类消息,工具不接受模型改写的正文。它需要独立 `continue-work-items` Policy 授权;该授权的 automation 资源必须是下面的 continuation ID。原有后台 admit、Automations reconcile/执行预算和 Delivery send 授权也必须分别成立。
|
|
425
|
+
|
|
426
|
+
```yaml
|
|
427
|
+
ownerRecall: { enabled: true }
|
|
428
|
+
admission:
|
|
429
|
+
allowedKinds: [foreground-repair, owner-continuation]
|
|
430
|
+
ownerContinuation:
|
|
431
|
+
enabled: true
|
|
432
|
+
automationId: work-item-continuation # 必须不同于 workerAutomationId
|
|
433
|
+
allowedTools: [work_item_claim, work_item_read_context, work_item_report]
|
|
434
|
+
budgetId: owner-continuation-runs
|
|
435
|
+
budgetAmount: 1
|
|
436
|
+
maxModelCalls: 8
|
|
437
|
+
maxToolCalls: 12
|
|
438
|
+
maxOutputTokens: 2048
|
|
439
|
+
timeoutMs: 120000
|
|
440
|
+
expiresAt: 1893456000000 # 必须在 scope 有效期内
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
这三个工具是必需项;执行文件、网络等工作需要在 `allowedTools` 中另外显式列出相应 Host 工具,并保留其原有 Policy。不得加入主人查询/续办工具或只读 diagnosis 的 `work_item_read_source`。continuation 使用独立 system-owned Host definition,不扩大 diagnosis 的固定五工具能力。
|
|
444
|
+
|
|
445
|
+
父项必须已有终态报告,且 Automations 能证明该报告所绑定的精确 production occurrence 已完成、静止且预算已结算(或原执行明确无需预算)。旧版本没有静止证明的历史、仍运行的任务及 unknown 均不能据此重放。续办追加一个独立子项,保留父项和根项引用、报告/结果修订上界,冻结当前消息实际使用的 provider/model/effort、工具 schema、预算和原期限;不延长旧授权、不将 ready 当作独立成功。同一父项和真实输入来源重复调用只得到同一个子项。
|
|
446
|
+
|
|
447
|
+
worker 仍由原生 Automations/AgentLoop 执行。认领后,`work_item_read_context` 默认分页读取当前主人原文;`parent-brief`、`parent-report`、`parent-outcome` 按冻结上界读取历史,正文最多 8192 UTF-8 字节一页并附续读引用。输入全文最多 256 KiB,按原字节持久保存。`/new` 后同一 owner lineage 可以查找并续办历史,运行中的精确 binding/generation 变化则停止原执行。
|
|
448
|
+
|
|
449
|
+
每个子项最多执行一次;`waiting` 不产生自动重试。未知派发或未确认的执行占槽等待处置。新的待派发项使用已有 scheduler wake 有界检查,前一 occurrence 真正停止后才继续。报告后原生最终工具 guard 拒绝更多工具效果,仍可生成简短结束语。暂停、删除或更改已建立的 worker/配置不会自动重新启用。
|
|
450
|
+
|
|
451
|
+
### 回复报告后自动继续(显式启用)
|
|
452
|
+
|
|
453
|
+
在上述完整配置中另加 `ownerReplyContinuation: true`,主人可以直接回复已经发到渠道的工作项终态报告。Delivery 必须具有 `workItemReplyProtocolVersion() === 1` 的接口,并单独允许真实外部主人的 `continue-work-items-from-reply` 操作:
|
|
454
|
+
|
|
455
|
+
```yaml
|
|
456
|
+
- id: owner-report-reply
|
|
457
|
+
effect: allow
|
|
458
|
+
subject:
|
|
459
|
+
kind: external
|
|
460
|
+
id: lark/my-account/my-tenant/my-owner
|
|
461
|
+
workspace: /absolute/workspace
|
|
462
|
+
actions: [continue-work-items-from-reply]
|
|
463
|
+
resource: { kind: automation, id: work-item-continuation }
|
|
464
|
+
context: { initiators: [external] }
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
将主体和路径替换为现有 owner scope;原有后台准入、预算、执行和发送授权仍须成立。开关默认关闭,不能代替 `ownerContinuation` 的工具和有限预算配置。开启后,符合条件的纯文本回复可作为续办指令,包括简短回复。同时启用 `ownerReportFeedback` 时,受支持的完整结果短句及反馈命令先走下述独立反馈路径,不启动续办;这使用确定性的完整语句匹配,不调用模型分类。已识别的结果短句若缺少独立反馈授权,会保留普通前台处理,不降级为续办。“还是不行,继续修复”等含附加指令的回复仍按原有续办或前台规则处理。其他命令、附件、普通消息及不支持的报告仍走原有入口。
|
|
468
|
+
|
|
469
|
+
只有当前同一 owner 对真实 provider 已接受的报告进行精确消息回复,才会持久转交后台。模型给出的 item ID、普通消息 metadata 或尚未确认的发送不能建立这条关系。转交后不再启动普通前台执行;父执行尚未静止时,回复保留在队列。连续回复同一报告会依次接上前一个子项的报告;手动调用 `work_item_continue` 产生的分支有自己的回复序列。
|
|
470
|
+
|
|
471
|
+
这一入口没有前台模型调用,因此后续 worker 继承执行父项已冻结的 provider/model/effort,并冻结本次续办的显式工具范围、预算和期限。仅支持有冻结计划及精确执行证明的 diagnosis 或 continuation 父项;不会从当前 preset 推测旧模型。ACK 前不会派发子项,重复消息及重启恢复不会创建第二份执行;未知派发仍停驻。`/stop`、`/new`、`/clear` 和授权变化会阻止旧来源继续产生工具作用,不撤销已经发生的外部效果。
|
|
472
|
+
|
|
473
|
+
Delivery schema 29 保存真实报告关联、回复原文与回复序列;WorkItems schema 9 用 v2 计划保存来源和继承关系。备份时须一致保留两份账本;不能只复制其中一个库来恢复续办。既有 v1 前台续办计划保持原协议。已有 continuation 配置不变时,单独开关回复入口不重建 worker;若同时更改准入种类、工具或预算,则仍受原有激活合同约束。
|
|
474
|
+
|
|
475
|
+
此入口覆盖同一渠道的报告回复,不实现运行中指令插入、任意跨信道身份合并(仅有上文的 linked 别名只读查询)、多对多 Thread 订阅(目前仅有上文 `follows` 的单项跟随与 event-triggers 被动来源)或长期自主成长。Provider 接受报告不等于用户已读或任务已独立验收。
|
|
476
|
+
|
|
477
|
+
### 主人评价报告结果(显式启用)
|
|
478
|
+
|
|
479
|
+
`ownerReportFeedback: true` 将已认证主人对真实工作项报告的明确结果反馈写入 Evaluation 的 `work-item` 主人评价。默认关闭;要求已有 `ownerContinuation` 配置来核对封存计划,但不要求 `ownerReplyContinuation`。它使用独立的外部 Policy 操作 `feedback-work-items`,资源为配置中的 continuation automation;录入评价不认领、续办任务,也不消耗新的 worker 执行额度。
|
|
480
|
+
|
|
481
|
+
```yaml
|
|
482
|
+
- id: owner-work-item-feedback
|
|
483
|
+
effect: allow
|
|
484
|
+
subject:
|
|
485
|
+
kind: external
|
|
486
|
+
id: lark/my-account/my-tenant/my-owner
|
|
487
|
+
workspace: /absolute/workspace
|
|
488
|
+
actions: [feedback-work-items]
|
|
489
|
+
resource: { kind: automation, id: work-item-continuation }
|
|
490
|
+
context: { initiators: [external] }
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Delivery 在普通续办前识别既有的完整自然语言结果短句及其标点,以及 `/feedback achieved|partial|not-achieved`、`/feedback status`、`/feedback correct <version> <previous-status> <achieved|partial|not-achieved>`、`/feedback withdraw <version> <previous-status>` 指令,并按原有 CAS 规则保存、纠正或撤回同一工作项的一个当前评价。“还是不行,继续修复”含有附加指令,保留普通续办或前台处理,不截取其中的评价词。实际发送报告的 provider 消息关联、当前 owner、scope 和独立授权均须成立;普通 metadata 不构成证据。评价始终指向所回复报告的原工作项、修订与摘要,即使回复序列已有后续子项,也不会转评当前 stream head。已经接收的评价意图不会在重启或重试时变成前台请求或续办。
|
|
494
|
+
|
|
495
|
+
报告可以先发送;只有其精确 native production occurrence 真正终止、静止且预算 finalized 后,才可生成结果证明。证明保留实际执行状态,包括 failed、timed-out、cancelled,不能因为主人回复或报告 ready 就改成 succeeded。缺少证明时 Delivery 保留待处理评价。主人的成功评价仍是主人证词,不能充当独立验收、能力采用或长期成长收益。
|
|
496
|
+
|
|
497
|
+
Host 只读接口 `issueOwnerWorkItemFeedbackTarget({itemId, reportRevision, reportDigest})` 返回不可序列化的进程内 capability;`resolveOwnerWorkItemFeedbackTarget(capability)` 重新核对当前 owner、服务代次、完整报告/brief 字节、封存 supplier 和真实执行证明。错误或已被取代的报告修订、错误摘要、旧服务 capability、owner 更换与不确定预算均拒绝;不会悄悄读取另一条最新报告。相同主人新会话可以评价旧报告。task/report 内容仍是不可信材料,不是执行指令。
|
|
498
|
+
|
|
499
|
+
Host 批量来源接口 `withOwnerWorkItemFeedbackTargetsFence(capabilities, callback)` 接受最多 1000 个 capability,在 WorkItems 的 SQLite writer 事务内完成所有 owner/production 读取,再一次性重核整个组的 capability 归属、当前配置及服务、peer 身份和封存报告/计划。后面的来源回调不能使已经读过的 A 被缓存洗白。结果按输入位置返回深冻结的 proof 或 `undefined`;伪造、其他 reader 和已失效的 capability 不取得来源,相同 capability 对象只读取一次并保留重复位置,不同 capability 即使指向同一报告也各自校验。关闭或失去当前来源资源时整体拒绝。
|
|
500
|
+
|
|
501
|
+
该锁持有到同步 callback 结束;Promise/thenable 会被拒绝并回滚本地事务。组合使用的锁顺序是 Delivery → Evaluation → WorkItems → 可信终端账本,callback 只做既定同步核心读写,不反向获取上游锁或再调用来源 producer。它不启动 Agent、创建 timer、续执行授权或消耗模型/worker 额度,原有单条 issue/resolve 与历史 proof 字节保持不变。
|
|
502
|
+
|
|
503
|
+
这条历史读取与新执行授权分离。显式开启反馈后,冷启动可保留已经过期的 scope、diagnosis、continuation 原期限来读取已有结果;不会延长期限、重置预算、重新 reconcile worker 或放行旧执行。当前 Delivery owner route 与反馈 Policy 仍须有效。两个入口同开时,当前执行资格独立控制普通续办;过期后的明确评价仍可记录,普通回复不会因此取得过期执行权限。既有 v1/v2 计划和激活摘要保持兼容。
|
|
504
|
+
|
|
505
|
+
这一切片引入时 WorkItems 使用 schema 9(现为 schema 13,见下节 intake 账本);Delivery schema 30 保存独占评价意图,Evaluation schema 13 保存新的 canonical 工作项主人结果。原有 settlement 行继续作为 objective unknown 的审计材料,不回填为主人评价。备份与恢复须一致保留相关账本;这一切片不自动触发 Growth、创建/采用候选或调用模型。
|
|
506
|
+
|
|
507
|
+
## 确定性 intake 账本(显式启用)
|
|
508
|
+
|
|
509
|
+
WP20 第 1 阶段:Host 为每条入站条目按 `(source, sourceEventId)` 记录一次确定性决策,供后续评估切片使用。它只是审计账本,不准入工作项、不派发、不通知,也不调用模型。
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
const { record, replayed } = ctx.assistantWorkItems.recordIntake({
|
|
513
|
+
source: 'lark-channel', // 小写来源 ID
|
|
514
|
+
sourceEventId: 'om_xxx', // 来源内稳定事件 ID(≤256 字节)
|
|
515
|
+
kind: 'message', // 'message' | 'event'
|
|
516
|
+
senderAllowlisted: true,
|
|
517
|
+
addressed: false, // @提及、私聊或命令
|
|
518
|
+
threadFollowUp: false, // 已跟踪工作线程上的跟进
|
|
519
|
+
senderIsBot: false,
|
|
520
|
+
groupMessage: true, // 仅 message
|
|
521
|
+
// subscribed: false, // 仅 event,默认 true
|
|
522
|
+
contentDigest: '<sha256 hex>', // 可选,Host 自行计算
|
|
523
|
+
occurredAt: Date.now(),
|
|
524
|
+
})
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- 强度:名单同时限定寄址与跟进——白名单且寄址,或白名单在已跟踪线程上的跟进 → `explicit`;白名单未寄址 → `inferred`;非白名单的跟进 → `ignore`(`follow-up-not-allowlisted`);其他 → `ignore`(`not-allowlisted`)。bot 发送者最高 `inferred`(原因码 `bot-capped`)。
|
|
528
|
+
- 决策:`ignore` 为 `filtered`(`strength-ignore`);既未寄址、也不是白名单跟进的群消息为 `filtered`(`unaddressed-group-disabled`),未订阅事件为 `filtered`(`unsubscribed-event-disabled`),除非 owner 在 `intakeLedger.sources.<source>` 中开启对应项;同一来源中内容摘要已有强度不低于本条的 `stored` 行的新事件为 `duplicate`(`duplicate-content`),更强的重发(如补 @ 后再发)不会被较弱的原行压掉;其余为 `stored`,只记元数据。
|
|
529
|
+
- 重放:同一 `(source, sourceEventId)` 返回原记录与 `replayed: true`,不写第二行,重启后同样成立;元数据不同时额外返回 `metadataChanged: true`,原决策不变。
|
|
530
|
+
- 数据:输入字段封闭,`text`/`body` 等任何额外字段会被拒绝;行只含来源事件 ID、布尔信号、原因码、强度和摘要,SQL 触发器禁止改写或删除。`contentDigest` 是无盐 SHA-256,短文本可被猜测还原,按敏感数据对待。
|
|
531
|
+
- 要求 `enabled: true` 且 `intakeLedger.enabled: true`,否则返回 `disabled`。入站接线见下节。
|
|
532
|
+
|
|
533
|
+
### 入站接线(WP20 第 2 阶段)
|
|
534
|
+
|
|
535
|
+
启用 `intakeLedger` 时,WorkItems 向 `assistant-delivery` 注册唯一的 intake observer(冻结对象 `{ protocol: 1, observe }`,以当前 WorkItems 所有权栅栏校验,dispose 时注销)。入站 adapter(目前为 `lark-channel`)对每条已规范化、未过期、非问题/审批答复的消息调用 `context.observe(...)`,只传事件 ID、时间、发送者、会话类型、是否寄址、可选回复目标与正文 SHA-256;Delivery 据此计算:
|
|
536
|
+
|
|
537
|
+
- `source` = `<channel>-channel`,`sourceEventId` = `<account>/<eventId>`;
|
|
538
|
+
- `senderAllowlisted` = 发送者当前为已配对且未撤销的主人主体;
|
|
539
|
+
- `threadFollowUp` 只在白名单发送者于同一会话回复已投递(accepted/delivered/read)的 WorkItems 报告时为真;非白名单发送者的回复永远不是跟进;
|
|
540
|
+
- `groupMessage` 来自会话类型。
|
|
541
|
+
|
|
542
|
+
observe 只写 intake 账本:不写 Delivery inbox/outbox,不改变 adapter 原有的接收与门控,不准入、不派发、不通知;observer 异常或校验失败只记 debug 日志并丢弃。默认关闭时不注册 observer,不写任何行。
|
|
543
|
+
|
|
544
|
+
adapter 可另传规范化正文 `text`(≤65,536 UTF-16 码元);Delivery 仅在 observer 声明 `acceptsSourceText: true` 且 `sha256(text) === contentDigest` 时以 `sourceText` 转交,否则丢弃。正文只在内存中流转,Delivery 与账本都不落盘。
|
|
545
|
+
|
|
546
|
+
### inferred 评估运行时(WP20 第 3 阶段,显式启用)
|
|
547
|
+
|
|
548
|
+
`intakeLedger.evaluation.enabled: true` 时,observer 声明接收正文,WorkItems 把 `stored`+`inferred` 行的认证正文暂存内存(最多 256 条),Host 注册唯一评估器后按规则评估:
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
ctx.assistantWorkItems.registerIntakeEvaluator({
|
|
552
|
+
protocol: 1,
|
|
553
|
+
policy: () => ({ version: 'v1', rules: [{ ruleId: 'triage', ceiling: 'notify' }] }), // ≤16 条规则
|
|
554
|
+
evaluate: async ({ sourceText, signal }) => ({ level: 'digest', quotes: ['…'] }), // 注入的 supplier
|
|
555
|
+
})
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
- 驱动:每条入站 observe 都立即触发一次 `run()`;另复用已有 scheduler wake(不新增 Loop/计时器),也可由 Host 调 `runIntakeEvaluation()`。无论哪种触发,每轮最多 `maxPerRun` 次调用、滑动一小时最多 `maxCallsPerHour` 次的预算都生效,进行中的一轮不重入。
|
|
559
|
+
- Host 直接调 `recordIntake(input, sourceText)` 时同样校验:正文 ≤65,536 UTF-16 码元且 SHA-256 等于 `contentDigest`,否则 `invalid-input` 且不写账本。
|
|
560
|
+
- 结果写入 schema 11 的 `work_item_intake_evaluation`(STRICT,触发器禁止改写/删除),每 `(source, sourceEventId, ruleId, policyVersion)` 一行;只存等级、原因码与引文的 UTF-16 偏移 `quoteSpans`,不存正文或引文。重放与重启均不再调用模型,返回原行。
|
|
561
|
+
- 引文非认证正文子串 → `ignore`/`quote-not-in-source`;超时(`timeoutMs`)或抛错按退避重排,连续 `maxConsecutiveFailures` 次后记 `unavailable`;调用中 Policy 版本变化的结果丢弃,按新版本重新排队;Policy 无效时不评估,`lastError` 为 `policy-unavailable`;恢复可读后(即使版本未变)暂存的正文幂等补入队并清除该错误。
|
|
562
|
+
- 仅 observe:`effect` 恒为 `none`,评估本身不准入、不派发、不通知(准入见下节显式 `admit`)。`intakeEvaluations(source, eventId)` 读取记录,`intakeEvaluationHealth()` 报告队列与最近错误。
|
|
563
|
+
- Policy 版本即规则集身份:同一版本的规则与上限必须不变;同版本内改规则不会触发重排,新增规则的正文会一直占位直到版本前进。改规则请同时换版本。
|
|
564
|
+
- 落库失败(如 `SQLITE_BUSY`)的结果按条指数退避重试:第 n 次失败后隔 2^(n−1) 次 refresh(上限 64 次)再试,同一条的相同错误只记一次日志;失败后的存在性检查本身读失败时按未落库处理,不影响同轮其余结果。待重试条数见 `intakeEvaluationHealth().unpersisted`,落库后才可驱逐。注销评估器时进行中的一轮仍按快照落库;该轮写失败的结果直接丢弃,不在内存中保留引文。
|
|
565
|
+
- 暂存:只驱逐当前全部规则都已落库的最旧正文;已在队列中落定但尚未写盘的结果不算,避免丢失。注销评估器或 dispose 时清空暂存正文。
|
|
566
|
+
- 局限:重启后尚未落定的候选需要重新收到正文才能评估;暂存满且无可驱逐项时跳过新候选(计入 `skipped`);尚无内置 Policy 规则源,规则与上限由 Host supplier 提供。
|
|
567
|
+
- 升级:schema 10 自动升级到 11;更新的 schema 或结构漂移 fail closed。
|
|
568
|
+
|
|
569
|
+
### inferred 评估准入(WP21 第 3 片,显式启用)
|
|
570
|
+
|
|
571
|
+
`intakeLedger.evaluation.admit.enabled: true` 时,评估结果为 accept 的候选交给 `admitDecision`。只处理 `inferred` 候选;`addressed`/`subscribed` 不经此路径。未配置时行为不变。
|
|
572
|
+
|
|
573
|
+
- 驱动:只在已有 scheduler wake 上、评估一轮之后运行(不新增 Loop/计时器),也可由 Host 调 `runIntakeAdmission()`;每轮最多 `evaluation.maxPerRun` 条。
|
|
574
|
+
- `mode: observe`(默认):只记一行 `would-admit`,不建工作项;之后切到 `auto` 也不追溯准入这些行。
|
|
575
|
+
- `mode: auto`:要求 WorkItems `enabled` + `active`,`kind` 在 `allowedKinds` 内;以 `admitDecision` 准入 inferred mandate(仅元数据:标题 `Intake <source>:<eventId>`、规则/Policy 版本,不含正文或引文),发往配置的 `originBindingId`。decisionId 由 `(source, sourceEventId, ruleId, policyVersion)` 确定性派生,重放或重启返回原项,不开第二项。
|
|
576
|
+
- 每小时最多 `maxPerHour` 次准入;超出记 `refused`/`budget-exhausted`。`admitDecision` 拒绝(如 `policy-rule-mismatch`)或错误码(`kind-rejected`、`identity-mismatch`、`source-rejected`)记一行 `refused` 并带原因码,终态不重试;仅 `storage`/`disabled` 推迟到下次 wake,同错误只记一次日志。
|
|
577
|
+
- 结果写入 schema 12 的 `work_item_intake_admission`(STRICT,触发器禁止改写/删除),每个评估键一行;`intakeAdmissions(source, eventId)` 读取。Host-only 只读 `inspectIntakeProvenance(itemId)` 反查准入某工作项的 intake 判断(`work-items/intake-provenance/v1`:source/eventId、rule、policy 版本、mode、level、`wouldAccept`、准入时间,不含正文),并带该项 owner scope,供 Evaluation 的 intake 与注意力结果吻合度使用;`admitted` 与崩溃恢复的 `replayed` 都算 intake 准入;项不存在或非 intake 准入时返回 `undefined`,intake ledger 关闭时抛 `disabled`(不把"无法判断"当成"非 intake");schema 13 起由部分索引 `work_item_intake_admission_item_idx`(`item_id IS NOT NULL`)按 item_id 查找,不再全表扫描。
|
|
578
|
+
- 升级:schema 11 自动升级到 12(仅新增表、索引与触发器);更新的 schema 或结构漂移 fail closed。
|
|
579
|
+
- 升级:schema 12 自动升级到 13(仅新增准入表的 item_id 部分索引,不改写行);12 及以下已存在同名索引、13 时索引缺失或定义不符均 fail closed。
|
|
580
|
+
|
|
581
|
+
## 配置
|
|
582
|
+
|
|
583
|
+
| 字段 | 默认值 | 含义 |
|
|
584
|
+
| --- | --- | --- |
|
|
585
|
+
| `enabled` | `false` | 总开关;关闭时不创建任何目录、文件或数据库 |
|
|
586
|
+
| `mode` | `observe` | `observe` 只记录观察行;`active` 才唤醒 worker、发通知 |
|
|
587
|
+
| `ownerContinuation` | 关闭 | 单独启用、独立 automation ID、显式工具范围与有限执行预算;见主人续办旧任务 |
|
|
588
|
+
| `ownerReplyContinuation` | `false` | 将已授权主人的真实报告回复持久转交续办;要求已启用 ownerContinuation 和独立外部 Policy 授权 |
|
|
589
|
+
| `ownerReportFeedback` | `false` | 认证报告结果反馈;要求 ownerContinuation,独立 `feedback-work-items` 授权,不要求回复续办,不取得新执行额度 |
|
|
590
|
+
| `intakeLedger.enabled` | `false` | 显式启用确定性 intake 账本;仅 Host 调用 `recordIntake`,并经 Delivery 接收入站 adapter 的元数据/摘要观察,observe 可用,无外部副作用 |
|
|
591
|
+
| `intakeLedger.sources.<id>.unaddressedGroupMessages` | `false` | 该来源未寄址群消息以元数据进入账本(否则 filtered);最多 32 个来源 |
|
|
592
|
+
| `intakeLedger.sources.<id>.unsubscribedEvents` | `false` | 该来源未订阅事件以元数据进入账本(否则 filtered) |
|
|
593
|
+
| `intakeLedger.evaluation.enabled` | `false` | inferred 候选的模型评估(仅 observe,需 Host 注册评估器);关闭时不接收正文、不写评估行 |
|
|
594
|
+
| `intakeLedger.evaluation.timeoutMs` / `maxConsecutiveFailures` | `30000` / `3` | 单次调用超时;连续失败达到次数后记 `unavailable` |
|
|
595
|
+
| `intakeLedger.evaluation.maxPerRun` / `maxCallsPerHour` | `5` / `20` | 每轮与每小时的调用上限 |
|
|
596
|
+
| `intakeLedger.evaluation.admit.enabled` / `mode` | `false` / `observe` | 仅 inferred 候选:评估 accept 后 `observe` 记 would-admit,`auto` 经 `admitDecision` 准入(需 enabled active) |
|
|
597
|
+
| `intakeLedger.evaluation.admit.kind` / `originBindingId` | `intake-inferred` / 必填 | 准入 mandate 的 kind(auto 须在 `allowedKinds` 内)与报告发往的 origin binding |
|
|
598
|
+
| `intakeLedger.evaluation.admit.maxPerHour` | `5` | 滑动一小时准入上限(1–100) |
|
|
599
|
+
| `follows.eventTriggers.enabled` | `false` | 有 `eventTriggers` 服务时注册 `event-triggers` follow 来源(ref `<triggerId>@<afterSequence>@<configDigest>`,序号须等于当前全局 high water,摘要须等于该 trigger 当前配置);要求 enabled intake 与完整 scope |
|
|
600
|
+
| `follows.eventTriggers.acceptHeadRef` | `false` | 另接受 `<triggerId>@head`,准入时同步改写为当前 high water 与配置摘要的三段式 ref 后入库;仅在 `enabled` 时生效 |
|
|
601
|
+
| `acceptance.autoSettle.enabled` / `maxPerTick` | `false` / `5` | 在既有 scheduler wake 上自动读回已执行绑定项的 verifier 回执并记判定(每次 wake 至多 1–10 项,需 enabled active);见独立验收绑定 |
|
|
602
|
+
| `acceptance.verdictGate.enabled` | `false` | 让独立验收判定改变工作项状态:not-achieved/unknown/needs-attention 把已执行 `ready` 项转入 `blocked`(`acceptance-*` 原因),achieved 保持 `ready`;需 enabled active;见独立验收绑定 |
|
|
603
|
+
| `acceptance.bindWorkerRun.enabled` / `maxPerTick` | `false` / `5` | 在既有 scheduler wake 上把已 dispatch 的未绑定项晚绑定到其 worker run 冻结的 `automation-run` 合同(事件行,不进 mandate digest;每次 wake 至多 1–10 项,需 enabled active);见独立验收绑定 |
|
|
604
|
+
| `acceptance.holdReport.enabled` / `holdTimeoutMs` | `false` / `1800000` | 已绑定验收项的 `ready` 报告压到 verifier 结算后再发:achieved 正常发送,其他结算把项转 `blocked` 并改发 blocked 报告,超时(60s–24h)无判定则标记 unverified 释放;需 enabled active 无 diagnosis,并配合 autoSettle 与 bindWorkerRun;释放按 `(recorded_at, item id)` 内存游标轮转以防等待行饿死后续行,失去绑定的行按普通报告恢复发送;关闭零行为变化;见独立验收绑定 |
|
|
605
|
+
| `ownerRecall.enabled` | `false` | 显式启用当前主人前台任务查询;要求 enabled active、完整 scope、worker automation 与独立 `inspect-work-items` 授权 |
|
|
606
|
+
| `ownerRecall.linkedAliases` | 未设置 | 主人显式允许的 Delivery `linked` principal id 名单(规范 `channel/account/tenant/user` 形式,≤8,去重,不含 owner);未设置时跨渠道别名读取一律拒绝;要求 `ownerRecall.enabled`;别名读取另需带 `budget` 的 `inspect-work-items` 规则及 Policy `authorizeAgentForPrincipal`(按别名 principal 独立计费) |
|
|
607
|
+
| `ownerReportRouting.linkedFallback` | 未设置 | origin binding 不可用时可接收终端报告只读通知的 Delivery `linked` principal id 有序名单(规范形式,≤8,去重,不含 owner);要求 enabled active、完整 scope 与 worker;回复不能续办 |
|
|
608
|
+
| `ownerReportRouting.prefer` | `'origin'` | `'recent'` 时按主人最近入站的别名信道发送终端报告(需要 `linkedFallback`);origin 无更新入站或别名不合格时回落 origin;改道的报告走 background lane,失去 `ownerReplyContinuation`/`ownerReportFeedback` |
|
|
609
|
+
| `databasePath` | `$DSH_HOME/assistant-work-items/state.sqlite` | 私有账本绝对路径 |
|
|
610
|
+
| `workRoot` | `$DSH_HOME/assistant-work-items/work` | 每项 scratch/artifact 根目录绝对路径 |
|
|
611
|
+
| `intakeSourceId` | `work-items/v1` | 写入 automation 事件与 Policy background 主体标识 |
|
|
612
|
+
| `workerAutomationId` | 无 | active 模式必填;通用模式需预先存在,diagnosis 模式首次激活按此 ID 建立固定 system-owned Host worker |
|
|
613
|
+
| `scope.workspace` / `scope.preset` / `scope.principalId` / `scope.ownerRouteId` | 无 | owner 五元组;active 模式必填,过期后 fail closed |
|
|
614
|
+
| `scope.expiresAt` | 无 | epoch 毫秒;执行要求未来,显式 ownerReportFeedback 可保留过期原值读取历史,不放行执行 |
|
|
615
|
+
| `admission.allowedKinds` | `[]` | 允许准入的 kind;active 模式至少一项 |
|
|
616
|
+
| `admission.maxOpenItems` | `8` | 未结工作项上限(1–100) |
|
|
617
|
+
| `admission.maxAdmitsPerHour` | `4` | 每小时准入上限(1–100) |
|
|
618
|
+
| `admission.dedupWindowMs` | `604800000` | 去重窗口(1 小时–30 天) |
|
|
619
|
+
| `admission.cooldownMs` | `600000` | 两次准入之间的最小间隔(0–1 天) |
|
|
620
|
+
| `admission.maxBriefBytes` | `8192` | brief UTF-8 上限(256 B–64 KiB) |
|
|
621
|
+
| `limits.leaseMs` | `900000` | claim 租约(30 秒–1 天) |
|
|
622
|
+
| `limits.maxAttempts` | `3` | 单工作项最大执行尝试(1–10) |
|
|
623
|
+
| `limits.maxReportsPerItem` | `20` | 每项最大报告修订(1–100) |
|
|
624
|
+
| `limits.maxReportBytes` | `16384` | 单条报告 summary UTF-8 上限(256 B–64 KiB) |
|
|
625
|
+
| `limits.waitingResumeMs` | `300000` | `waiting` 报告后延迟重新唤醒 worker 的时间(60 秒–1 小时) |
|
|
626
|
+
| `delivery.notifyOn` | `[ready, blocked, failed]` | 触发 owner 通知的终态报告状态 |
|
|
627
|
+
| `sources.foregroundFailures.enabled` | `false` | 是否武装内置的 owner foreground 失败来源;关闭时扫描是 no-op |
|
|
628
|
+
| `sources.foregroundFailures.kind` | `foreground-repair` | 该来源准入的工作项 kind;active 模式必须在 `admission.allowedKinds` 内 |
|
|
629
|
+
| `sources.foregroundFailures.includeTimedOut` | `true` | 通用模式接收 `timed-out`;diagnosis 模式允许已 disposed 的 `configured-timeout` 诊断候选 |
|
|
630
|
+
| `sources.foregroundFailures.maxCandidatesPerSweep` | `10` | 单次扫描最多读取的候选数(1–100) |
|
|
631
|
+
| `sources.foregroundFailures.maxAdmitsPerSweep` | `2` | 单次扫描最多准入数量(1–10) |
|
|
632
|
+
| `sources.foregroundFailures.sweepIntervalMs` | `3600000` | 两次自动扫描的最小间隔(60 秒–1 天,持久化门控) |
|
|
633
|
+
| `diagnosis.enabled` | `false` | 显式启用只读诊断;要求 enabled active、完整 scope 和启用的内置前台失败来源 |
|
|
634
|
+
| `diagnosis.budgetId` | 无 | 启用时必填;独立批准的累计 Policy 预算 ID,最多 200 字节 |
|
|
635
|
+
| `diagnosis.budgetAmount` | 无 | 启用时必填;每次诊断 reserve 数量(整数 1–10000000),不推断原任务剩余额度 |
|
|
636
|
+
| `diagnosis.maxModelCalls` | 无 | 启用时必填;每次执行模型调用上限(整数 1–100) |
|
|
637
|
+
| `diagnosis.maxToolCalls` | 无 | 启用时必填;每次执行工具调用上限(整数 1–1000) |
|
|
638
|
+
| `diagnosis.maxOutputTokens` | 无 | 启用时必填;每次执行输出 token 上限(整数 1–131072) |
|
|
639
|
+
| `diagnosis.timeoutMs` | 无 | 启用时必填;固定 Host 执行时限(整数 1000–300000 毫秒) |
|
|
640
|
+
| `diagnosis.expiresAt` | 无 | 启用时必填;未来的安全整数 epoch 毫秒,不晚于 scope.expiresAt;重启不续期 |
|
|
641
|
+
| `settlement.enabled` | `true` | 是否在终态报告后维护独立结果证据窗 |
|
|
642
|
+
| `settlement.windowMs` | `604800000` | 证据窗长度(1 小时–9 天) |
|
|
643
|
+
| `settlement.sweepIntervalMs` | `600000` | 两次结算扫描的最小间隔(60 秒–1 天,持久化门控,无新 timer) |
|
|
644
|
+
| `settlement.maxSourcesPerSweep` | `25` | 单次来源分页读取的页大小(每页条数,作为 `limit`,1–100) |
|
|
645
|
+
| `settlement.maxPendingPerSweep` | `10` | 单次最多检查的 pending 窗口;按 `deadlineAt` 升序,超出部分顺延到后续 pass(1–50) |
|
|
646
|
+
| `settlement.maxSettlementsPerSweep` | `10` | 单次最多追加的结算修订(1–50) |
|
|
647
|
+
|
|
648
|
+
所有可变部署值都经过同步 Schemastery 校验;未知字段、相对路径、文件系统根目录、`databasePath` 与 `workRoot` 重叠、过期 scope 都会失败关闭。
|
|
649
|
+
|
|
650
|
+
## 权限与数据
|
|
651
|
+
|
|
652
|
+
本插件直接文件权限限于显式 `databasePath` 与 `workRoot`。通用模式跨插件复用 Delivery owner route/execution 查询、受 Policy 约束的 automation dispatch 和终态通知,以及可选 Evaluation 证据 registration。diagnosis 模式还会注册私有 Host executor、首次 reconcile 固定 system-owned definition,并通过 Automations 的带授权 fence 原生 Agent 执行入口运行诊断;每一步受前节的 owner、Policy、冻结合同和有限预算约束。
|
|
653
|
+
|
|
654
|
+
- 文件系统:只读写显式 `databasePath` 和 `workRoot`;目录 `0700`,数据库文件 `0600`,拒绝 leaf symlink。artifact 只能落在工作项自己的 scratch 目录内。
|
|
655
|
+
- 网络:插件自身不发网络请求。模型/工具调用发生在 `assistant-automations` 创建的原生 Agent run 内,其 provider 与工具权限由宿主和 Policy 决定。
|
|
656
|
+
- 子进程、凭据、浏览器:插件自身不直接使用;continuation 显式允许的 Host 工具可能使用,仍受这些工具的声明与 Policy 约束。
|
|
657
|
+
- 安装脚本:包内没有 postinstall 或其他生命周期脚本;`prepublishOnly` 只要求用 pnpm 发布。
|
|
658
|
+
- 消息投递:只有 active 模式的终态报告才会经 `assistant-delivery` 入队;observe 模式从不调用 Delivery 的写接口。Delivery 会在入队和发送前重查 owner route、binding 版本/代次、有效期和 background `send` Policy。
|
|
659
|
+
- 跨插件读取:通用自动来源使用 `assistant-delivery` 的 `validateOwnerRoute` + `listOwnerForegroundTaskSources`(owner 认证、游标分页、host 公开、只读,不触发任何发送或写操作)。`assistant-delivery` 缺失或版本过旧(无该 API)时,来源退化为 no-op 或 `degraded: 'unsupported'`,通用来源扫描按此方式降级。diagnosis 模式必须具备现代 Delivery 的逐条 inspect/有界原输入读取及带 fence 的 Automations 能力;缺失、过旧或授权失效时不激活/不执行,health 显示 unavailable 或待处置,工具动作可失败关闭,不承诺 claim 成功。
|
|
660
|
+
- 结果结算:只读取本地账本和 `assistant-delivery` 的只读 `listOwnerForegroundTaskSources`;不新增网络、子进程、凭据或写接口,结算失败/缺依赖时降级而不是重试外部写入。可选 `assistant-evaluation` 只通过进程内 registration 追加 trusted 证据,同样不发网络请求。
|
|
661
|
+
- 数据保留:continuation schema 9 另外保存当前人类输入全文、父/根引用和不可变执行计划;原文可能包含敏感信息。账本保存外部 ID、标题、brief、owner 身份、报告正文和 artifact 路径;diagnosis 还保存冻结供应商、owner/source/binding、完整工具 schema、预算和激活合同。活动 claim 可将有界原输入读入原生 Agent 上下文/会话记录,报告也可能包含原文片段,均可能包含私人信息;备份时按 SQLite(含 WAL)一致性方式复制。删除 scratch 目录不影响账本,但会丢失完整 artifact。
|
|
662
|
+
|
|
663
|
+
## 限制
|
|
664
|
+
|
|
665
|
+
- 单独使用 observe 模式没有产品价值:它只积累观察行,不唤醒、不通知。observe 仍要求 `assistant-delivery` 可用,因为准入要先校验 owner route;没有 Delivery 时准入记录不会写入(失败关闭),这是当前实现的已知限制。
|
|
666
|
+
- 尚未**随包发布**仓库外真实来源(GitHub、邮件、IM、任务日历等):插件提供了 `registerSignalSource(...)` 扩展点,但具体来源适配器仍待独立插件实现;内置来源只是 `assistant-delivery` 已认证的 owner foreground 记录,仍属于 in-tree 证据。
|
|
667
|
+
- 通用内置来源的 title/brief 由 Delivery 行的**元数据**派生(inbox、binding、版本/代次、完成时间),不读取 owner 原文;通用 worker 依据 brief、provenance 和自身可用工具定位上下文。diagnosis 的 brief 同样不复述原文,但本人活动 claim 可通过固定 `work_item_read_source` 逐页读取原请求;每次读取重验当前 owner、来源摘要和授权,原文仍是不可信数据。
|
|
668
|
+
- canonical key 以**单次 execution**(inbox + binding 版本/代次 + executionRef,不含 `disposition`)为单位,不是「同一意图第 N 次重复」;同一 execution 的状态修订(失败→撤回)共享同一身份,因此撤回能定位并退役已准入的项。owner 同一意图重试失败会产生新的工作项(cooldown 与 `maxOpenItems` 限制总量)。
|
|
669
|
+
- 已准入信号的 `disposition` 若被修正为成功(`succeeded`,即不再产生候选),不会自动关闭旧工作项;只有 `cancelled` 撤回会传播到在途项。撤回报告已经交给 delivery 的终态报告无法撤回,worker 只能从后续工具调用看到 `dropped`。observe 模式不会新建在途项,但若切换前遗留的在途项仍在,撤回仍会把它退役(内部账本变化,不唤醒、不通知)。
|
|
670
|
+
- 升级注意:本版本把 `disposition` 从 canonical key 移除。升级前已准入的旧工作项仍带旧 key,之后的撤回对它们会解析为 `absent`,只能等其自然结项;新信号不受影响。
|
|
671
|
+
- 观察期记录会占用去重窗口:同一 signal 从 observe 切到 active 后,窗口内仍视为已准入;需要时等待窗口过期或换 `externalId`。
|
|
672
|
+
- 工作项终态只是运维证据,不代表任务质量。`ready` 仅是 worker 自报,结果结算也只给出 `unknown/observed/unverified`;导出到 Evaluation 的是同一份证据,不证明修复质量,也不构成采用依据。
|
|
673
|
+
- `stats().openItems` 与 `health().openItems` 只统计 `queued/claimed/working/waiting`,不含仍需人处理的 `ready/blocked` 终态项。
|
|
674
|
+
- 精确定时仍属于 `assistant-automations`;本插件不增加 timer 或 retry daemon。diagnosis 只注册 external schedule 的固定 Host definition,扫描借用既有原生 scheduler wake。
|
|
675
|
+
|
|
676
|
+
## 兼容性
|
|
677
|
+
|
|
678
|
+
以 DeepSeek Harness `0.1.2-rc.1` 的服务与工具契约验证。依赖 `node:sqlite`(Node.js 22+ 内置),不兼容 Node.js 20 及更早版本。
|
|
679
|
+
|
|
680
|
+
### Host-only engineering source context
|
|
681
|
+
|
|
682
|
+
`issueOwnerWorkItemEngineeringTarget(feedbackCapability)`, `resolveOwnerWorkItemEngineeringTarget(capability)` and `withOwnerWorkItemEngineeringTargetsFence(capabilities, callback)` expose the separate `assistant-work-items/engineering-material/v1` historical source protocol. The opaque capability binds an existing feedback proof and the exact material digest to the current service/store/configuration. It grants no execution, model, budget, filesystem, network or installation authority. Existing feedback and Memory protocols are unchanged.
|
|
683
|
+
|
|
684
|
+
The material follows a sealed diagnosis root through at most 32 parent links, preserving each v2 reply's exact UTF-8 input (including decomposed Unicode and NUL), root Inbox locator and fixed parent report/outcome cutoffs. Reports are background only; null cutoffs stay empty, and later reports never replace the selected revision. Missing ancestors, changed selected text/plans, owner or binding lineage mismatch, cycles and oversize material abstain. Original input must be nonblank, attachment-free text: Delivery verifies the actual root/reply Inbox envelopes. Historical v1 continuation plans lack a complete original input/attachment locator, so any v1 child makes this new protocol abstain; legacy continuation execution and Memory remain supported.
|
|
685
|
+
|
|
686
|
+
Continuation input text totals at most 16,384 bytes; each selected report is at most 65,536 bytes, all report text at most 262,144 bytes, and complete canonical material at most 524,288 bytes. Limits reject rather than truncate. Delivery separately bounds the framed objective. A batch accepts at most 1,000 aligned capabilities; unknown/stale slots return `undefined`. The synchronous fence reuses one feedback source writer transaction, completes all foreign owner/production reads before locally rechecking every chain, and holds the source lock through the consumer. Thenables and consumer errors roll back. Closed/replaced service generations fail closed. Historical reads do not renew expired execution grants or start automations. This protocol supplies evidence for downstream Growth; it does not by itself establish successful engineering adoption or improvement.
|