@sema-agent/server 7.43.0 → 7.44.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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -2,41 +2,9 @@ import { isTerminalRunStatus } from "./plugins/store-contracts.js";
|
|
|
2
2
|
import { DENY_REASONS, VOID_REASONS } from "./approval-deny-reasons.js";
|
|
3
3
|
import { buildRevokeFrame } from "./approval-card.js";
|
|
4
4
|
import { encodeCheckpointScope } from "./security.js";
|
|
5
|
-
/* 判据 2 的「非 suspended 终局」判据 = `store-contracts.ts` 的 {@link isTerminalRunStatus}(词表属主处的
|
|
6
|
-
* 穷举 switch,core 加/删 `TaskStatus` 成员当场 tsc 红)。此处**不再手抄**一份 `ReadonlySet<string>`:
|
|
7
|
-
* 那份的门对象是 `string`,对词表增删两向都无钉,而这张词表真变过(core 5.8.0 删 `"timeout"`)。
|
|
8
|
-
* `suspended`/`needs_review`/`running` 都不是终局 ⇒ 落判据 3 保持。 */
|
|
9
|
-
/** core 给「被取消」的 run 打的 `errorCode`(§9 C3 的取消判别键)。 */
|
|
10
5
|
const CANCELLED_ERROR_CODE = "cancelled";
|
|
11
|
-
/**
|
|
12
|
-
* 🔴 收敛器每一次 store/读口调用的墙钟上限(codex 交叉复审 round3 R3-2,2026-08-06 真缺陷)。
|
|
13
|
-
*
|
|
14
|
-
* 为什么必须有:`runOnce` 被 reaper 腿的 **in-flight 守卫**包着(一次只跑一轮)。一个挂死的依赖
|
|
15
|
-
* (checkpoint 店 / run 店 / ask 店任意一个黑洞化)会让 `runOnce` 永不返回 ⇒ 守卫从此再不放行 ⇒
|
|
16
|
-
* **全部** approval 行的对账永久停摆,而这正是本模块存在的目的(崩溃恢复)。超时后单行进 per-row
|
|
17
|
-
* catch(warn + 计数 + 留在原态下轮重试),其余行照常推进 —— 一条坏依赖不许饿死整段扫描。
|
|
18
|
-
* 10s:远大于任何健康查询,又远小于 reaper tick 的实际容忍度。
|
|
19
|
-
*/
|
|
20
6
|
const RECONCILE_STORE_TIMEOUT_MS = 10_000;
|
|
21
|
-
/**
|
|
22
|
-
* 🔴 **每段**扫描的墙钟预算(codex 交叉复审 round4 R4-2,2026-08-06 验真)。
|
|
23
|
-
*
|
|
24
|
-
* 单看 per-call deadline 是不够的:行是**串行**处理的,一个黑洞化的依赖会让每行都吃满
|
|
25
|
-
* {@link RECONCILE_STORE_TIMEOUT_MS}。默认 batch=200 时 `runOnce` 要跑掉半小时量级,上帽 10000 行更是
|
|
26
|
-
* 一天量级 —— 而 reaper 的 in-flight 守卫在这期间会**跳过每一个 tick**,孤儿段(段二)甚至一次都轮不到。
|
|
27
|
-
* 于是「一个依赖挂了」被放大成「整个对账面停摆数小时」,正好是本模块要防的那件事。
|
|
28
|
-
*
|
|
29
|
-
* 修法:**每段各自**一份预算(不是共享一份)——段一吃光也绝不影响段二开跑(孤儿代打是崩溃恢复的兜底,
|
|
30
|
-
* 优先级不比判据表低)。超预算即停本段,剩下的行下轮接着扫(`deferReconcile` 的队列轮转保证「下轮」
|
|
31
|
-
* 真的能轮到新行),并记一条 warn。
|
|
32
|
-
*/
|
|
33
7
|
const RECONCILE_SEGMENT_BUDGET_MS = 30_000;
|
|
34
|
-
/** 给一次读/写套墙钟上限(超时以 Error 拒绝,由 per-row catch 接住)。定时器 `unref`,绝不持住进程;
|
|
35
|
-
* 竞速输的那一路由 `Promise.race` 自己的 handler 接住,不会变成 unhandled rejection。
|
|
36
|
-
*
|
|
37
|
-
* A-002.10:竞速一结束就 `clearTimeout`。`unref` 只保证不吊住进程退出,**不**回收定时器本身——本函数在
|
|
38
|
-
* 逐行串行的 `runOnce` 里每行要走 1~6 次,不清就是「每行 1~6 只挂满 {@link RECONCILE_STORE_TIMEOUT_MS}」
|
|
39
|
-
* 的累积(batchLimit 默认 200 ⇒ 一轮几百只带闭包的定时器)。`finally` 对成功/失败两路都清。 */
|
|
40
8
|
function withDeadline(op, label, timeoutMs = RECONCILE_STORE_TIMEOUT_MS) {
|
|
41
9
|
let timer;
|
|
42
10
|
return Promise.race([
|
|
@@ -50,64 +18,19 @@ function withDeadline(op, label, timeoutMs = RECONCILE_STORE_TIMEOUT_MS) {
|
|
|
50
18
|
clearTimeout(timer);
|
|
51
19
|
});
|
|
52
20
|
}
|
|
53
|
-
/**
|
|
54
|
-
* 这只 ask 是不是**祖先冻结 approver 层**在委派 fold 中途铸的那一份(#168 件1④,黑板 [2912]③)。
|
|
55
|
-
*
|
|
56
|
-
* 判别位是**产品自己发的**,不是外部约定:core 的 `withDelegationProvenance` 只包**子代自己那条缝**,
|
|
57
|
-
* 祖先层那次走未包装的原函数 ⇒ `AskRequest.delegation` 在不在,就是「这一份是哪一层发的」。落到行上,
|
|
58
|
-
* `delegation.parentToolCallId` 是必填字段,铸行时逐字透传进 `parent_tool_call_id` 列 ⇒ **列非 NULL
|
|
59
|
-
* ⇔ delegation 在场**(不需要新列,也不需要回 `req` 里再读一次)。
|
|
60
|
-
*
|
|
61
|
-
* 第二维 `sourceTaskId !== sessionId` 把「根腿」摘出去:根腿的 `sourceTaskId` 恒等于会话锚(core 给
|
|
62
|
-
* `AskRequest.sourceTaskId` 填的就是该腿 sessionId),它根本不在任何 fold 里,`parent_tool_call_id`
|
|
63
|
-
* 为 NULL 是它的常态而不是信号。
|
|
64
|
-
*
|
|
65
|
-
* 命中 ⇒ 退出硬相等(判据 1 结构上不命中,行落 ②③④⑤)。实测(test AI 围栏)checkpoint 存的摘要**逐字
|
|
66
|
-
* 等于子代自己那条缝**、只不等于祖先层那一次 —— 所以收窄到这一层,子代自己的缝照常参与。
|
|
67
|
-
*/
|
|
68
21
|
export function isAncestorFoldMint(ask) {
|
|
69
22
|
return ask.parentToolCallId === null && ask.sourceTaskId !== ask.sessionId;
|
|
70
23
|
}
|
|
71
|
-
/**
|
|
72
|
-
* 判据 1 的**硬谓词**(纯函数,§9 C2 + §8 D-2;#168 件1 起返回分类而不是布尔)。
|
|
73
|
-
*
|
|
74
|
-
* 逐条:
|
|
75
|
-
* - 祖先层 fold 中途铸点 ⇒ 直接退出(见 {@link isAncestorFoldMint});
|
|
76
|
-
* - `unparseable` 候选直接出局(读不出 ⇒ 不确定 ⇒ 不命中);
|
|
77
|
-
* - **身份三元组**:`sourceTaskId` **相等** ∧ `toolCallId` **相等** ∧ 时间维的**因果下界**
|
|
78
|
-
* `candidate.createdAtMs >= ask.createdAtMs`(⚠️ 第三维**不是等式** —— 把它读成等式会让合法的 park
|
|
79
|
-
* 几乎命不中,行随后落 ②③⑤ 被误判)—— 摘要**不是**身份([2897]③①:
|
|
80
|
-
* 同 args 的两次调用摘要天然相同,只靠摘要硬相等会把第二次投递并进第一次的票);任一维在候选侧
|
|
81
|
-
* 缺席(读不出的 blob 没有 `sourceTaskId`)即身份不成立;
|
|
82
|
-
* - **hash 双等**:两侧都必须在场且相等,任一侧缺席一律不 bind;禁「能取到时才比」的可选谓词。
|
|
83
|
-
* 归因分两级(顺序即代码顺序):身份这一层先判 —— 同身份候选为空**且**候选集非空 ⇒ `identity_miss`;
|
|
84
|
-
* 走到 hash 这一层才把缺席记成**单铸** `single_mint`(见 {@link GateMatchOutcome})。
|
|
85
|
-
*
|
|
86
|
-
* 多候选时的取舍:优先 `status === "pending"`(活着的那张 gate),否则取最早的一条(读口按
|
|
87
|
-
* `created_at_ms ASC` 返回)。两者都满足全部硬谓词,选谁都不会错配;取 pending 只是让 `PARKED` 行落到
|
|
88
|
-
* 一个还能被 resume 的坐标上,对壳更有用。
|
|
89
|
-
*/
|
|
90
24
|
export function classifyGateMatch(ask, candidates) {
|
|
91
25
|
const outcome = classifyGateMatchBeforeFoldVeto(ask, candidates);
|
|
92
|
-
// 🔴 祖先层否决**排在最后**、而且只否决已经成立的命中(纯减法)。放在最前面判会让这条谓词的每一次
|
|
93
|
-
// 假阳性(`sourceTaskId ≠ sessionId` 还有别的成因,例如 session-less 的 adhoc 腿)都白白关掉判据 1,
|
|
94
|
-
// 并把一条本该记成 `single_mint`/`identity_miss` 的行错记成「祖先层铸点」。放在这里,它最坏只是**不
|
|
95
|
-
// 命中**(与判据 1 的既有 fail-safe 同向),而计数从此只统计「本来会 bind、被这条裁定拦下」的行 ——
|
|
96
|
-
// 非零读数因此是真信号,不是噪声底。
|
|
97
26
|
if (outcome.kind === "match" && isAncestorFoldMint(ask))
|
|
98
27
|
return { kind: "ancestor_fold_mint" };
|
|
99
28
|
return outcome;
|
|
100
29
|
}
|
|
101
30
|
function classifyGateMatchBeforeFoldVeto(ask, candidates) {
|
|
102
|
-
// 身份三元组的前两维 + 因果下界(摘要不参与这一层——它是身份之外的**第二道**等式)。
|
|
103
31
|
const sameIdentity = candidates.filter((c) => c.unparseable !== true && c.sourceTaskId !== null && c.sourceTaskId === ask.sourceTaskId && c.boundCallId === ask.toolCallId && c.createdAtMs >= ask.createdAtMs);
|
|
104
32
|
const askHash = ask.boundInputHash;
|
|
105
33
|
if (sameIdentity.length === 0) {
|
|
106
|
-
// 「同身份的 checkpoint 一条都没有」有两种成因,不能混:
|
|
107
|
-
// · 候选集**空** ⇒ 这条弧上根本没有对家,只有 ask 侧铸过(或两侧都没铸)= 单铸;
|
|
108
|
-
// · 候选集非空 ⇒ 有对家、只是不是这一只(别的 sourceTaskId/callId),或者读不出(`unparseable`,
|
|
109
|
-
// 「不确定」)。两者都**不是**「只有一侧铸过」这个事实陈述,一律记 `identity_miss`——
|
|
110
|
-
// 把一条读不出的坏行说成「对家从来不存在」是拿不确定冒充确定。
|
|
111
34
|
if (candidates.length > 0)
|
|
112
35
|
return { kind: "identity_miss" };
|
|
113
36
|
return { kind: "single_mint", side: askHash === null ? "neither" : "ask_only" };
|
|
@@ -116,61 +39,46 @@ function classifyGateMatchBeforeFoldVeto(ask, candidates) {
|
|
|
116
39
|
if (askHash === null)
|
|
117
40
|
return { kind: "single_mint", side: withHash.length > 0 ? "checkpoint_only" : "neither" };
|
|
118
41
|
if (withHash.length === 0)
|
|
119
|
-
return { kind: "single_mint", side: "ask_only" };
|
|
42
|
+
return { kind: "single_mint", side: "ask_only" };
|
|
120
43
|
const matches = withHash.filter((c) => c.boundInputHash === askHash);
|
|
121
44
|
const picked = matches.find((c) => c.status === "pending") ?? matches[0];
|
|
122
45
|
if (picked !== undefined)
|
|
123
46
|
return { kind: "match", candidate: picked };
|
|
124
47
|
return { kind: "hash_mismatch", candidates: withHash.length };
|
|
125
48
|
}
|
|
126
|
-
/** {@link classifyGateMatch} 的布尔面(判据 1 命中即返回那条候选)。分类信息由调用方按需另取。 */
|
|
127
49
|
export function selectGateCandidate(ask, candidates) {
|
|
128
50
|
const outcome = classifyGateMatch(ask, candidates);
|
|
129
51
|
return outcome.kind === "match" ? outcome.candidate : undefined;
|
|
130
52
|
}
|
|
131
|
-
/** 判据表 v2 的判定(纯函数;顺序 = 设计稿 §9 尾的五臂汇总,注见文件头)。 */
|
|
132
53
|
export function decideReconcileAction(input) {
|
|
133
54
|
const { ask, candidates, run, batchState, nowMs, adhocGraceMs, orphanTtlMs } = input;
|
|
134
|
-
// ① identity ∧ hash 双等
|
|
135
55
|
if (input.allowBind !== false) {
|
|
136
56
|
const match = selectGateCandidate(ask, candidates);
|
|
137
57
|
if (match) {
|
|
138
58
|
return { kind: "bind", gate: { gateToken: match.token, gateBoundCallId: match.boundCallId, gateBoundInputHash: match.boundInputHash } };
|
|
139
59
|
}
|
|
140
60
|
}
|
|
141
|
-
// 批已 ABORTED = 取消腿走过 ⇒ abort 语义(§9 C3),与 run 终局与否无关:批一旦 ABORTED,这只 ask 的
|
|
142
|
-
// 投递面已经被整批撤掉,再判 run 状态没有意义。
|
|
143
61
|
if (batchState === "ABORTED")
|
|
144
62
|
return { kind: "void", reason: VOID_REASONS.BATCH_ABORTED };
|
|
145
|
-
// 🔴 bind-once 落选者(codex round2 R2-3):批已绑给**别人** ⇒ 本行结构上再也赢不了 `bindBatch`。
|
|
146
|
-
// 它是**当下就可判**的终局(VOID),不是「等等看」——落 hold 会让它滞留到 run 终局,然后被判据 2 误报
|
|
147
|
-
// 成路由失败。判据要求 `boundAskId` 真的读到且不是自己:读不到(null)时不据此下判(未知 ≠ 落选)。
|
|
148
63
|
if (batchState === "ROUTING_BOUND" && input.batchBoundAskId != null && input.batchBoundAskId !== ask.askId) {
|
|
149
64
|
return { kind: "void", reason: VOID_REASONS.BATCH_BOUND_ELSEWHERE };
|
|
150
65
|
}
|
|
151
|
-
// ② run 终局分臂
|
|
152
66
|
if (run !== null && isTerminalRunStatus(run.status)) {
|
|
153
67
|
if (run.status === "failed" && run.errorCode === CANCELLED_ERROR_CODE) {
|
|
154
68
|
return { kind: "void", reason: VOID_REASONS.RUN_CANCELLED };
|
|
155
69
|
}
|
|
156
70
|
return { kind: "deny", reason: DENY_REASONS.ROUTING_FAILURE };
|
|
157
71
|
}
|
|
158
|
-
// ④ adhoc 双谓词(§8 C-2):结构判别 + 窗过 + 宽限。**不是** DENIED(约束②禁超时 denial)。
|
|
159
72
|
if (run === null && ask.sessionId === ask.taskId && nowMs > ask.expiresAtMs + adhocGraceMs) {
|
|
160
73
|
return { kind: "void", reason: VOID_REASONS.ADHOC_LEG_NO_DURABLE_DOMAIN };
|
|
161
74
|
}
|
|
162
|
-
// ⑤ 遗孤最终可判(§8 A-1 放宽形):量 immutable 的 createdAtMs。放在 ③ 之前判 —— 它是**所有**
|
|
163
|
-
// 「本轮判不出终局」的行的共同兜底,包括 run 一直在跑的那一支。
|
|
164
75
|
if (nowMs - ask.createdAtMs >= orphanTtlMs)
|
|
165
76
|
return { kind: "void", reason: VOID_REASONS.ORPHAN_TTL_EXCEEDED };
|
|
166
|
-
// ③ else:保持 PARKING(候下轮;执行器负责 deferReconcile 排队尾)。`candidates` 在这一臂被显式忽略
|
|
167
|
-
// ——判据 1 已经判过且没命中,这里不做任何「差不多算匹配」的兜底。
|
|
168
77
|
void candidates;
|
|
169
78
|
return { kind: "hold" };
|
|
170
79
|
}
|
|
171
80
|
export function createApprovalReconciler(deps) {
|
|
172
81
|
const { askStore, checkpoints, runs, logger, metrics, batchLimit, pendingGraceMs, adhocGraceMs, orphanTtlMs, emitRevoke } = deps;
|
|
173
|
-
/** 撤卡帧发射(live only;失败 catch + warn,**绝不回滚 CAS** —— 帧是通知,真源是行)。空名单不发。 */
|
|
174
82
|
const revoke = (row, askIds, reason, nowMs) => {
|
|
175
83
|
if (askIds.length === 0 || !emitRevoke)
|
|
176
84
|
return;
|
|
@@ -182,45 +90,19 @@ export function createApprovalReconciler(deps) {
|
|
|
182
90
|
logger.warn("approval_revoke_emit_failed", { batchId: row.batchId, count: askIds.length, err: err instanceof Error ? err.message : String(err) });
|
|
183
91
|
}
|
|
184
92
|
catch {
|
|
185
|
-
// 可观测面自身绝不能变成故障源(reapers.ts createThrottledReaperCatch 同款纪律)。
|
|
186
93
|
}
|
|
187
94
|
}
|
|
188
95
|
};
|
|
189
|
-
/** 一条 `PARKING` 行的收敛(读事实 → 纯判定 → 一次 CAS)。返回本行落到哪一臂 + 判据 1 的分类(供统计;
|
|
190
|
-
* `match` 缺席 = 本轮压根没走到判据 1,与「走了但没命中」是两回事,不能记成同一格)。 */
|
|
191
96
|
const reconcileOne = async (ask, nowMs) => {
|
|
192
|
-
// ⓪ 批态**先读**(codex round3 R3-2):它是唯一一个能让本行「结构上再也不可能 bind」的事实,而且是
|
|
193
|
-
// 一次主键读。批已 `ABORTED` 或已绑给**别人** ⇒ 判据 1 的 `bindBatch` 谓词(`ROUTING_UNBOUND`)
|
|
194
|
-
// 注定不成立 ⇒ 跳过 checkpoint/run 两次读**结论不变**,却少挂两个可能黑洞化的依赖(原先的次序下,
|
|
195
|
-
// 一条挂死的 checkpoint 店会把这类**当下就可判**的行连同整个 `runOnce` 一起拖住,再经 reaper 的
|
|
196
|
-
// in-flight 守卫把后续所有 tick 一并封死)。
|
|
197
97
|
const batchRow = await withDeadline(askStore.getBatch(ask.batchId), "getBatch");
|
|
198
98
|
let batchState = batchRow?.state ?? "MISSING";
|
|
199
99
|
let batchBoundAskId = batchRow?.boundAskId ?? null;
|
|
200
100
|
const irreversibleBatch = batchState === "ABORTED" || (batchState === "ROUTING_BOUND" && batchBoundAskId !== null && batchBoundAskId !== ask.askId);
|
|
201
|
-
// 判据 1:窄谓词精确查(scope 必填 —— 租户门是读口自己的责任,§8 D-3)。因果下界直接下推成
|
|
202
|
-
// `sinceMs`,SQL 侧就把早于本 ask 的 checkpoint 排掉了(`selectGateCandidate` 里还会再判一次,
|
|
203
|
-
// 纵深防御)。
|
|
204
101
|
let gateMatch;
|
|
205
102
|
if (checkpoints && !irreversibleBatch) {
|
|
206
|
-
// 🔴 **已知缺口,登记而不假称已解**(codex 交叉复审 2026-08-07 [high] 一,验真;**先存**,不是本批
|
|
207
|
-
// 引入):这里的 session 维传的是 `ask.sessionId` = **投递上下文**的会话(`tool-approval.ts` 铸行时
|
|
208
|
-
// 取 `primary.sessionId`),而委派子代那条腿的 checkpoint 落在**子代自己**的 sessionId 上
|
|
209
|
-
// (core 铸 `cp.sessionId = 该腿 sessionId`,同值也写进 `cp.sourceTaskId`)。⇒ 子代自己那条 approver
|
|
210
|
-
// 缝的 park 在 SQL 谓词这一层就被滤掉了,判据 1 对委派腿结构上够不着 —— 黑板 [2912]③「子代自己那条
|
|
211
|
-
// 缝可以参与硬相等」的那一半,在本仓当前接线下还落不了地(行改走 ②③④⑤,fail-safe)。
|
|
212
|
-
//
|
|
213
|
-
// 顺带说清本批**改善**了什么:三元组把 `cp.sourceTaskId === ask.sourceTaskId` 变成硬谓词之后,
|
|
214
|
-
// 一只子代 ask 再也不可能认领**宿主**腿的 park(同 session 查出来的候选,callId 撞车 + 同 args 摘要
|
|
215
|
-
// 相同就够了,而 `PARKED` 不可回滚)——那是本批之前真实存在的错配面。
|
|
216
|
-
//
|
|
217
|
-
// 修法(未做,需自带判据):把 session 维换成「铸 park 的那条腿的会话」。风险在于 `ask.sourceTaskId`
|
|
218
|
-
// 有 `?? origin.taskId` 兜底,core 万一不填就会把一个 taskId 当 session 去查(比今天更差),所以
|
|
219
|
-
// **必须**配一条用真 checkpoint 店、宿主/子代两个不同 sessionId 的端到端钉才动。开题上报。
|
|
220
103
|
const candidates = await withDeadline(checkpoints.findCheckpointCandidatesForAsk(encodeCheckpointScope(ask.owner), ask.sessionId, ask.toolCallId, ask.createdAtMs), "findCheckpointCandidatesForAsk");
|
|
221
104
|
gateMatch = classifyGateMatch(ask, candidates);
|
|
222
105
|
if (gateMatch.kind === "hash_mismatch") {
|
|
223
|
-
// 留痕不拒(见 ReconcileStats.hashMismatch 顶注):warn 说清成因方向,行继续按 ②③④⑤ 判。
|
|
224
106
|
try {
|
|
225
107
|
logger.warn("approval_reconcile_hash_mismatch", {
|
|
226
108
|
askId: ask.askId,
|
|
@@ -233,13 +115,10 @@ export function createApprovalReconciler(deps) {
|
|
|
233
115
|
});
|
|
234
116
|
}
|
|
235
117
|
catch {
|
|
236
|
-
/* 可观测面绝不成为故障源 */
|
|
237
118
|
}
|
|
238
119
|
}
|
|
239
120
|
const match = gateMatch.kind === "match" ? gateMatch.candidate : undefined;
|
|
240
121
|
if (match) {
|
|
241
|
-
// 🔴 `bindBatch` 内部**一次**完成 `PARKING→PARKED` + gate 落行 + 兄弟连坐 VOID(§8 A-2:正文
|
|
242
|
-
// 「先 transitionAsk 再 bindBatch」是死锁形,已作废)。只调它一次。
|
|
243
122
|
const res = await withDeadline(askStore.bindBatch(ask.batchId, ask.askId, {
|
|
244
123
|
gateToken: match.token,
|
|
245
124
|
gateBoundCallId: match.boundCallId,
|
|
@@ -249,53 +128,32 @@ export function createApprovalReconciler(deps) {
|
|
|
249
128
|
revoke(ask, res.voidedSiblings, "superseded_by_park", nowMs);
|
|
250
129
|
return { outcome: "parked", match: gateMatch };
|
|
251
130
|
}
|
|
252
|
-
// 判别式失败臂(§9 C3):拿到批的**真实**态 + 中选者,降级续判 —— `ABORTED` ⇒ VOID(abort 语义),
|
|
253
|
-
// `ROUTING_BOUND` 且中选者是兄弟 ⇒ VOID(bind-once 落选者,R2-3),其余按 ②③④⑤。
|
|
254
131
|
batchState = res.batchState;
|
|
255
132
|
batchBoundAskId = res.boundAskId ?? null;
|
|
256
133
|
}
|
|
257
134
|
}
|
|
258
|
-
// run 行只在批**还可能**让本行走下去时才读(不可逆批态下它对结论零影响,见 ⓪)。
|
|
259
135
|
const run = runs && !irreversibleBatch ? ((await withDeadline(runs.getRun(ask.taskId), "getRun")) ?? null) : null;
|
|
260
|
-
// 判据 1 已经在上面单独判过(命中即 `bindBatch`,不命中/绑失败都落这里)⇒ `allowBind: false`。
|
|
261
136
|
const action = decideReconcileAction({ ask, candidates: [], run, batchState, batchBoundAskId, nowMs, adhocGraceMs, orphanTtlMs, allowBind: false });
|
|
262
137
|
switch (action.kind) {
|
|
263
138
|
case "bind": {
|
|
264
|
-
// `allowBind: false` 下结构上不可达;留一条防御性 hold,绝不在这里第二次调 bindBatch。
|
|
265
139
|
await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.rev, nowMs), "deferReconcile");
|
|
266
140
|
return { outcome: "held", match: gateMatch };
|
|
267
141
|
}
|
|
268
142
|
case "deny": {
|
|
269
|
-
// 🔴 全仓唯一的 `DENIED` 写点(grep 钉)。归因是判据 2 的**终局证据**,不是超时推断。
|
|
270
|
-
// CAS 输(行在本轮判定期间被别人收走)= 正常,如实记成 held(没动过行),下轮重扫。
|
|
271
143
|
const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "DENIED", { deniedReason: action.reason, updatedAtMs: nowMs }), "transitionAsk(DENIED)");
|
|
272
144
|
return { outcome: won ? "denied" : "held", match: gateMatch };
|
|
273
145
|
}
|
|
274
146
|
case "void": {
|
|
275
|
-
// 🔴 codex 交叉复审 C3(2026-08-06 真缺陷):**必须看 CAS 的返回值**。原先无条件记账 + 发撤卡帧
|
|
276
|
-
// ——本行若在判定与写入之间被另一个副本 `bindBatch` 收成 PARKED,这次 CAS 干净地输,行**没有**
|
|
277
|
-
// 被翻成 VOID,而帧却告诉壳「这张卡作废了」⇒ 直接违反撤卡帧守恒(名单 ≡ 真被翻成 VOID 的集合),
|
|
278
|
-
// 壳会清掉一张仍然有效、还在等 gate 的卡。输 ⇒ 什么都不做,下轮按新态重判。
|
|
279
147
|
const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "VOID", { deniedReason: action.reason, updatedAtMs: nowMs }), "transitionAsk(VOID)");
|
|
280
148
|
if (!won)
|
|
281
149
|
return { outcome: "held", match: gateMatch };
|
|
282
150
|
if (action.reason === VOID_REASONS.ORPHAN_TTL_EXCEEDED) {
|
|
283
151
|
logger.warn("approval_ask_orphan_voided", { askId: ask.askId, taskId: ask.taskId, ageMs: nowMs - ask.createdAtMs, ttlMs: orphanTtlMs });
|
|
284
152
|
}
|
|
285
|
-
// 单行 VOID 也是撤卡:壳上那张卡必须消失(名单 = 这一行自己)。
|
|
286
|
-
// reason 取 `"aborted"`:v1 的闭集只有两员,而 `"superseded_by_park"` 的字面语义是「**被别人的
|
|
287
|
-
// park 连坐**」——②④⑤ 三条臂(run 取消 / 批 ABORTED / adhoc 无对账域 / 遗孤 TTL)都不是那回事,
|
|
288
|
-
// 硬套会把归因写成谎。`"aborted"`(这条腿死了)是两员里唯一诚实的那个;真正的细分归因在**行上**
|
|
289
|
-
// (`denied_reason` 落的是 VOID_REASONS 的具体值),帧只负责让壳清卡。
|
|
290
|
-
// (存疑登记:v2 若要让壳按成因分渲,给 reason 闭集加一个 `"voided"` 成员即可,属 wire 加员。)
|
|
291
|
-
// 落选者(R2-3)是**真的**被别人的 park 顶掉 ⇒ 用 `superseded_by_park`;其余单行 VOID 用 `"aborted"`
|
|
292
|
-
// (两员闭集里唯一诚实的那个,见上注)。
|
|
293
153
|
revoke(ask, [ask.askId], action.reason === VOID_REASONS.BATCH_BOUND_ELSEWHERE ? "superseded_by_park" : "aborted", nowMs);
|
|
294
154
|
return { outcome: "voided", match: gateMatch };
|
|
295
155
|
}
|
|
296
156
|
case "hold": {
|
|
297
|
-
// 队列轮转(§8 D-4 / §9 C5):推 `updated_at_ms` 排到队尾。CAS 输(版本已被别人推进)= 正常,
|
|
298
|
-
// 本轮判定已过期,下轮重来。
|
|
299
157
|
await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.rev, nowMs), "deferReconcile");
|
|
300
158
|
return { outcome: "held", match: gateMatch };
|
|
301
159
|
}
|
|
@@ -319,10 +177,9 @@ export function createApprovalReconciler(deps) {
|
|
|
319
177
|
budgetExhausted: 0,
|
|
320
178
|
orphansExpired: 0,
|
|
321
179
|
};
|
|
322
|
-
// ── 段一:PARKING 判据表 ────────────────────────────────────────────────────────────────
|
|
323
180
|
const parking = await withDeadline(askStore.listByState("PARKING", batchLimit), "listByState(PARKING)");
|
|
324
181
|
stats.scanned = parking.length;
|
|
325
|
-
const parkingDeadline = Date.now() + RECONCILE_SEGMENT_BUDGET_MS;
|
|
182
|
+
const parkingDeadline = Date.now() + RECONCILE_SEGMENT_BUDGET_MS;
|
|
326
183
|
for (const ask of parking) {
|
|
327
184
|
if (Date.now() >= parkingDeadline) {
|
|
328
185
|
stats.budgetExhausted += 1;
|
|
@@ -330,12 +187,11 @@ export function createApprovalReconciler(deps) {
|
|
|
330
187
|
logger.warn("approval_reconcile_budget_exhausted", { segment: "PARKING", processed: stats.parked + stats.denied + stats.voided + stats.held + stats.failed, scanned: stats.scanned, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
|
|
331
188
|
}
|
|
332
189
|
catch {
|
|
333
|
-
/* 可观测面绝不成为故障源 */
|
|
334
190
|
}
|
|
335
|
-
break;
|
|
191
|
+
break;
|
|
336
192
|
}
|
|
337
193
|
if (ask.boundInputHash === null)
|
|
338
|
-
stats.unmatchableNoHash += 1;
|
|
194
|
+
stats.unmatchableNoHash += 1;
|
|
339
195
|
try {
|
|
340
196
|
const { outcome, match } = await reconcileOne(ask, nowMs);
|
|
341
197
|
if (outcome === "parked")
|
|
@@ -346,8 +202,6 @@ export function createApprovalReconciler(deps) {
|
|
|
346
202
|
stats.voided += 1;
|
|
347
203
|
else
|
|
348
204
|
stats.held += 1;
|
|
349
|
-
// 判据 1 的分类计数(#168 件1②③④)——`match` 缺席 = 本轮没走到判据 1(无 checkpoint 面 /
|
|
350
|
-
// 批已不可逆),那既不是 mismatch 也不是单铸,一格都不记。
|
|
351
205
|
switch (match?.kind) {
|
|
352
206
|
case "hash_mismatch":
|
|
353
207
|
stats.hashMismatch += 1;
|
|
@@ -364,26 +218,20 @@ export function createApprovalReconciler(deps) {
|
|
|
364
218
|
stats.singleMintNeither += 1;
|
|
365
219
|
break;
|
|
366
220
|
default:
|
|
367
|
-
break;
|
|
221
|
+
break;
|
|
368
222
|
}
|
|
369
223
|
}
|
|
370
224
|
catch (err) {
|
|
371
|
-
// 🔴 per-row 隔离(邻居 bg-agent per-scope 先例):一条行的坏 blob / 一次瞬时错误不许饿死同批
|
|
372
|
-
// 其余 199 行。失败响亮(warn + 计数),行留在 PARKING 下轮重试。
|
|
373
225
|
stats.failed += 1;
|
|
374
226
|
try {
|
|
375
227
|
logger.warn("approval_reconcile_row_failed", { askId: ask.askId, taskId: ask.taskId, err: err instanceof Error ? err.message : String(err) });
|
|
376
228
|
}
|
|
377
229
|
catch {
|
|
378
|
-
/* 可观测面绝不成为故障源 */
|
|
379
230
|
}
|
|
380
231
|
}
|
|
381
232
|
}
|
|
382
|
-
// ── 段二:孤儿 STREAM_PENDING 代打 expire(§1.3 + §8 D-1.1 宽限)──────────────────────────
|
|
383
|
-
// 🔴 **不是第五竞争者**:它就是窗到期竞争者在崩溃后的补位,同一 CAS 同一转移。宽限保证活属主的
|
|
384
|
-
// 本地 timer 常态必胜(reaper 退回纯崩溃兜底),不与在场闭包抢。
|
|
385
233
|
const pending = await withDeadline(askStore.listByState("STREAM_PENDING", batchLimit), "listByState(STREAM_PENDING)");
|
|
386
|
-
const orphanDeadline = Date.now() + RECONCILE_SEGMENT_BUDGET_MS;
|
|
234
|
+
const orphanDeadline = Date.now() + RECONCILE_SEGMENT_BUDGET_MS;
|
|
387
235
|
for (const row of pending) {
|
|
388
236
|
if (Date.now() >= orphanDeadline) {
|
|
389
237
|
stats.budgetExhausted += 1;
|
|
@@ -391,16 +239,15 @@ export function createApprovalReconciler(deps) {
|
|
|
391
239
|
logger.warn("approval_reconcile_budget_exhausted", { segment: "STREAM_PENDING", expired: stats.orphansExpired, scanned: pending.length, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
|
|
392
240
|
}
|
|
393
241
|
catch {
|
|
394
|
-
/* 同上 */
|
|
395
242
|
}
|
|
396
243
|
break;
|
|
397
244
|
}
|
|
398
245
|
if (row.expiresAtMs + pendingGraceMs >= nowMs)
|
|
399
|
-
continue;
|
|
246
|
+
continue;
|
|
400
247
|
try {
|
|
401
248
|
const res = await withDeadline(askStore.expireAsk(row.askId, row.batchId), "expireAsk(orphan)");
|
|
402
249
|
if (!res.won)
|
|
403
|
-
continue;
|
|
250
|
+
continue;
|
|
404
251
|
stats.orphansExpired += 1;
|
|
405
252
|
revoke(row, res.voidedSiblings, "superseded_by_park", nowMs);
|
|
406
253
|
}
|
|
@@ -410,12 +257,9 @@ export function createApprovalReconciler(deps) {
|
|
|
410
257
|
logger.warn("approval_orphan_expire_failed", { askId: row.askId, taskId: row.taskId, err: err instanceof Error ? err.message : String(err) });
|
|
411
258
|
}
|
|
412
259
|
catch {
|
|
413
|
-
/* 同上 */
|
|
414
260
|
}
|
|
415
261
|
}
|
|
416
262
|
}
|
|
417
|
-
// 判据 1 盲区响亮化:有行结构上无法命中就每 tick 报一次(不是每行一次——刷屏没有信息量)。
|
|
418
|
-
// 静默地把「没法比」当成「比过了、不匹配」正是这条要治的病(它下游就是一个不诚实的 DENIED)。
|
|
419
263
|
if (stats.unmatchableNoHash > 0) {
|
|
420
264
|
try {
|
|
421
265
|
logger.warn("approval_reconcile_rows_unmatchable", {
|
|
@@ -425,10 +269,8 @@ export function createApprovalReconciler(deps) {
|
|
|
425
269
|
});
|
|
426
270
|
}
|
|
427
271
|
catch {
|
|
428
|
-
/* 可观测面绝不成为故障源 */
|
|
429
272
|
}
|
|
430
273
|
}
|
|
431
|
-
// 健康 tick 保持安静(邻居 reapCount 同姿势):只有真动了行才记一条。
|
|
432
274
|
if (stats.parked + stats.denied + stats.voided + stats.orphansExpired > 0) {
|
|
433
275
|
metrics?.inc("approval_reconciled_total", { outcome: "parked" }, stats.parked);
|
|
434
276
|
metrics?.inc("approval_reconciled_total", { outcome: "denied" }, stats.denied);
|
package/dist/approval.js
CHANGED
|
@@ -1,35 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Human-in-the-loop approval gate (F4) — durable-only since 5.0.0 ([2354]/[2356] A-5:OA 轮询车道
|
|
3
|
-
* (createOaApprovalPolicy + ApprovalStore pending 行轮询)整体退役;唯一门形 = durable checkpoint
|
|
4
|
-
* suspend/resume,见 createDurableAskPolicy 与 /v1/approvals 的 checkpointStore 腿)。
|
|
5
|
-
*/
|
|
6
1
|
import {} from "@sema-agent/core";
|
|
7
|
-
/**
|
|
8
|
-
* 运维**是否表达了门意图** —— boot 的单用户 allow-all 基线只在"零门意图"时才允许铺开,
|
|
9
|
-
* 所以这个谓词漏一格 = 那一格的意图被 allow-all 静默吞掉。
|
|
10
|
-
*
|
|
11
|
-
* 🔴 由来(2026-07-31 缝合审):这判据本来是 `main.ts` 里的一行内联表达式,只枚举了
|
|
12
|
-
* requireApproval / deny / durableApproval **三格**,漏了 `approvalNeverAuto`。漏的根因不是手滑,
|
|
13
|
-
* 是 `config-types.ts` 当时把 neverAuto 描述成 "Subset of approvalRequire" —— 若真是子集,
|
|
14
|
-
* 枚举 requireApproval 就覆盖了它。但本文件 inv#2 的执行面逐字相反(neverAuto ⊄ requireApproval),
|
|
15
|
-
* 于是「单用户 + 只配 APPROVAL_NEVER_AUTO=Bash」这一**语义完全合理**的形被判成零门意图 ⇒
|
|
16
|
-
* 落进 `createAllowDenyPolicy({})` 全面放行 ⇒ 那张"永远问人"的表从未进入任何 policy,Bash 直接跑。
|
|
17
|
-
*
|
|
18
|
-
* 判定挪到**与 inv#2 同一个文件**就是为了这个:两者再想漂开,得有人同时改这两段。
|
|
19
|
-
* 新增任何"门意图"配置项时,这里必须同步加一格(下面的表驱动测试会点名漏的那格)。
|
|
20
|
-
*/
|
|
21
2
|
export function hasOperatorGateIntent(config) {
|
|
22
3
|
return (config.approvalRequire.length > 0 ||
|
|
23
4
|
config.approvalDeny.length > 0 ||
|
|
24
|
-
config.approvalNeverAuto.length > 0 ||
|
|
5
|
+
config.approvalNeverAuto.length > 0 ||
|
|
25
6
|
config.durableApproval);
|
|
26
7
|
}
|
|
27
|
-
/**
|
|
28
|
-
* 门意图不可服务=boot 拒启(全窗复审 D3-F1,HIGH;与 env 墓碑族同谱系)。
|
|
29
|
-
* v4.3.0 的轮询门形(APPROVAL_REQUIRE + DB backend + DURABLE_APPROVAL 未设)在 5.0.0 只剩 core 每任务
|
|
30
|
-
* warn——**原先拦、现在放**(方向反转)。「留 undefined 让 core warn」对新配置是曝露误配,对升级存量是
|
|
31
|
-
* 门静默消失,后者必须响亮。放这文件与 hasOperatorGateIntent/inv#2 同源(意图判据再漂要同时改两段)。
|
|
32
|
-
*/
|
|
33
8
|
export function assertGateIntentServiceable(config, checkpointStorePresent) {
|
|
34
9
|
if (!hasOperatorGateIntent(config) || checkpointStorePresent)
|
|
35
10
|
return;
|
|
@@ -39,53 +14,31 @@ export function assertGateIntentServiceable(config, checkpointStorePresent) {
|
|
|
39
14
|
throw new Error("DURABLE_APPROVAL=true but no checkpoint store is available — the configured DB backend has no checkpoint face (or no backend is configured). Wire a MySQL-protocol/PostgreSQL/file backend, or unset DURABLE_APPROVAL.");
|
|
40
15
|
}
|
|
41
16
|
export function createDurableAskPolicy(opts) {
|
|
42
|
-
// 🔒 RAW-name matching(core 5.0.0 RB-476:canonicalToolName 折叠面整体退役)。安全前提没有变松,
|
|
43
|
-
// 只是换了执行者:live 请求名恒为 CC canonical(旧名在 core roster 层响亮 miss,永远到不了本 policy);
|
|
44
|
-
// 配置侧写了退役名 = core prepare 硬失败 `config.legacy_tool_name`(+ 本仓 config 装配墓碑先一步拒启)。
|
|
45
|
-
// 旧的双侧归一防的「旧名滑过新名规则」在 5.0.0 下无成立路径——折叠臂留着才是静默兼容层。
|
|
46
17
|
const require = new Set(opts.requireApproval);
|
|
47
18
|
const deny = new Set(opts.deny ?? []);
|
|
48
19
|
const neverAuto = new Set(opts.neverAuto ?? []);
|
|
49
20
|
const budget = Math.max(0, Math.floor(opts.autoBudget ?? 0));
|
|
50
|
-
// design/80 D-E (upgrade-budget): a per-run-leg counter of NORMAL gated calls auto-approved so far. The policy
|
|
51
|
-
// is rebuilt per resolveSpec call (= per submit AND per resume — server.ts), so this closure is per-leg —
|
|
52
|
-
// adequate for a runaway-loop circuit-breaker (the loop lives within one leg; a suspend only fires on an
|
|
53
|
-
// over-budget / never-auto ask, which escalates to a human anyway). A durable per-run budget is a refinement.
|
|
54
21
|
let autoApproved = 0;
|
|
55
22
|
return {
|
|
56
23
|
check: (req) => {
|
|
57
|
-
const toolName = req.toolName;
|
|
24
|
+
const toolName = req.toolName;
|
|
58
25
|
if (deny.has(toolName))
|
|
59
26
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
60
|
-
// inv#2: a never-auto (irreversible / safety) tool ALWAYS asks a human — UNCONDITIONALLY, even if it was not
|
|
61
|
-
// also listed in requireApproval. An operator marking a tool "never auto-approve" means "always human"; it
|
|
62
|
-
// must never fall through to a bare allow (the footgun: neverAuto ⊄ requireApproval). Checked BEFORE the
|
|
63
|
-
// budget so a safety tool is never auto-approved. The enforcement is THIS deterministic policy `ask` — NOT a
|
|
64
|
-
// decisionReason (core 1.102 council proved decisionReason both misses policy-ask asks AND is forgeable, so
|
|
65
|
-
// the safety axis is sourced only from tool marks / core's persisted CheckpointGate.safetyAxis, never here).
|
|
66
27
|
if (neverAuto.has(toolName))
|
|
67
28
|
return { action: "ask", message: `human approval required to run "${req.toolName}"` };
|
|
68
29
|
if (require.has(toolName)) {
|
|
69
|
-
// Invariant alignment (workflow audit 2026-07-13): the DURABLE-resource-exhausted escalation is a
|
|
70
|
-
// SAFETY-axis ask (like neverAuto), so it outranks an exemption. In practice core already fail-fasts
|
|
71
|
-
// an exhausted leg before any tool call (runtask budget gate) — this is defense-in-depth ordering,
|
|
72
|
-
// zero behavior change on the live path, and it makes "耗尽恒问人" structural rather than incidental.
|
|
73
30
|
const remEarly = req.budget?.resourceRemainingMicroUsd;
|
|
74
31
|
if (remEarly !== undefined && remEarly <= 0) {
|
|
75
32
|
return { action: "ask", message: `durable resource budget exhausted — human approval required to run "${req.toolName}"` };
|
|
76
33
|
}
|
|
77
|
-
// Session exemption — an APPROVAL MEMORY, not a policy loosen: deny/neverAuto already won
|
|
78
|
-
// above; this only skips the HUMAN ask for a require-listed tool the operator said "remember" on. The
|
|
79
|
-
// probe is fail-closed (store error ⇒ not exempt ⇒ the normal ask path). AskUserQuestion is excluded
|
|
80
|
-
// for the same reason as the budget below (owned by the durable-question policy; its ask must stand).
|
|
81
34
|
if (opts.exempt && toolName !== "AskUserQuestion") {
|
|
82
35
|
return (async () => {
|
|
83
36
|
let exempted = false;
|
|
84
37
|
try {
|
|
85
|
-
exempted = (await opts.exempt(toolName)) === true;
|
|
38
|
+
exempted = (await opts.exempt(toolName)) === true;
|
|
86
39
|
}
|
|
87
40
|
catch {
|
|
88
|
-
exempted = false;
|
|
41
|
+
exempted = false;
|
|
89
42
|
}
|
|
90
43
|
if (exempted) {
|
|
91
44
|
opts.onExempted?.(toolName, req.toolName);
|
|
@@ -99,27 +52,11 @@ export function createDurableAskPolicy(opts) {
|
|
|
99
52
|
return { action: "allow" };
|
|
100
53
|
},
|
|
101
54
|
};
|
|
102
|
-
/** The pre-existing require-branch tail (durable resource cap → per-leg count budget → ask), factored so the
|
|
103
|
-
* async exemption probe and the sync path share ONE implementation (no drift). */
|
|
104
55
|
function decideRequired(req, toolName) {
|
|
105
|
-
// a NORMAL gated tool AUTO-APPROVES while under the per-leg budget (the AI proceeds, no human); once the
|
|
106
|
-
// budget is exhausted the rest escalate to a human (the circuit-breaker trips). budget 0 ⇒ always ask.
|
|
107
|
-
// 🔴 EXCLUDE AskUserQuestion: it is owned by the durable-question policy (it ALWAYS asks), and
|
|
108
|
-
// combinePolicies (deny>ask>allow) makes that ask outrank our allow anyway — so counting it would
|
|
109
|
-
// silently shrink the real auto-approve budget for genuine NORMAL tools (adversarial finding). It falls
|
|
110
|
-
// through to ask here, consuming no budget.
|
|
111
|
-
// design/80 D-E-core (b, Framing 3): self-limit against the DURABLE resource ledger. If the run's durable
|
|
112
|
-
// resource budget is EXHAUSTED, escalate to a human REGARDLESS of the per-leg count budget. `req.budget`
|
|
113
|
-
// (core 1.103, built per-leg from the frozen-total resourceLedger) survives suspend/resume and CANNOT be
|
|
114
|
-
// re-granted (frozen-total, prior-wins) — so this is the durable cap that closes the per-leg re-grant below.
|
|
115
|
-
// 🔴 `resourceRemainingMicroUsd` is NEVER falsy-tested: 0 = exhausted (escalate), undefined = no ceiling.
|
|
116
56
|
const rem = req.budget?.resourceRemainingMicroUsd;
|
|
117
57
|
if (rem !== undefined && rem <= 0) {
|
|
118
58
|
return { action: "ask", message: `durable resource budget exhausted — human approval required to run "${req.toolName}"` };
|
|
119
59
|
}
|
|
120
|
-
// The per-leg COUNT budget remains a circuit-breaker (looser than configured across a deny-resume, but
|
|
121
|
-
// BOUNDED by core's suspendLoopCap, and inv#2 holds — a safety tool returned ask above). The durable
|
|
122
|
-
// resource cap above is the exact-per-run limiter the per-leg counter could not be (finding#1).
|
|
123
60
|
if (toolName !== "AskUserQuestion" && budget > 0 && autoApproved < budget) {
|
|
124
61
|
autoApproved++;
|
|
125
62
|
return { action: "allow" };
|
package/dist/audit.js
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
// 🔴 core 1.403 迁移(2026-07-25):`Session` 从 runtime class 降级成纯 interface,可构造的 storage-backed
|
|
2
|
-
// class 改名 `StoredSession`(core CHANGELOG 1.403.0:"the constructable storage-backed class formerly
|
|
3
|
-
// exported as `Session` is now `StoredSession`")——这里是 runtime 构造用法,照 core 给的迁移指南改。
|
|
4
1
|
import { StoredSession, FileSessionRepo, InMemorySessionStorage, boundedTail } from "@sema-agent/core";
|
|
5
2
|
import { TiDBSessionStorage } from "./plugins/tidb-session-storage.js";
|
|
6
3
|
import { PgSessionStorage } from "./plugins/pg-session-storage.js";
|
|
7
|
-
/** Build the audit view from the durable log. Returns undefined if the session does not exist. */
|
|
8
4
|
export async function buildSessionAudit(pool, sessionId) {
|
|
9
5
|
return auditFromStorage(await TiDBSessionStorage.wake(pool, sessionId), sessionId);
|
|
10
6
|
}
|
|
11
|
-
/** The PG twin — same view over PgSessionStorage.wake. Until this, the
|
|
12
|
-
* audit face was TiDB-only and a PG/local deployment 404'd the whole web session area. local = follow-on
|
|
13
|
-
* (its storage seam has no wake-equivalent yet — may turn out to be a core gap). */
|
|
14
7
|
export async function buildSessionAuditPg(pool, sessionId) {
|
|
15
8
|
return auditFromStorage(await PgSessionStorage.wake(pool, sessionId), sessionId);
|
|
16
9
|
}
|
|
17
|
-
/** The LOCAL (File) leg — web session area on DB_BACKEND=local.
|
|
18
|
-
* 实现姿势:`FileSessionRepo.exportEntries`(纯读、torn-tail 恢复、missing→not_found)→
|
|
19
|
-
* `boundedTail`(纯函数切 floor)→ InMemorySessionStorage → 同一 auditFromStorage 内核。零写面、
|
|
20
|
-
* 零新 seam;createdAt 从 repo.list() 的 metadata 取(File 形态 meta 与 entries 分储)。 */
|
|
21
10
|
export async function buildSessionAuditLocal(root, sessionId) {
|
|
22
11
|
const repo = new FileSessionRepo(root);
|
|
23
12
|
let entries;
|
|
@@ -32,19 +21,14 @@ export async function buildSessionAuditLocal(root, sessionId) {
|
|
|
32
21
|
const meta = (await repo.list()).find((m) => m.id === sessionId);
|
|
33
22
|
const { tail, floorEntryId } = boundedTail(entries);
|
|
34
23
|
const storage = new InMemorySessionStorage({ entries: tail, floorEntryId, ...(meta ? { metadata: meta } : {}) });
|
|
35
|
-
return auditFromStorage(storage, sessionId);
|
|
24
|
+
return auditFromStorage(storage, sessionId);
|
|
36
25
|
}
|
|
37
|
-
/** The dialect-neutral kernel: both wake() twins land on core's StoredSession over a BaseSessionStorage. */
|
|
38
26
|
async function auditFromStorage(storage, sessionId) {
|
|
39
27
|
if (!storage)
|
|
40
28
|
return undefined;
|
|
41
29
|
const session = new StoredSession(storage);
|
|
42
30
|
const meta = await session.getMetadata();
|
|
43
31
|
const ctx = await session.buildContext();
|
|
44
|
-
// [1023]①: the typed prompt_epoch entry is NOT an LLM message, so it never appears in buildContext()'s
|
|
45
|
-
// messages — attach it as its own additive field. One kernel call covers all three dialect twins.
|
|
46
|
-
// (On the Local leg the bounded tail may pre-date the root pin; core resolves through the kept
|
|
47
|
-
// compaction's restatement — a truly pre-epoch session stays undefined → key omitted.)
|
|
48
32
|
const promptEpoch = await session.getPromptEpoch();
|
|
49
33
|
return {
|
|
50
34
|
sessionId,
|
|
@@ -56,18 +40,6 @@ async function auditFromStorage(storage, sessionId) {
|
|
|
56
40
|
...(promptEpoch ? { promptEpoch } : {}),
|
|
57
41
|
};
|
|
58
42
|
}
|
|
59
|
-
// ── web 会话区性能 PRD 的两条 additive 缝 ────────────────────────────────────────────────────
|
|
60
|
-
//
|
|
61
|
-
// The audit view's `messages` used to be ALL-OR-NOTHING: a 1000-entry session shipped its whole current
|
|
62
|
-
// context on open (first-paint transfer + parse both scale with history). Two PURE post-processing helpers
|
|
63
|
-
// give the route the additive knobs (hard requirement: parameterless requests stay byte-identical):
|
|
64
|
-
// windowMessages — tail/before+limit windowing over the messages array (offset-based; offsets are stable
|
|
65
|
-
// within one floor generation — the response's floorEntryId is the generation tag, a compaction changes
|
|
66
|
-
// it and the web refetches);
|
|
67
|
-
// truncateMessageBlobs — cap oversized string bodies (tool outputs are the whale) to a preview + a
|
|
68
|
-
// `truncated:true` marker; the web expands one message on demand via the single-message face.
|
|
69
|
-
/** Window `messages` — `tail=N` (last N) or `before=<offset>&limit=N` ([max(0,before-N), before)).
|
|
70
|
-
* Returns the window + its {offset,total} so the web can page upward. */
|
|
71
43
|
export function windowMessages(messages, opts) {
|
|
72
44
|
const total = messages.length;
|
|
73
45
|
if (opts.before !== undefined) {
|
|
@@ -80,16 +52,7 @@ export function windowMessages(messages, opts) {
|
|
|
80
52
|
const offset = Math.max(0, total - tail);
|
|
81
53
|
return { messages: messages.slice(offset), window: { offset, total } };
|
|
82
54
|
}
|
|
83
|
-
/** 引擎注入块的闭集标签([1075]①,web [1069] 同款拆法):user wire 消息里每轮前置的
|
|
84
|
-
* <system-reminder>/<task-notification> 原文段。裁剪时这些段**优先整段丢弃**——blobCap 预算留给
|
|
85
|
-
* 用户真正说的话(clay 报障现场:注入块排在人话前把 4096 字节吃光,`[truncated …]` 后正文整段消失)。
|
|
86
|
-
* ⚠️ 开标签必须**属性容忍**([4974] 同形第三例):core ≥5.46(design/319)铸的 <system-reminder> 恒带
|
|
87
|
-
* `mark="<22位base64url>"` 属性,裸标签精确匹配对新 core 恒不命中=本剥离整条失效、[1075]① 报障病静默回归
|
|
88
|
-
* (常绿测试固件手写裸标签掩盖了它——红先格改用 core 真 mintSystemReminder 铸形)。剥离是裁剪优先级判定
|
|
89
|
-
* 不是信任判定,宽容属性无伪造面(带假属性的假 reminder 被剥=同样正确的让位)。 */
|
|
90
55
|
const INJECTED_BLOCK_RE = /<(system-reminder|task-notification)(?:\s[^>]*)?>[\s\S]*?<\/\1>\s*/g;
|
|
91
|
-
/** blobCap 下先剥注入块再量预算:剥后若已在预算内=保住全部人话(加 `injectedDropped:true` 注记);
|
|
92
|
-
* 仍超才截人话本身。剥离只在「原文本超预算」时发生——预算内的消息原样(含注入块)零变化,旧行为不动。 */
|
|
93
56
|
function capWithInjectionAwareness(s, capBytes) {
|
|
94
57
|
const total = Buffer.byteLength(s, "utf8");
|
|
95
58
|
if (total <= capBytes)
|
|
@@ -99,14 +62,8 @@ function capWithInjectionAwareness(s, capBytes) {
|
|
|
99
62
|
if (droppedAny && Buffer.byteLength(stripped, "utf8") <= capBytes) {
|
|
100
63
|
return { text: `${stripped}…[${total - Buffer.byteLength(stripped, "utf8")} bytes of injected context omitted]`, dropped: true };
|
|
101
64
|
}
|
|
102
|
-
// 剥后仍超(或无注入块):截 stripped——人话最后动,且从头保留(注入块已让位)。
|
|
103
65
|
return { text: `${stripped.slice(0, capBytes)}…[truncated ${total} bytes total]`, dropped: droppedAny };
|
|
104
66
|
}
|
|
105
|
-
/** Cap oversized STRING bodies inside a message (top-level `content` string, or `text`/`content`
|
|
106
|
-
* string fields of content parts — the shapes core emits; anything else passes through untouched). A capped
|
|
107
|
-
* message gains `truncated: true` (message level — the web renders an expand affordance and refetches that
|
|
108
|
-
* one message at full size). Pure/copy-on-write: uncapped messages keep their original reference.
|
|
109
|
-
* [1075]①:超预算时引擎注入块(INJECTED_BLOCK_RE 闭集)优先整段让位,用户正文最后截。 */
|
|
110
67
|
export function truncateMessageBlobs(messages, capBytes) {
|
|
111
68
|
const capStr = (s) => capWithInjectionAwareness(s, capBytes)?.text;
|
|
112
69
|
return messages.map((m) => {
|