@sema-agent/server 7.116.1 → 7.118.0
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/MIGRATION.md +195 -0
- package/USAGE.md +65 -41
- package/dist/approval-card.d.ts +82 -18
- package/dist/approval-card.js +46 -4
- package/dist/approval.d.ts +4 -12
- package/dist/approval.js +4 -26
- package/dist/boot/budget-tracing.js +2 -3
- package/dist/boot/config-center.d.ts +2 -2
- package/dist/boot/config-center.js +5 -7
- package/dist/boot/parked-revive-gate.d.ts +1 -7
- package/dist/boot/parked-revive-gate.js +2 -12
- package/dist/boot/resolve-spec.js +18 -16
- package/dist/boot/runner-deps.d.ts +5 -3
- package/dist/boot/runner-deps.js +6 -4
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/brain.d.ts +17 -8
- package/dist/brain.js +10 -7
- package/dist/capabilities/hands-lane.d.ts +25 -0
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +2 -2
- package/dist/config-catalog.js +3 -2
- package/dist/config-center/apply-effective.d.ts +6 -16
- package/dist/config-center/apply-effective.js +0 -6
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-types.d.ts +12 -16
- package/dist/config.d.ts +32 -29
- package/dist/config.js +20 -27
- package/dist/deployment-governance.d.ts +13 -19
- package/dist/deployment-governance.js +7 -14
- package/dist/governance-overrides.d.ts +20 -0
- package/dist/governance-overrides.js +8 -0
- package/dist/host-decision.d.ts +83 -25
- package/dist/host-decision.js +31 -1
- package/dist/http/admission.js +13 -7
- package/dist/http/approval-sweeps.js +16 -13
- package/dist/http/checkpoint-ctx.d.ts +83 -24
- package/dist/http/checkpoint-ctx.js +68 -8
- package/dist/http/mcp-dial-deadline.d.ts +35 -0
- package/dist/http/mcp-dial-deadline.js +21 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
- package/dist/http/memory-compliance-failure-reply.js +17 -8
- package/dist/http/memory-store-refusal-reply.d.ts +14 -11
- package/dist/http/memory-store-refusal-reply.js +16 -5
- package/dist/http/resume-legs.js +18 -18
- package/dist/http/retired-skill-manifest.d.ts +1 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/routes/approvals-assistant.js +21 -24
- package/dist/http/routes/capabilities.js +5 -3
- package/dist/http/routes/mcp-reconnect.d.ts +17 -0
- package/dist/http/routes/mcp-reconnect.js +6 -1
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/runs.js +53 -33
- package/dist/http/routes/sessions.d.ts +1 -1
- package/dist/http/routes/sessions.js +3 -6
- package/dist/http/routes/workflows.js +2 -0
- package/dist/http/wire-types.d.ts +6 -1
- package/dist/memory-store-refusal.d.ts +42 -17
- package/dist/memory-store-refusal.js +25 -6
- package/dist/model-select.d.ts +16 -5
- package/dist/model-select.js +7 -3
- package/dist/model-unreachable-notice.d.ts +1 -1
- package/dist/namespaced-rule-name.d.ts +45 -39
- package/dist/namespaced-rule-name.js +36 -22
- package/dist/observability/corrupt-read-seat.d.ts +4 -0
- package/dist/observability/corrupt-read-seat.js +1 -0
- package/dist/observability/fail-open.d.ts +15 -3
- package/dist/observability/fail-open.js +15 -3
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/observability/run-terminal.js +21 -1
- package/dist/operator-ask.d.ts +4 -1
- package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
- package/dist/orchestration/workflow-agent-session-index.js +80 -14
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
- package/dist/orchestration/workflow-completion-inbox.js +39 -17
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
- package/dist/orchestration/workflow-notify-journal.js +7 -2
- package/dist/outbound-dispatcher.d.ts +47 -20
- package/dist/outbound-dispatcher.js +198 -23
- package/dist/parked-decide.d.ts +43 -48
- package/dist/parked-decide.js +54 -16
- package/dist/permission-rule-list.d.ts +20 -31
- package/dist/permission-rule-vocab.d.ts +4 -3
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
- package/dist/plugins/approval-ask-store-sql.js +0 -3
- package/dist/plugins/one-time-migrations.d.ts +25 -0
- package/dist/plugins/one-time-migrations.js +192 -6
- package/dist/plugins/pg-pool.js +1 -1
- package/dist/plugins/tidb-pool.js +1 -1
- package/dist/plugins/web-search.d.ts +20 -1
- package/dist/plugins/web-search.js +39 -10
- package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
- package/dist/plugins/workflow-run-store-sql.js +5 -7
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +1 -1
- package/dist/router/route-orchestration.d.ts +2 -23
- package/dist/rule-entry-list.d.ts +88 -0
- package/dist/rule-entry-list.js +102 -0
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.d.ts +4 -2
- package/dist/run-local.js +4 -3
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +1 -0
- package/dist/runtime-governance.d.ts +5 -21
- package/dist/runtime-governance.js +4 -8
- package/dist/session-policy-wire.d.ts +13 -12
- package/dist/session-policy-wire.js +11 -6
- package/dist/spec-fields.d.ts +13 -24
- package/dist/spec-fields.js +13 -17
- package/dist/standing-answer-grant.d.ts +14 -2
- package/dist/standing-answer-grant.js +8 -1
- package/dist/task-settings.d.ts +18 -18
- package/dist/task-settings.js +63 -32
- package/dist/tool-approval.d.ts +77 -145
- package/dist/tool-approval.js +80 -62
- package/dist/trace/core-keyset-guard.d.ts +6 -6
- package/dist/trace/engine-notice-wire.d.ts +4 -6
- package/dist/trace/engine-notice-wire.js +14 -1
- package/dist/trace/self-minted-wire.d.ts +16 -2
- package/dist/trace/self-minted-wire.js +7 -0
- package/dist/user-input-ledger.d.ts +24 -12
- package/dist/user-input-ledger.js +33 -14
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +103 -7
- package/scripts/drain-check.mysql.sql +59 -8
- package/scripts/drain-check.pg.sql +66 -10
|
@@ -18,7 +18,11 @@ export const FAIL_OPEN_TAGS = {
|
|
|
18
18
|
},
|
|
19
19
|
"server.workflow-park-index.sync-failed": {
|
|
20
20
|
cls: "P-DEBT",
|
|
21
|
-
note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join
|
|
21
|
+
note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join 不到。**多数情形**是诚实 miss:这张卡没有别的指针 ⇒ `/decide` 结构性退回本批之前的行为(409 `conflict.resume_context_unavailable`),不误配、不伪造、不放行,丢的是「人能不能把这条批准送出去」这个能力。**7.118.0 起不止于此**:路由把指针候选当持有者的全集读,并有分叉门(两条同代次持有者 ⇒ 409 `decide.workflow_park_forked`);同一张卡上一枚**竞争**指针没写上(本臂的写失败,或写记录与写指针之间进程崩溃 —— 后者不经本臂、不计数)⇒ 分叉被判成唯一持有者 ⇒ 决定投给幸存的那一支(它确实持有这张卡,但另一支同样站在上面,引擎把这种形交给消费方拒)。归 P 类的理由:缺指针决定**车道选择**(缺行 ⇒ 选中 legacy 腿),且能让分叉门失效;归 DEBT 的理由是真正的解要一条**按子代 checkpoint 键的耐久决议回执 + 重启期回填**(与 codex r1-F3 同一件欠账,另车),分叉门失效那一形另有设计票。计数非零 = 有 park 可能决不掉、或这张卡的分叉门可能失效,运维要按 detail 里的 runId 去查那条 run 的续跑链。",
|
|
22
|
+
},
|
|
23
|
+
"server.workflow-park-index.parks-unreadable": {
|
|
24
|
+
cls: "F",
|
|
25
|
+
note: "S-1173(`workflow-notify-journal.ts` 的 `syncAgentSessionIndex`,与上一条 `sync-failed` 同一个写点、另一臂):workflow run 记录的 `parks[]` **读不出**(在场却不是列表 / 某条目身份位或持有者两位形坏;读者 = `workflow-agent-session-index.ts` 的 `readRunParks`,整档成败制)⇒ 这一份指针投影派生不出来 ⇒ 写者**不动**这条 run 已有的指针(上一份已知良好的投影留着)。修前这一臂静默 `return`。**`parks` 缺席不进这一臂**:那是引擎契约里的合法形(更早引擎写的记录 / 准入没走完的续跑记录),读者答 `[]`(没有条目),写者照常收敛成空指针集,不计。**与 `sync-failed` 分家的理由(后果不同句,不并成一个 tag)**:`sync-failed` 是索引**写**挂了、丢的是一份已知良好的投影(park 常常是 run 的最后一次写 ⇒ 永久 join 不到,归 P-DEBT);本臂没有丢任何一份已知良好的投影 —— 读不出的是**记录本身**,引擎对条目形坏的记录整条拒续跑(`park_binding_broken`),读口对已有指针的 run 据记录响亮拒(`unproven/unreadable`),所以无需「重试这次写」,要的是查那条记录为什么读不出(投影把它写坏 / 手改)。放行的最坏后果:这条 run 的指针不被刷新(从没写过则没有指针 ⇒ 这张卡没有别的指针时 `/decide` 落 legacy 腿、409 响亮拒),多数情形方向是 fail-closed,不误配、不放行任何东西;例外与上一条同形 —— 首写就读不出的这一支没有指针,同一张卡上另一支的指针在时分叉门判不出、决定投给那一支(引擎从不写坏形,要这一支在首写之前就被写坏,比上一条的写失败更罕见);derived cache 没更新、真源读口自带响亮拒 ⇒ F 类。detail = `runId=`;计数非零 = 有记录的 `parks[]` 读不出,按 runId 去查记录。",
|
|
22
26
|
},
|
|
23
27
|
"server.trace.gate-outcome-defective": {
|
|
24
28
|
cls: "F",
|
|
@@ -110,7 +114,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
110
114
|
},
|
|
111
115
|
"server.approvals.risk-descriptor-cell-unreadable": {
|
|
112
116
|
cls: "F",
|
|
113
|
-
note: "运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint.risk_descriptor` 这一格**不是合法 JSON 文本**(手改过的行 / 回滚残留 / 列语义漂;与同一 `rows.map` 里的 `rule_suggestions` 是同族坏法,两方言都是 TEXT 列)。放行的最坏后果 = **这一行**的分诊 descriptor 落 null:severity 折 0 排到有 descriptor
|
|
117
|
+
note: "运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint.risk_descriptor` 这一格**不是合法 JSON 文本**(手改过的行 / 回滚残留 / 列语义漂;与同一 `rows.map` 里的 `rule_suggestions` 是同族坏法,两方言都是 TEXT 列)。放行的最坏后果 = **这一行**的分诊 descriptor 落 null:severity 折 0 排到有 descriptor 的行之后、披露位(`allowRuleShadowed`;7.117.0 前叫 `shadowedRule`)无可脱敏、读面派生的 `governanceForced` 徽章不亮 —— 三者都是**展示/分诊**,不是执法判据(门早已 park,真按 `shellGateDoctrine` 判的 `active-run-conflict.ts` 读的是 checkpoint blob 的 `get()`,不经本读面),且 `governanceForced` 的成文语义本就是「缺席 = 没有治理来源的**证据**」。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 cell 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),连同其余所有行一起消失。故 F 类;tag 与候选那格**分开计**,并计会让「哪一格在坏」在遥测里读不出来。",
|
|
114
118
|
},
|
|
115
119
|
"server.approvals.park-row-blob-unreadable": {
|
|
116
120
|
cls: "F",
|
|
@@ -269,7 +273,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
269
273
|
"server.redact.ppk-eat-to-end": { cls: "F", note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。" },
|
|
270
274
|
"server.approval-card.closed-word-out-of-set": {
|
|
271
275
|
cls: "F",
|
|
272
|
-
note: "S-249(`approval-card.ts` 的边界窄读族;今天三个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,判法 = core 屏 `screenAskClassMembers`(S-955 ⑥ / core 7.38.0 #1249,本仓零自写对判),两键同在同缺,半对 / 集外 / 首员不符 / 重复员 = 屏拒 = 读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。",
|
|
276
|
+
note: "S-249(`approval-card.ts` 的边界窄读族;今天三个成员:`readRuleOffersAbsence`、S-934 ② 起的 `readAskClassPair`(core 7.37.0 #1214 的类对 `askClass` / `classes`,活卡帧 / 呈卡帧的卡 / 运维队列行同一只,判法 = core 屏 `screenAskClassMembers`(S-955 ⑥ / core 7.38.0 #1249,本仓零自写对判),两键同在同缺,半对 / 集外 / 首员不符 / 重复员 = 屏拒 = 读不出;detail 前缀 `askClass=` / `classes=`),与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛)、S-1170 起的 `classifierNoRuling`(core 7.43.0 #1353 的成因词,活卡帧读 `AskRequest.classifierNoRuling`,闭集筛 = core `isAutoModeFallbackCause`)与 `differs`(core 7.44.0 #1374,载体是 `runs.ts` 的 409 `steering.duplicate_input_id` 体读的引擎拒错 `detail.differs`,闭集筛 = core `STEER_REPLAY_FACETS` 的非空子集;集外成员 / 空 / 非数组 ⇒ 不铸 `detail`,码仍是契约);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` / `classifierNoRuling=` / `differs=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。",
|
|
273
277
|
},
|
|
274
278
|
"server.pg-pool.idle-client-error": {
|
|
275
279
|
cls: "F",
|
|
@@ -307,6 +311,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
307
311
|
cls: "F",
|
|
308
312
|
note: "S-977(`orchestration/workflow-agent-session-index.ts`,local 车道 workflow 出身 park 的 join 索引账本的 boot 重放;登记名 `workflow-agent-session-index`):账本**中间**一行读不出形(`42` / 数组 / 缺 `runId`·`rows` / 任一行成员缺身份位 / 半截 JSON;撕尾不走本 tag)⇒ **整行**跳过,那条 run 保留上一份已知良好的快照(不做「丢坏成员、留好成员」—— 那会铸出比真相少几条 park 的行集)。最坏后果 = 该 run 的 park 在索引里缺行 ⇒ `/decide` 找不到它、退回 legacy 腿(409 `conflict.resume_context_unavailable`):诚实 miss,不是错配 —— 赎回键由记录上的 `parks[]` 逐字比对,不读本索引。索引不开门 ⇒ F。修前:形不对的整行响亮(`workflow_agent_session_ledger_line_rejected`,同事件名保留)但**无 tag**,带换行的半截 JSON 静默 `continue`。",
|
|
309
313
|
},
|
|
314
|
+
"server.workflow-completion-inbox.corrupt-record-skipped": {
|
|
315
|
+
cls: "F",
|
|
316
|
+
note: "S-978(`orchestration/workflow-completion-inbox.ts`,local 车道 File 完成信箱的 boot 重放;登记名 `workflow-completion-inbox`):账本**中间**一行读不出形(`42` / `null` / 数组 / 缺 `op` 或 `op` 词表外 / `enqueue` 缺 `entry.sessionId`·`entry.runId` / `ack` 缺 `sessionId`·`runId` / `drain` 缺 `sessionId` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。最坏后果按行种:丢 `enqueue` ⇒ 那条完成推送不进信箱(`WorkflowStatus` 轮询地板照旧兜住);丢 `ack` ⇒ 一条已送达的完成在重启后再投一次(壳按 runId 去重,至少一次的既有语义);丢 `drain`(会话清除墓碑)⇒ 被删会话里尚未送达的条目复活 —— 出帧前仍过 owner 二道闸(`emitPendingWorkflowCompletions`:条目盖的 owner 与本次调用方不符 ⇒ ack 丢弃、不出帧),同属主重领同一 sessionId 只会多收一条按 runId 去重的推送。信箱不开门、不改任何 run 的执行或授权 ⇒ F。修前:解析失败 `continue`(静默),而解析成功的 `null` 行在读 `rec.op` 时抛 TypeError ⇒ `new FileWorkflowCompletionInbox` 抛(构造点无 catch ⇒ 这一面起不来),其余非对象 / 缺键形静默忽略。boot 压实随后从内存活集重写账本 ⇒ 坏行的字节压实后不留盘,披露是它唯一的记录。",
|
|
317
|
+
},
|
|
310
318
|
"server.user-input-ledger.append-failed": {
|
|
311
319
|
cls: "F",
|
|
312
320
|
note: "S-928(`src/user-input-ledger.ts`):200 承诺**之后**的一次账本写失败 —— detail 的 `op=` 点名是哪一种:`settle`(腿在本进程收场)/ `withdraw`(引擎没收下这次 steer)/ `converge`(收敛器判孤儿)/ `announce`(通告已投出,记「告知过」)。放行的最坏后果逐个说清:settle / withdraw 漏写 ⇒ 这次受理在账上仍是 open —— 本进程下一次收敛扫描补结清(本进程写的、已不在活腿上 ⇒ leg_over);进程在那之前就崩了 ⇒ 下一次 boot 的收敛读 run 店判(S-969 L8 起两车道同一只判词:承载它的 run 正常翻面 / 已 park ⇒ leg_over 补结清、不告知;落款死亡判决或写者已死仍 running ⇒ crashed)—— 仍会多说一句的只剩两形:withdraw 漏写后那条腿崩了(一次没变成 200 的受理被告知一次);settle 漏写后本进程续跑了同一条 run、续跑腿崩了(run 行说的是同一 taskId 上**最新**那条腿)。方向都是多说,不漏;converge 漏写 ⇒ 留在 open,下一次扫描再判(延迟,不丢);announce 漏写 ⇒ 下一条 run 再说一遍(至多一次退化成至少一次)。都是**告知面**的偏差,不开任何门、不改任何 run 的执行 ⇒ F。计数非零 = 这台机器的账本写不动了(local:数据根盘满 / 只读;SQL:库不可达),steer 此刻多半也在回 503。",
|
|
@@ -347,6 +355,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
347
355
|
cls: "F",
|
|
348
356
|
note: "7.116.0 修复轮(`approval-crash-convergence.ts` 的 `convergeCrashedAsksAtBoot`;local 车道启动收敛):前世留下的一条 `STREAM_PENDING` 问行,它的卡信封(`card_json`)读不出 ⇒ 读不出 wire `approvalId`、无从向审计账要成因词 ⇒ 成因**回落**成 `crashed_before_park`。行照样收 VOID(决断口照样 410 —— 可决性 fail-closed,不受影响);最坏后果 = 一张其实是被有序关停截断的卡,在 410 体的 `deniedReason` 上说成「进程崩了」(`crashConverged` 读面若有它的审计组,那里的成因仍按审计账判,两处可能不同词)。计数非零 = 问账里有读不出卡信封的行(位腐 / 手改 / 版本错位),去看探针 detail 点名的 askId。读数另在启动日志 `approval_ask_audit_crash_converged` 的 `causeUnreadable` 格单列。",
|
|
349
357
|
},
|
|
358
|
+
"server.outbound.hop-member-unreadable": {
|
|
359
|
+
cls: "F",
|
|
360
|
+
note: "出网代理那一跳建连失败的 wire 投影(`outbound-dispatcher.ts` `projectHopFailure`):按 core 连接失败分类器的读法复制错误图时,某个成员的访问器抛了(或对象不可 toString)。读法与分类器同一把,所以抛了当缺席;最坏后果 = 副本上少一个成员,分类器把这一次失败读成 unclassified(终局句只剩 `via outbound proxy` 一句、不带码),不影响重试阶梯的方向。只在怪形错误对象上触发,计数即可。",
|
|
361
|
+
},
|
|
350
362
|
"server.outbound.connect-observer-threw": {
|
|
351
363
|
cls: "F",
|
|
352
364
|
note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。",
|
|
@@ -11,6 +11,9 @@ export declare const PERMISSION_RULE_EVENT_TABLE: readonly [{
|
|
|
11
11
|
}, {
|
|
12
12
|
readonly event: "skill_allowed";
|
|
13
13
|
readonly meaning: "an activated skill's allowed-tools list cleared the ask for the rest of that run (core decisionReason \"skill\"; today only a skill the request itself carried can do this — this deployment's own skills carry no lists; it clears only the unstated / ordinary questions a persisted allow rule may clear, never a deny / ask rule, an operator list, a governance or shell-floor question, or a mandated one)";
|
|
14
|
+
}, {
|
|
15
|
+
readonly event: "allowed_tools_allowed";
|
|
16
|
+
readonly meaning: "the request's allow list (settings.permissions.allow, sent whole to the engine's allow layer) or the deployment's (APPROVAL_GRANT) cleared the ask (core decisionReason \"request_rule\" / \"deployment_rule\"; the log line carries source and the matched entries, the label carries the event word only)";
|
|
14
17
|
}];
|
|
15
18
|
/** 词表里的一个词(闭集)。 */
|
|
16
19
|
export type PermissionRuleEventWord = (typeof PERMISSION_RULE_EVENT_TABLE)[number]["event"];
|
|
@@ -16,6 +16,10 @@ export const PERMISSION_RULE_EVENT_TABLE = [
|
|
|
16
16
|
event: "skill_allowed",
|
|
17
17
|
meaning: "an activated skill's allowed-tools list cleared the ask for the rest of that run (core decisionReason \"skill\"; today only a skill the request itself carried can do this — this deployment's own skills carry no lists; it clears only the unstated / ordinary questions a persisted allow rule may clear, never a deny / ask rule, an operator list, a governance or shell-floor question, or a mandated one)",
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
event: "allowed_tools_allowed",
|
|
21
|
+
meaning: "the request's allow list (settings.permissions.allow, sent whole to the engine's allow layer) or the deployment's (APPROVAL_GRANT) cleared the ask (core decisionReason \"request_rule\" / \"deployment_rule\"; the log line carries source and the matched entries, the label carries the event word only)",
|
|
22
|
+
},
|
|
19
23
|
];
|
|
20
24
|
const PERMISSION_RULE_EVENT_KINDS = new Set(PERMISSION_RULE_EVENT_TABLE.map((r) => `${PERMISSION_TRACE_PREFIX}${r.event}`));
|
|
21
25
|
export function isPermissionRuleEventKind(kind) {
|
|
@@ -416,7 +416,7 @@ const RISK_DESCRIPTOR_TREATMENT = {
|
|
|
416
416
|
shellGateDoctrine: "verbatim",
|
|
417
417
|
summary: "text",
|
|
418
418
|
touchedPaths: "text[]",
|
|
419
|
-
|
|
419
|
+
allowRuleShadowed: "allow-rule-shadows",
|
|
420
420
|
probeReason: "text",
|
|
421
421
|
probeCause: "probe-cause",
|
|
422
422
|
};
|
|
@@ -473,12 +473,32 @@ export function redactRiskDescriptor(rd) {
|
|
|
473
473
|
out[key] = np;
|
|
474
474
|
break;
|
|
475
475
|
}
|
|
476
|
+
case "allow-rule-shadows": {
|
|
477
|
+
const recs = r[key];
|
|
478
|
+
if (!Array.isArray(recs))
|
|
479
|
+
break;
|
|
480
|
+
out[key] = recs.map((rec) => {
|
|
481
|
+
if (rec === null || typeof rec !== "object" || Array.isArray(rec))
|
|
482
|
+
return rec;
|
|
483
|
+
const o = rec;
|
|
484
|
+
return Array.isArray(o.entries) ? { ...o, entries: strs(o.entries) } : rec;
|
|
485
|
+
});
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
476
488
|
default: {
|
|
477
489
|
const unhandled = treatment;
|
|
478
490
|
throw new Error(`unhandled leaf treatment: ${String(unhandled)}`);
|
|
479
491
|
}
|
|
480
492
|
}
|
|
481
493
|
}
|
|
494
|
+
for (const [key, v] of Object.entries(r)) {
|
|
495
|
+
if (Object.hasOwn(RISK_DESCRIPTOR_TREATMENT, key))
|
|
496
|
+
continue;
|
|
497
|
+
if (typeof v === "string")
|
|
498
|
+
out[key] = str(v);
|
|
499
|
+
else if (Array.isArray(v))
|
|
500
|
+
out[key] = strs(v);
|
|
501
|
+
}
|
|
482
502
|
return changed ? out : rd;
|
|
483
503
|
}
|
|
484
504
|
const STATS_KEY_TREATMENT = {
|
package/dist/operator-ask.d.ts
CHANGED
|
@@ -59,7 +59,10 @@ export declare function operatorAsk(rule: string, message: string): OperatorAskR
|
|
|
59
59
|
* 席的同一只判据按 `mandated` 挡住它,耐久 `/decide` 腿的可记性(park 行 `mandated` 孪生位)同判 ⇒ 回执 `rememberApplied:false`。
|
|
60
60
|
* live 腿修前这里本就是个洞(进程内 `allow_session` 授权可退役 neverAuto 卡),随类收掉。
|
|
61
61
|
*
|
|
62
|
-
* ⚠️ **只**这两类:普通 `APPROVAL_REQUIRE` 仍只经 {@link operatorAsk}(
|
|
62
|
+
* ⚠️ **只**这两类:普通 `APPROVAL_REQUIRE` 仍只经 {@link operatorAsk}(不盖出身)。它的问带 `matchedAskRule` ⇒ 引擎类 `ask_rule`;站着的答案
|
|
63
|
+
* (「本会话放行」、耐久豁免行)清不清得掉它,出处是引擎类表 `foldArbitrationRow(classes).ruleMayClear` —— `ask_rule` 那一行说清不掉
|
|
64
|
+
* (`tool-approval.ts` 的 `standingAnswerMayRetire` 与它同读)。所以在「站着的答案清不掉」这一问上它与「恒问人」两类同答;不同在出身位
|
|
65
|
+
* (只这两类盖部署出身 ⇒ core 读 `operator_forced`、盖 `mandated`),其余各列按引擎类表各读各的行。
|
|
63
66
|
*/
|
|
64
67
|
export declare function operatorAlwaysHumanAsk(rule: string, message: string): OperatorAskResult;
|
|
65
68
|
/**
|
|
@@ -1,83 +1,104 @@
|
|
|
1
1
|
import { type WorkflowRun } from "@sema-agent/core";
|
|
2
|
-
/**
|
|
2
|
+
/** 一条索引行 = 一枚**指针**:记录 `runId` 的 `parks[]` 点名过子会话 `sessionId`。主键 = (子会话, run)—— 一条续跑链 / 一次分叉上
|
|
3
|
+
* 同一个子会话可以被好几条记录点名,判决要看到它们全部(见 {@link routeWorkflowPark})。 */
|
|
3
4
|
export interface WorkflowAgentSessionRow {
|
|
4
|
-
/**
|
|
5
|
+
/** 那只 workflow 子代自己的会话 id(= 条目的 `sessionId` = 那条 checkpoint 行的 `sessionId`;
|
|
5
6
|
* **不是** `sourceTaskId` —— 后者在 core 契约上是 ECHO-ONLY、明令不可作门输入)。 */
|
|
6
7
|
sessionId: string;
|
|
7
|
-
/**
|
|
8
|
+
/** 点名它的 workflow run(`w*`)。 */
|
|
8
9
|
runId: string;
|
|
9
|
-
/**
|
|
10
|
+
/** 该条目的稳定顺位身份(条目的 `callKey`)—— 行自描述用,判决以记录上的条目为准。 */
|
|
10
11
|
agentKey: string;
|
|
11
12
|
/** run 的租户键(`WorkflowRun.scope`)。 */
|
|
12
13
|
scope: string;
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
|
-
* 索引面的两个动词。**写者只有一个**(见文件头)
|
|
16
|
+
* 索引面的两个动词。**写者只有一个**(见文件头),读者是判别式。
|
|
16
17
|
*
|
|
17
|
-
* `syncRun` 语义 = 「这条 run
|
|
18
|
-
*
|
|
18
|
+
* `syncRun` 语义 = 「这条 run 当前的指针集**就是** rows」:多出来的旧行删,rows 里的行 upsert。幂等、可重放、与 run 的一次成功写同拍。
|
|
19
|
+
* `listByAgentSession` = 点名这个子会话的**全部**指针(各 run 一行;顺序不承重)。
|
|
19
20
|
*/
|
|
20
21
|
export interface WorkflowAgentSessionIndex {
|
|
21
22
|
syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
|
|
22
|
-
|
|
23
|
+
listByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow[]>;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
|
-
* 🔴 **park
|
|
26
|
+
* 🔴 **park 身份的单一真源**(core 7.17.0 [ref];持有者两成员 core 7.44.0 #1319)—— 引擎写在记录上的 `parks[]` 的元素形。
|
|
26
27
|
*
|
|
27
|
-
* 型从 core 的那一位取、不在本仓复述:core 往元素上加一位 ⇒ `trace/core-keyset-guard.ts` 的 ⑬b
|
|
28
|
-
* 与下面那只读者的构造式**同时**红,加键的人二选一,没有第三条静默的路。
|
|
28
|
+
* 型从 core 的那一位取、不在本仓复述:core 往元素上加一位 ⇒ `trace/core-keyset-guard.ts` 的 ⑬b 成员门与下面那只读者的构造式**同时**红。
|
|
29
29
|
*/
|
|
30
30
|
export type WorkflowRunParkKey = NonNullable<WorkflowRun["parks"]>[number];
|
|
31
31
|
/**
|
|
32
|
-
* 盘上 run blob → `parks[]` 的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as`
|
|
33
|
-
* {@link
|
|
32
|
+
* 盘上 run blob → `parks[]` 的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as` 铸形)。路由({@link routeWorkflowPark})与写者
|
|
33
|
+
* ({@link buildParkSessionRows})都经它读,「缺席」与「读不出」的边界只在这里划一次。
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
* ·
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* ·
|
|
43
|
-
*
|
|
44
|
-
* 而这只读者的消费面正是 park 的赎回路由。
|
|
45
|
-
*
|
|
46
|
-
* ⚠️ 「读不出来」与「读得出来、只是空的」是**两个状态**:后者 = 这条 run 不负责任何 park(诚实 miss),
|
|
47
|
-
* 前者 = 证不了,两者的处置相反 ⇒ 缺席永远不折成 `[]`。
|
|
35
|
+
* 判据:
|
|
36
|
+
* · 这一位**缺席** ⇒ `[]`:没有条目,持有不了任何东西。缺席是引擎契约里的合法形 —— core 7.17.0 之前写下的记录没有它,core 7.44.0
|
|
37
|
+
* 准入没走完的续跑记录(`resumeAdmissionIncomplete`)也没有它;引擎续跑这样的记录时自己从 journal 派生,走到停驻时写下带持有者位的新条目;
|
|
38
|
+
* · 这一位**在场却不是数组** ⇒ 读不出(`undefined`):记录说了话、说的不是一份列表;
|
|
39
|
+
* · 每条四个身份位必须是非空串;`originUnconfirmed` 只认字面 `true`(core 从不写 `false`);
|
|
40
|
+
* · **持有者两成员同进同出**(core 7.44.0 逐字:both or neither):`parkedBy` 非空串、`generation` 非负安全整数;只有其一、或任一形坏
|
|
41
|
+
* ⇒ 坏条目。两者皆无 = 旧引擎写的条目(证不了持有者),照读;
|
|
42
|
+
* · **任一条目形坏 ⇒ 整只读不出来**(整档成败制):core 对同一形整条拒 resume(`park_binding_broken`);「丢掉坏的、留下好的」会静默
|
|
43
|
+
* 铸出一份**比真相少几条 park** 的集合,而这只读者的消费面正是 park 的路由与赎回。
|
|
48
44
|
*/
|
|
49
45
|
export declare function readRunParks(run: unknown): WorkflowRunParkKey[] | undefined;
|
|
50
|
-
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
46
|
+
/**
|
|
47
|
+
* 🔴 **「这条记录的 run 持有这条停驻」的唯一读法**(core 7.44.0 #1319 逐字):`entry.parkedBy === record.id`。
|
|
48
|
+
* 继承来、本跑还没走到的条目带上一跑的 id;旧引擎写的条目两成员皆无 —— 两者都不是本记录持有的。
|
|
49
|
+
*/
|
|
50
|
+
export declare function heldParksOf(recordId: string, parks: readonly WorkflowRunParkKey[]): WorkflowRunParkKey[];
|
|
51
|
+
/**
|
|
52
|
+
* 派生的**唯一属主**:一只 `WorkflowRun` 的指针行 = 它 `parks[]` 里点名的每一个子会话(一个子会话一行)。`parks` 缺席 ⇒ 零条
|
|
53
|
+
* ({@link readRunParks}:没有条目)⇒ 写者按「这条 run 的指针集就是这一份」清掉它已有的指针。
|
|
54
|
+
* `parks[]` 读不出 ⇒ `undefined`(**派生不出来**,不是「零条」):写者据此不动这条 run 已有的指针(文件头「单写者」段)。
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildParkSessionRows(runId: string, run: {
|
|
57
|
+
scope: string;
|
|
58
|
+
parks?: unknown;
|
|
59
|
+
}): WorkflowAgentSessionRow[] | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* 一次路由的判决(闭集):
|
|
62
|
+
* · `held` —— 持有这个子会话的记录里最高代次**恰一条**:`park` = 它持有的那一条条目(赎回键在条目上);
|
|
63
|
+
* · `forked` —— 最高代次上不止一条记录(同一条 run 先后被续跑两次,两支都走到了这里):`runIds` 升序;
|
|
64
|
+
* · `unproven` —— 没有任何记录持有它,而这件事证不了:`unreadable` = 某一条候选记录的 `parks[]` 读不出(它可能就是持有者);
|
|
65
|
+
* `older_engine` = 点名它的条目没有持有者位(写于不带这两位的旧引擎);`runId` = 去续跑的那一条(`unreadable` 时是读不出的那条里 id 最小的);
|
|
66
|
+
* `readableHolderRunId` 只在 `unreadable` 上可能在场 —— **其余可读候选里**持有它的最高代次**恰一条**记录(规则 ②,与 `held` 同一只判);没有 / 并列 ⇒ 缺席。
|
|
67
|
+
* 它不改判决(任一读不出 ⇒ 整卡拒不变),只让拒体多给一条线索:读不出的那条引擎往往恰拒续跑,而健康的持有者就在旁边(工作可从它续上;
|
|
68
|
+
* 读不出的那条的指针仍在候选里,续跑它之后这张卡照旧整卡拒,直到那条记录被修好);
|
|
69
|
+
* · `none` —— 没有任何记录持有它,且候选全部读得出、条目都带持有者位 ⇒ 诚实 miss。
|
|
70
|
+
*/
|
|
71
|
+
export type WorkflowParkRoute = {
|
|
72
|
+
readonly kind: "held";
|
|
73
|
+
readonly runId: string;
|
|
74
|
+
readonly run: WorkflowRun;
|
|
75
|
+
readonly park: WorkflowRunParkKey;
|
|
76
|
+
} | {
|
|
77
|
+
readonly kind: "forked";
|
|
78
|
+
readonly runIds: readonly string[];
|
|
79
|
+
} | {
|
|
80
|
+
readonly kind: "unproven";
|
|
81
|
+
readonly cause: "unreadable" | "older_engine";
|
|
82
|
+
readonly runId: string;
|
|
83
|
+
readonly run: WorkflowRun;
|
|
84
|
+
readonly readableHolderRunId?: string;
|
|
85
|
+
} | {
|
|
86
|
+
readonly kind: "none";
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* 🔴 **路由与赎回的唯一判决**(纯函数;输入 = 点名这个子会话的每一条记录,读不到的记录 —— 已被收割 —— 由调用方先剔掉)。
|
|
60
90
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* `persist("update")` 打在 append **之前**。⇒ 那条判据会在 append 失败(以及 append 前的那一拍)上把
|
|
69
|
-
* 真正持有**新 token** 的那一跑整只排除,而上一跑的记录里只有**旧** token ⇒ 这条审批再也送不出去。
|
|
91
|
+
* 一条规则,按序:
|
|
92
|
+
* ① 任一候选的 `parks[]` 读不出 ⇒ `unproven/unreadable`(它可能正是最高代次的持有者;宁可不投,绝不投错 —— 引擎对条目形坏的记录整条拒续跑);
|
|
93
|
+
* 其余可读候选按下一条判出的持有者(恰一条)带进 `readableHolderRunId`(只给出路,不改判决);
|
|
94
|
+
* ② 其余候选里,持有这个子会话({@link heldParksOf})的条目取最高 `generation`:恰一条记录 ⇒ `held`;多于一条 ⇒ `forked`;
|
|
95
|
+
* ③ 没有持有者:有条目两成员皆无(旧引擎写的)⇒ `unproven/older_engine`,`runId` = 这类记录里最晚建的那条(续跑它,新引擎走到这张停驻时
|
|
96
|
+
* 写下自己的代次 0 行);否则 `none`。
|
|
97
|
+
* 判决不看写序、不看 `agents[]`、不看 `originRunId`(core:那一位说的是「材料在哪条 run 的 journal 里」,不是「谁站在上面」)。
|
|
70
98
|
*
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
export declare function
|
|
74
|
-
scope: string;
|
|
75
|
-
agents?: ReadonlyArray<{
|
|
76
|
-
status?: unknown;
|
|
77
|
-
sessionId?: unknown;
|
|
78
|
-
callKey?: unknown;
|
|
79
|
-
}>;
|
|
80
|
-
}): WorkflowAgentSessionRow[];
|
|
99
|
+
* 🔴 **窗与删码**:规则 ③ 的 `older_engine` 读臂是一次性迁移 M9 的读侧半场(升级那一刻还停驻着的存量记录)。窗口与删码清单见 `plugins/one-time-migrations.ts` 登记表 M9 一行,随 M9 一起删;这里不写第二份窗。
|
|
100
|
+
*/
|
|
101
|
+
export declare function routeWorkflowPark(sessionId: string, candidates: readonly WorkflowRun[]): WorkflowParkRoute;
|
|
81
102
|
/** 两处(装饰器的变更检测、local 后端的重放)共用的行集等值判据 —— 顺序无关的集合比较,单一属主。 */
|
|
82
103
|
export declare function sameAgentSessionRows(a: readonly WorkflowAgentSessionRow[], b: readonly WorkflowAgentSessionRow[]): boolean;
|
|
83
104
|
/** 账本一行的**内域形**:一次 `syncRun` 的整份行集快照(`{runId, rows}`)。 */
|
|
@@ -88,7 +109,7 @@ export interface WorkflowAgentSessionLedgerLine {
|
|
|
88
109
|
/**
|
|
89
110
|
* 盘上账本行 → 内域形的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as` 铸形)。
|
|
90
111
|
*
|
|
91
|
-
* 姿态与 SQL 孪生逐字同源:那边 `
|
|
112
|
+
* 姿态与 SQL 孪生逐字同源:那边 `listByAgentSession` 逐字段收窄(`String(r.session_id)` …),这边逐字段
|
|
92
113
|
* 判形 —— 一张索引面、两个后端、**一种**入域姿态。
|
|
93
114
|
*
|
|
94
115
|
* **整行成败制**(与既有的撕裂行臂同一条规则):一行 = 一次 syncRun 的**整份**快照,任一位读不出来就说明
|
|
@@ -109,18 +130,20 @@ export declare function readAgentSessionLedgerLine(v: unknown): WorkflowAgentSes
|
|
|
109
130
|
* 追加 / 重放走本仓 JSONL 店的**共用口**(S-977;此前 ctor 里自写 `truncateTornTail` + 手写 `split("\n") + JSON.parse` 重放):
|
|
110
131
|
* 追加 = core `AppendLog`(惰性打开、撕尾在**打开与每次追加前**修 —— 修前只在 boot 修,运行期半写后的下一次成功写仍会被粘进残尾),
|
|
111
132
|
* 重放 = `replayJsonlSkipping`(登记座 `workflow-agent-session-index`:中间坏行 —— 含半截 JSON 与形不对的整行 —— 跳过 + 逐条披露 + F tag;
|
|
112
|
-
* 撕尾不计、不披露)。盘上字节 / 0o600 账本 / 0o700
|
|
133
|
+
* 撕尾不计、不披露)。盘上字节 / 0o600 账本 / 0o700 目录与修前逐字同,升级零迁移:一行仍是「这条 run 的指针集」,旧账本行就是旧记录的指针
|
|
134
|
+
* (判决从记录读,指针本身没有新旧两种读法)。
|
|
113
135
|
*/
|
|
114
136
|
export declare class LocalWorkflowAgentSessionIndex implements WorkflowAgentSessionIndex {
|
|
115
137
|
private readonly fsyncEnabled;
|
|
116
138
|
private readonly ledgerPath;
|
|
117
139
|
private readonly byRun;
|
|
140
|
+
/** 子会话 → (run → 指针)。一个子会话可以被好几条记录点名(续跑链 / 分叉),全部留着 —— 判决看全集。 */
|
|
118
141
|
private readonly bySession;
|
|
119
142
|
private log;
|
|
120
143
|
constructor(root?: string, fsyncEnabled?: boolean);
|
|
121
|
-
/** 内存两张表的收敛(重放与写路径共用一份逻辑)
|
|
144
|
+
/** 内存两张表的收敛(重放与写路径共用一份逻辑):先摘这条 run 的旧指针,再挂新的。 */
|
|
122
145
|
private apply;
|
|
123
146
|
syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
|
|
124
|
-
|
|
147
|
+
listByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow[]>;
|
|
125
148
|
}
|
|
126
149
|
//# sourceMappingURL=workflow-agent-session-index.d.ts.map
|
|
@@ -6,27 +6,87 @@ export function readRunParks(run) {
|
|
|
6
6
|
if (typeof run !== "object" || run === null)
|
|
7
7
|
return undefined;
|
|
8
8
|
const v = run.parks;
|
|
9
|
+
if (v === undefined)
|
|
10
|
+
return [];
|
|
9
11
|
if (!Array.isArray(v))
|
|
10
12
|
return undefined;
|
|
11
13
|
const out = [];
|
|
12
14
|
for (const raw of v) {
|
|
13
15
|
if (typeof raw !== "object" || raw === null)
|
|
14
16
|
return undefined;
|
|
15
|
-
const { callKey, sessionId, token, originRunId } = raw;
|
|
17
|
+
const { callKey, sessionId, token, originRunId, originUnconfirmed, parkedBy, generation } = raw;
|
|
16
18
|
if (!nonEmptyString(callKey) || !nonEmptyString(sessionId) || !nonEmptyString(token) || !nonEmptyString(originRunId))
|
|
17
19
|
return undefined;
|
|
18
|
-
|
|
20
|
+
if (originUnconfirmed !== undefined && originUnconfirmed !== true)
|
|
21
|
+
return undefined;
|
|
22
|
+
const holder = parkedBy === undefined && generation === undefined
|
|
23
|
+
? {}
|
|
24
|
+
: nonEmptyString(parkedBy) && typeof generation === "number" && Number.isSafeInteger(generation) && generation >= 0
|
|
25
|
+
? { parkedBy, generation }
|
|
26
|
+
: undefined;
|
|
27
|
+
if (holder === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
out.push({ callKey, sessionId, token, originRunId, ...(originUnconfirmed === true ? { originUnconfirmed } : {}), ...holder });
|
|
19
30
|
}
|
|
20
31
|
return out;
|
|
21
32
|
}
|
|
22
|
-
export function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
export function heldParksOf(recordId, parks) {
|
|
34
|
+
return parks.filter((p) => p.parkedBy === recordId);
|
|
35
|
+
}
|
|
36
|
+
export function buildParkSessionRows(runId, run) {
|
|
37
|
+
const parks = readRunParks(run);
|
|
38
|
+
if (parks === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
const bySession = new Map();
|
|
41
|
+
for (const p of parks)
|
|
42
|
+
if (!bySession.has(p.sessionId))
|
|
43
|
+
bySession.set(p.sessionId, { sessionId: p.sessionId, runId, agentKey: p.callKey, scope: run.scope });
|
|
44
|
+
return [...bySession.values()];
|
|
45
|
+
}
|
|
46
|
+
export function routeWorkflowPark(sessionId, candidates) {
|
|
47
|
+
const ordered = [...candidates].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
48
|
+
const readable = [];
|
|
49
|
+
let firstUnreadable;
|
|
50
|
+
for (const run of ordered) {
|
|
51
|
+
const parks = readRunParks(run);
|
|
52
|
+
if (parks === undefined)
|
|
53
|
+
firstUnreadable ??= run;
|
|
54
|
+
else
|
|
55
|
+
readable.push({ run, parks });
|
|
56
|
+
}
|
|
57
|
+
const top = highestHolders(sessionId, readable);
|
|
58
|
+
if (firstUnreadable !== undefined) {
|
|
59
|
+
const holder = top.length === 1 ? top[0].run.id : undefined;
|
|
60
|
+
return { kind: "unproven", cause: "unreadable", runId: firstUnreadable.id, run: firstUnreadable, ...(holder !== undefined ? { readableHolderRunId: holder } : {}) };
|
|
61
|
+
}
|
|
62
|
+
if (top.length === 1)
|
|
63
|
+
return { kind: "held", runId: top[0].run.id, run: top[0].run, park: top[0].park };
|
|
64
|
+
if (top.length > 1)
|
|
65
|
+
return { kind: "forked", runIds: top.map((t) => t.run.id) };
|
|
66
|
+
const older = readable.filter(({ parks }) => parks.some((p) => p.sessionId === sessionId && p.parkedBy === undefined));
|
|
67
|
+
if (older.length > 0) {
|
|
68
|
+
const latest = older.reduce((a, b) => (b.run.createdAt > a.run.createdAt || (b.run.createdAt === a.run.createdAt && b.run.id > a.run.id) ? b : a));
|
|
69
|
+
return { kind: "unproven", cause: "older_engine", runId: latest.run.id, run: latest.run };
|
|
70
|
+
}
|
|
71
|
+
return { kind: "none" };
|
|
72
|
+
}
|
|
73
|
+
function highestHolders(sessionId, readable) {
|
|
74
|
+
let top = [];
|
|
75
|
+
let topGeneration = -1;
|
|
76
|
+
for (const { run, parks } of readable) {
|
|
77
|
+
for (const park of heldParksOf(run.id, parks)) {
|
|
78
|
+
if (park.sessionId !== sessionId || park.generation === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
if (park.generation > topGeneration) {
|
|
81
|
+
topGeneration = park.generation;
|
|
82
|
+
top = [{ run, park }];
|
|
83
|
+
}
|
|
84
|
+
else if (park.generation === topGeneration && !top.some((t) => t.run.id === run.id)) {
|
|
85
|
+
top.push({ run, park });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
28
88
|
}
|
|
29
|
-
return
|
|
89
|
+
return top;
|
|
30
90
|
}
|
|
31
91
|
export function sameAgentSessionRows(a, b) {
|
|
32
92
|
if (a.length !== b.length)
|
|
@@ -97,15 +157,21 @@ export class LocalWorkflowAgentSessionIndex {
|
|
|
97
157
|
}
|
|
98
158
|
apply(runId, rows) {
|
|
99
159
|
for (const old of this.byRun.get(runId) ?? []) {
|
|
100
|
-
|
|
160
|
+
const runs = this.bySession.get(old.sessionId);
|
|
161
|
+
runs?.delete(runId);
|
|
162
|
+
if (runs?.size === 0)
|
|
101
163
|
this.bySession.delete(old.sessionId);
|
|
102
164
|
}
|
|
103
165
|
if (rows.length === 0)
|
|
104
166
|
this.byRun.delete(runId);
|
|
105
167
|
else
|
|
106
168
|
this.byRun.set(runId, [...rows]);
|
|
107
|
-
for (const r of rows)
|
|
108
|
-
this.bySession.
|
|
169
|
+
for (const r of rows) {
|
|
170
|
+
let runs = this.bySession.get(r.sessionId);
|
|
171
|
+
if (runs === undefined)
|
|
172
|
+
this.bySession.set(r.sessionId, (runs = new Map()));
|
|
173
|
+
runs.set(runId, r);
|
|
174
|
+
}
|
|
109
175
|
}
|
|
110
176
|
async syncRun(runId, rows) {
|
|
111
177
|
if (this.ledgerPath !== undefined) {
|
|
@@ -114,8 +180,8 @@ export class LocalWorkflowAgentSessionIndex {
|
|
|
114
180
|
}
|
|
115
181
|
this.apply(runId, rows);
|
|
116
182
|
}
|
|
117
|
-
async
|
|
118
|
-
return this.bySession.get(sessionId) ??
|
|
183
|
+
async listByAgentSession(sessionId) {
|
|
184
|
+
return [...(this.bySession.get(sessionId)?.values() ?? [])];
|
|
119
185
|
}
|
|
120
186
|
}
|
|
121
187
|
//# sourceMappingURL=workflow-agent-session-index.js.map
|
|
@@ -177,7 +177,12 @@ export declare class FileWorkflowCompletionInbox extends InMemoryWorkflowComplet
|
|
|
177
177
|
private fd;
|
|
178
178
|
private appendsSinceCompact;
|
|
179
179
|
constructor(root: string, onWarn?: (msg: string, meta: Record<string, unknown>) => void, fsyncEnabled?: boolean);
|
|
180
|
+
/** 重放 = 本仓 JSONL 重放属主(登记座 `workflow-completion-inbox`,fail-open + F tag):每行过 {@link INBOX_RECORD_SHAPE},
|
|
181
|
+
* 读不出形(`null` / 非对象 / 缺 `op` / 缺 id …)⇒ 属主跳过 + 逐条披露,**不抛**;撕尾(崩在一次 append 中途)是崩溃的正常落盘形,
|
|
182
|
+
* 属主不计。好行按序折叠 —— {@link foldRecord} 与运行期各口同一套语义(重放是严格折叠)。 */
|
|
180
183
|
private replay;
|
|
184
|
+
/** 把一条已判过形的记录折进内存。穷尽 switch:加一种记录 ⇒ 编译红,由人处置(形判据与折叠同批改)。 */
|
|
185
|
+
private foldRecord;
|
|
181
186
|
private commit;
|
|
182
187
|
/** Compact iff we've appended enough since the last one. MUST be called AFTER the in-memory mutation the
|
|
183
188
|
* triggering commit records — compact() rewrites from `bySession`, so a compact fired BEFORE the caller
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdirSync,
|
|
1
|
+
import { mkdirSync, openSync, writeSync, fsyncSync, closeSync, renameSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { redactSecrets } from "../trace/redact.js";
|
|
4
|
+
import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
|
|
4
5
|
import { injectionTierOrAbsent } from "../trace/injection-tier.js";
|
|
5
6
|
import { semaProvenanceOrAbsent } from "../trace/sema-provenance.js";
|
|
6
7
|
import { resupplyFields, terminalFrameFacetFields } from "../trace/task-notification-facets.js";
|
|
@@ -37,6 +38,28 @@ export function foldKeyFamily(runId) {
|
|
|
37
38
|
return i > 0 && /^\d+$/.test(runId.slice(i + 1)) ? runId.slice(0, i) : runId;
|
|
38
39
|
}
|
|
39
40
|
const servedKey = (sessionId, runId) => JSON.stringify([sessionId, runId]);
|
|
41
|
+
const isLedgerId = (v) => typeof v === "string" && v.length > 0;
|
|
42
|
+
const INBOX_RECORD_SHAPE = {
|
|
43
|
+
what: 'an inbox record {op: "enqueue" (entry with sessionId + runId) | "ack" (sessionId + runId) | "drain" (sessionId)}',
|
|
44
|
+
read(record) {
|
|
45
|
+
const r = record;
|
|
46
|
+
switch (r.op) {
|
|
47
|
+
case "enqueue": {
|
|
48
|
+
const e = r.entry;
|
|
49
|
+
if (typeof e !== "object" || e === null || Array.isArray(e))
|
|
50
|
+
return undefined;
|
|
51
|
+
const { sessionId, runId } = e;
|
|
52
|
+
return isLedgerId(sessionId) && isLedgerId(runId) ? { op: "enqueue", entry: e } : undefined;
|
|
53
|
+
}
|
|
54
|
+
case "ack":
|
|
55
|
+
return isLedgerId(r.sessionId) && isLedgerId(r.runId) ? { op: "ack", sessionId: r.sessionId, runId: r.runId } : undefined;
|
|
56
|
+
case "drain":
|
|
57
|
+
return isLedgerId(r.sessionId) ? { op: "drain", sessionId: r.sessionId } : undefined;
|
|
58
|
+
default:
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
40
63
|
export class InMemoryWorkflowCompletionInbox {
|
|
41
64
|
onWarn;
|
|
42
65
|
bySession = new Map();
|
|
@@ -137,29 +160,28 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
|
|
|
137
160
|
this.compact();
|
|
138
161
|
}
|
|
139
162
|
replay() {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let rec;
|
|
147
|
-
try {
|
|
148
|
-
rec = JSON.parse(line);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (rec.op === "enqueue" && rec.entry?.sessionId && rec.entry.runId)
|
|
163
|
+
for (const rec of replayJsonlSkipping(this.ledgerPath, "workflow-completion-inbox", INBOX_RECORD_SHAPE).records)
|
|
164
|
+
this.foldRecord(rec);
|
|
165
|
+
}
|
|
166
|
+
foldRecord(rec) {
|
|
167
|
+
switch (rec.op) {
|
|
168
|
+
case "enqueue":
|
|
154
169
|
this.applyEnqueue(rec.entry, false);
|
|
155
|
-
|
|
170
|
+
return;
|
|
171
|
+
case "ack": {
|
|
156
172
|
const q = this.bySession.get(rec.sessionId);
|
|
157
173
|
q?.delete(rec.runId);
|
|
158
174
|
if (q && q.size === 0)
|
|
159
175
|
this.bySession.delete(rec.sessionId);
|
|
176
|
+
return;
|
|
160
177
|
}
|
|
161
|
-
|
|
178
|
+
case "drain":
|
|
162
179
|
this.bySession.delete(rec.sessionId);
|
|
180
|
+
return;
|
|
181
|
+
default: {
|
|
182
|
+
const unreachable = rec;
|
|
183
|
+
return unreachable;
|
|
184
|
+
}
|
|
163
185
|
}
|
|
164
186
|
}
|
|
165
187
|
commit(rec) {
|
|
@@ -322,7 +322,7 @@ export declare class JournalingWorkflowRunStore implements WorkflowRunStore {
|
|
|
322
322
|
* **自吞错**:索引是观察面的派生物,它的故障绝不能把一次已成功的 run 持久化变成失败 —— 与
|
|
323
323
|
* `publishFleet` 的姿态同源。但自吞 ≠ 静默([ref]):留痕走**登记过的** fail-open tag
|
|
324
324
|
* `server.workflow-park-index.sync-failed`(P-DEBT;射程、后果与欠账的唯一属主 = 那条词表项的
|
|
325
|
-
* `note`,census 里有对应行)。 */
|
|
325
|
+
* `note`,census 里有对应行);记录的 `parks[]` 读不出而不同步的那一臂另走 `server.workflow-park-index.parks-unreadable`(F 类,见下)。 */
|
|
326
326
|
private syncAgentSessionIndex;
|
|
327
327
|
/** Derive + publish the MF-Fleet workflow row from a WorkflowRun (doneCount/totalCount from agents, tokens from
|
|
328
328
|
* stats); a terminal workflow LEAVES the fleet (publish terminal then remove), a running one stays/updates. */
|
|
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { AppendLog, ensureDir } from "@sema-agent/core";
|
|
3
3
|
import { buildFleetWorkflowRow } from "../fleet/fleet-bus.js";
|
|
4
4
|
import { isTerminalWorkflowRowStatus, seedTerminalWorkflowRow } from "../fleet/fleet-terminal-window.js";
|
|
5
|
-
import {
|
|
5
|
+
import { buildParkSessionRows, sameAgentSessionRows } from "./workflow-agent-session-index.js";
|
|
6
6
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
7
7
|
import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
|
|
8
8
|
const SUMMARY_MAX = 500;
|
|
@@ -278,7 +278,12 @@ export class JournalingWorkflowRunStore {
|
|
|
278
278
|
const index = this.agentSessionIndex;
|
|
279
279
|
if (index === undefined)
|
|
280
280
|
return;
|
|
281
|
-
const rows =
|
|
281
|
+
const rows = buildParkSessionRows(id, run);
|
|
282
|
+
if (rows === undefined) {
|
|
283
|
+
this.indexedRows.delete(id);
|
|
284
|
+
recordFailOpen("server.workflow-park-index.parks-unreadable", `runId=${id}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
282
287
|
const prior = this.indexedRows.get(id);
|
|
283
288
|
if (prior === undefined || !sameAgentSessionRows(prior, rows)) {
|
|
284
289
|
try {
|