@sema-agent/server 7.142.0 → 7.143.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 +442 -0
- package/USAGE.md +41 -24
- package/dist/adoption/plan.js +22 -0
- package/dist/approval-ask-audit-store.d.ts +7 -5
- package/dist/approval-ask-audit-store.js +12 -6
- package/dist/approval-ask-machine.d.ts +15 -7
- package/dist/approval-ask-machine.js +10 -4
- package/dist/approval-card.d.ts +124 -25
- package/dist/approval-card.js +103 -23
- package/dist/approval-queue-kind.d.ts +27 -0
- package/dist/approval-queue-kind.js +6 -0
- package/dist/approval-reconciler.d.ts +8 -8
- package/dist/approval-reconciler.js +3 -3
- package/dist/audit.js +4 -2
- package/dist/boot/budget-tracing.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +26 -41
- package/dist/boot/governance-seams.js +61 -28
- package/dist/boot/late-binds.d.ts +7 -4
- package/dist/boot/late-binds.js +3 -1
- package/dist/boot/leader.js +2 -4
- package/dist/boot/memory-consolidation.d.ts +0 -8
- package/dist/boot/memory-consolidation.js +3 -3
- package/dist/boot/reapers.js +5 -4
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +8 -8
- package/dist/boot/runner-deps.d.ts +10 -7
- package/dist/boot/runner-deps.js +5 -17
- package/dist/boot/shutdown.d.ts +9 -0
- package/dist/boot/shutdown.js +16 -2
- package/dist/boot/stage-02-web-admin-pull.js +3 -0
- package/dist/boot/stage-03-store-backend.d.ts +4 -1
- package/dist/boot/stage-03-store-backend.js +1 -2
- package/dist/boot/stage-04-stores.d.ts +5 -2
- package/dist/boot/stage-04-stores.js +3 -5
- package/dist/boot/stage-05-execution-env.d.ts +5 -2
- package/dist/boot/stage-06-runners.d.ts +5 -2
- package/dist/boot/stage-06-runners.js +1 -3
- package/dist/boot/stage-07-capability-layer.d.ts +5 -2
- package/dist/boot/stage-07-capability-layer.js +5 -1
- package/dist/boot/stage-08-reapers.d.ts +5 -2
- package/dist/boot/stage-09-leader.d.ts +5 -2
- package/dist/boot/stage-10-http-server.d.ts +5 -2
- package/dist/boot/stage-10-http-server.js +9 -1
- package/dist/boot/stores.d.ts +5 -2
- package/dist/boot/stores.js +7 -7
- package/dist/boot/web-admin.d.ts +5 -1
- package/dist/boot/web-admin.js +31 -19
- package/dist/capabilities/hands-lane.d.ts +9 -39
- package/dist/capabilities/hands-lane.js +8 -13
- package/dist/capabilities/mcp-probe.d.ts +3 -2
- package/dist/capabilities/scenarios.d.ts +11 -1
- package/dist/capabilities/scenarios.js +1 -0
- package/dist/capabilities/skills.d.ts +1 -17
- package/dist/capabilities/skills.js +0 -8
- package/dist/capability-facts.d.ts +8 -8
- package/dist/capability-facts.js +4 -4
- package/dist/card-void.d.ts +6 -9
- package/dist/config-catalog.js +1 -1
- package/dist/config-dir-trust.js +58 -29
- package/dist/config-invariants.d.ts +7 -1
- package/dist/config-invariants.js +7 -0
- package/dist/config-provider.d.ts +10 -0
- package/dist/config-provider.js +15 -4
- package/dist/config-root-migration.d.ts +13 -0
- package/dist/config-root-migration.js +99 -0
- package/dist/config-types.d.ts +5 -5
- package/dist/config.d.ts +4 -3
- package/dist/config.js +1 -1
- package/dist/delegated-session-owner.d.ts +5 -3
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-ws-protocol.d.ts +1 -0
- package/dist/device-ws-protocol.js +2 -1
- package/dist/env-model-table-gate.d.ts +53 -0
- package/dist/env-model-table-gate.js +53 -0
- package/dist/fleet/fleet-bus.d.ts +28 -52
- package/dist/fleet/fleet-bus.js +35 -100
- package/dist/fleet/fleet-reconciler.d.ts +4 -4
- package/dist/fleet/fleet-reconciler.js +1 -1
- package/dist/fleet/subagent-tail-bus.d.ts +14 -11
- package/dist/fleet/subagent-tail-bus.js +15 -28
- package/dist/hooks/hook-runner.js +28 -7
- package/dist/host-decision.d.ts +54 -20
- package/dist/host-decision.js +42 -14
- package/dist/http/admission.d.ts +1 -1
- package/dist/http/admission.js +7 -3
- package/dist/http/approval-sweeps.js +11 -8
- package/dist/http/caller-trust.d.ts +1 -1
- package/dist/http/durable-run-leg.js +2 -2
- package/dist/http/engine-refusal.d.ts +16 -0
- package/dist/http/engine-refusal.js +17 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
- package/dist/http/memory-compliance-failure-reply.js +4 -6
- package/dist/http/owner-access.d.ts +11 -0
- package/dist/http/owner-access.js +13 -0
- package/dist/http/resume-legs.d.ts +21 -7
- package/dist/http/resume-legs.js +71 -49
- package/dist/http/resume-restatement.d.ts +39 -0
- package/dist/http/resume-restatement.js +25 -0
- package/dist/http/route-ctx.d.ts +35 -10
- package/dist/http/route-ctx.js +1 -1
- package/dist/http/routes/approvals-assistant.d.ts +9 -1
- package/dist/http/routes/approvals-assistant.js +142 -31
- package/dist/http/routes/capabilities.js +11 -2
- package/dist/http/routes/memory-policy.d.ts +2 -2
- package/dist/http/routes/memory-policy.js +1 -1
- package/dist/http/routes/notify-wake.js +23 -25
- package/dist/http/routes/rules.js +48 -13
- package/dist/http/routes/runs.js +9 -8
- package/dist/http/routes/session-memory-sync.d.ts +34 -6
- package/dist/http/routes/session-memory-sync.js +27 -10
- package/dist/http/routes/session-sync.js +13 -20
- package/dist/http/routes/sessions.d.ts +2 -2
- package/dist/http/routes/sessions.js +22 -23
- package/dist/http/routes/tasks.js +27 -49
- package/dist/http/server.d.ts +13 -7
- package/dist/http/server.js +2 -2
- package/dist/http/stored-body-replay.js +15 -4
- package/dist/http/wire-types.d.ts +17 -7
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.d.ts +7 -1
- package/dist/leader/wire.js +6 -5
- package/dist/leg-facts.d.ts +37 -0
- package/dist/leg-facts.js +15 -0
- package/dist/memory-bundle-engine.d.ts +1 -4
- package/dist/memory-bundle-engine.js +2 -2
- package/dist/memory-operator-faces.d.ts +30 -66
- package/dist/memory-operator-faces.js +17 -88
- package/dist/memory-scope-stores.d.ts +38 -40
- package/dist/memory-scope-stores.js +51 -36
- package/dist/memory-scope.d.ts +0 -9
- package/dist/memory-scope.js +0 -9
- package/dist/memory-sync-client.js +14 -3
- package/dist/memory-sync.d.ts +20 -4
- package/dist/memory-sync.js +23 -4
- package/dist/model-plane-face.d.ts +13 -7
- package/dist/model-plane-face.js +2 -1
- package/dist/observability/fail-open.d.ts +20 -4
- package/dist/observability/fail-open.js +17 -4
- package/dist/observability/run-terminal.js +4 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
- package/dist/orchestration/workflow-completion-inbox.js +0 -12
- package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
- package/dist/orchestration/workflow-park-decision.d.ts +8 -3
- package/dist/orchestration/workflow-park-decision.js +5 -3
- package/dist/org-delivery-gate.d.ts +11 -0
- package/dist/org-delivery-gate.js +19 -1
- package/dist/parked-decide.d.ts +13 -9
- package/dist/parked-decide.js +7 -7
- package/dist/parked-revive-surface.js +1 -1
- package/dist/peer-inbound-face.d.ts +127 -0
- package/dist/peer-inbound-face.js +158 -0
- package/dist/permission-rule-list.d.ts +13 -9
- package/dist/permission-rule-list.js +7 -3
- package/dist/plugins/approval-ask-store-file.js +18 -6
- package/dist/plugins/approval-ask-store-memory.js +8 -6
- package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
- package/dist/plugins/approval-ask-store-sql.js +30 -11
- package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
- package/dist/plugins/checkpoint-store-sql.js +9 -7
- package/dist/plugins/file-history-store-sql.d.ts +24 -18
- package/dist/plugins/file-history-store-sql.js +24 -19
- package/dist/plugins/file-run-store.d.ts +29 -0
- package/dist/plugins/file-run-store.js +97 -8
- package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
- package/dist/plugins/imported-layer-root-retirement.js +96 -0
- package/dist/plugins/local-checkpoint-store.d.ts +1 -1
- package/dist/plugins/local-checkpoint-store.js +2 -2
- package/dist/plugins/local-session-store.d.ts +6 -2
- package/dist/plugins/local-session-store.js +8 -2
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
- package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
- package/dist/plugins/mailbox-store-sql.d.ts +12 -8
- package/dist/plugins/mailbox-store-sql.js +76 -7
- package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
- package/dist/plugins/memory-sync-store-pg.js +21 -0
- package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
- package/dist/plugins/memory-sync-store-tidb.js +13 -1
- package/dist/plugins/one-time-migrations.d.ts +32 -2
- package/dist/plugins/one-time-migrations.js +106 -3
- package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
- package/dist/plugins/org-delivery-quota-store.js +2 -12
- package/dist/plugins/park-commit-watch.d.ts +1 -1
- package/dist/plugins/permission-rule-store-file.d.ts +10 -1
- package/dist/plugins/permission-rule-store-file.js +15 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
- package/dist/plugins/permission-rule-store-sql.js +35 -2
- package/dist/plugins/remote-env-device.d.ts +8 -11
- package/dist/plugins/remote-env-device.js +6 -3
- package/dist/plugins/remote-env-k8s.d.ts +37 -24
- package/dist/plugins/remote-env-k8s.js +50 -17
- package/dist/plugins/retention-store-sql.d.ts +2 -2
- package/dist/plugins/sandbox-read-floor.d.ts +5 -7
- package/dist/plugins/sandbox-read-floor.js +2 -7
- package/dist/plugins/sql-errors.d.ts +10 -1
- package/dist/plugins/sql-errors.js +13 -0
- package/dist/plugins/store-backend.d.ts +23 -28
- package/dist/plugins/store-backend.js +9 -1
- package/dist/plugins/store-contracts.d.ts +29 -5
- package/dist/plugins/store-contracts.js +4 -1
- package/dist/plugins/task-attachment-store.d.ts +2 -2
- package/dist/plugins/task-attachment-store.js +37 -52
- package/dist/plugins/tool-result-store-sql.d.ts +9 -8
- package/dist/plugins/tool-result-store-sql.js +23 -2
- package/dist/plugins/wiring-section-codec.d.ts +32 -0
- package/dist/plugins/wiring-section-codec.js +55 -0
- package/dist/plugins/workspace-fill.d.ts +1 -1
- package/dist/rule-entry-list.d.ts +3 -2
- package/dist/rule-entry-list.js +5 -1
- package/dist/rules-consent.d.ts +28 -5
- package/dist/rules-consent.js +40 -28
- package/dist/run-local.js +9 -5
- package/dist/runs.d.ts +12 -8
- package/dist/runs.js +6 -7
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/server-secret-env.d.ts +4 -0
- package/dist/session-error-wire.d.ts +12 -0
- package/dist/session-error-wire.js +6 -0
- package/dist/session-incarnation.d.ts +1 -1
- package/dist/session-notify-door.d.ts +106 -0
- package/dist/session-notify-door.js +93 -0
- package/dist/session-sync.d.ts +9 -14
- package/dist/session-sync.js +5 -5
- package/dist/session-workspace.d.ts +3 -13
- package/dist/session-workspace.js +0 -9
- package/dist/standing-answer-grant.d.ts +5 -0
- package/dist/standing-answer-grant.js +10 -0
- package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
- package/dist/stores-sql/session-policy-store-sql.js +3 -13
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +11 -8
- package/dist/task-settings.js +31 -11
- package/dist/tool-approval.d.ts +52 -34
- package/dist/tool-approval.js +66 -57
- package/dist/trace/core-keyset-guard.d.ts +11 -8
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +3 -1
- package/dist/trace/exit-seat.d.ts +7 -1
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/ledger-sink.d.ts +2 -2
- package/dist/trace/model-segment.d.ts +55 -25
- package/dist/trace/model-segment.js +101 -33
- package/dist/trace/project.d.ts +7 -4
- package/dist/trace/project.js +6 -0
- package/dist/trace/redact.d.ts +61 -9
- package/dist/trace/redact.js +1490 -468
- package/dist/trace/self-minted-wire.d.ts +510 -38
- package/dist/trace/self-minted-wire.js +165 -17
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/verifier-pause-card.d.ts +33 -5
- package/dist/verifier-pause-card.js +15 -10
- package/dist/web-admin-client/apply-effective.d.ts +12 -16
- package/dist/web-admin-client/apply-effective.js +90 -10
- package/dist/workspace-space.d.ts +19 -3
- package/dist/workspace-space.js +8 -4
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
- package/package.json +3 -2
- package/wire-changes/server@7.143.0.json +1167 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ASK_TERMINAL, canAskTransition } from "../approval-ask-machine.js";
|
|
2
2
|
import { isExecutionLane } from "../execution-lane-caps.js";
|
|
3
3
|
import { APPROVAL_ENDING_CAUSES } from "../approval-deny-reasons.js";
|
|
4
|
-
import { askRowCarriesOwner, askRowOnCall,
|
|
4
|
+
import { askRowCarriesOwner, askRowOnCall, assertDecideSource, assertIdempotencyKeyShape, assertIntegralEpochMs, assertReapBounds, ApprovalAskReadAbortedError, claimTargetState, POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth, projectDecideReading } from "./approval-ask-store-sql.js";
|
|
5
5
|
function applyPatch(row, patch) {
|
|
6
6
|
if (patch.decision !== undefined)
|
|
7
7
|
row.decision = patch.decision;
|
|
@@ -58,7 +58,7 @@ export class InMemoryApprovalAskStore {
|
|
|
58
58
|
}
|
|
59
59
|
importRows(snapshot) {
|
|
60
60
|
for (const r of snapshot.asks ?? [])
|
|
61
|
-
this.asks.set(r.askId, { ...r, settledBy: r.settledBy ?? null });
|
|
61
|
+
this.asks.set(r.askId, { ...r, settledBy: r.settledBy ?? null, decidedVia: r.decidedVia ?? null });
|
|
62
62
|
for (const b of snapshot.batches ?? [])
|
|
63
63
|
this.batches.set(b.batchId, { ...b });
|
|
64
64
|
}
|
|
@@ -82,7 +82,7 @@ export class InMemoryApprovalAskStore {
|
|
|
82
82
|
const created = {
|
|
83
83
|
askId: row.askId,
|
|
84
84
|
taskId: row.taskId,
|
|
85
|
-
|
|
85
|
+
sourceSessionId: row.sourceSessionId,
|
|
86
86
|
sessionId: row.sessionId,
|
|
87
87
|
owner: row.owner ?? null,
|
|
88
88
|
executionLane: row.executionLane,
|
|
@@ -98,6 +98,7 @@ export class InMemoryApprovalAskStore {
|
|
|
98
98
|
decisionActor: null,
|
|
99
99
|
decisionNote: null,
|
|
100
100
|
settledBy: null,
|
|
101
|
+
decidedVia: null,
|
|
101
102
|
decidedAtMs: null,
|
|
102
103
|
endingCause: null,
|
|
103
104
|
parkTrigger: null,
|
|
@@ -132,7 +133,7 @@ export class InMemoryApprovalAskStore {
|
|
|
132
133
|
async decideAsk(askId, batchId, owner, decision) {
|
|
133
134
|
if (decision.idempotencyKey != null)
|
|
134
135
|
assertIdempotencyKeyShape(decision.idempotencyKey);
|
|
135
|
-
|
|
136
|
+
assertDecideSource(decision);
|
|
136
137
|
const reading = (row) => projectDecideReading(row ? { ...row } : null, owner);
|
|
137
138
|
const batch = this.batches.get(batchId);
|
|
138
139
|
if (!batch || batch.state !== "OPEN") {
|
|
@@ -160,6 +161,7 @@ export class InMemoryApprovalAskStore {
|
|
|
160
161
|
ask.decisionActor = decision.decisionActor ?? null;
|
|
161
162
|
ask.decisionNote = decision.decisionNote ?? null;
|
|
162
163
|
ask.settledBy = decision.settledBy ?? null;
|
|
164
|
+
ask.decidedVia = decision.decidedVia ?? null;
|
|
163
165
|
if (decision.updatedInput === undefined)
|
|
164
166
|
delete ask.updatedInput;
|
|
165
167
|
else
|
|
@@ -188,7 +190,7 @@ export class InMemoryApprovalAskStore {
|
|
|
188
190
|
break;
|
|
189
191
|
}
|
|
190
192
|
case "refuse": {
|
|
191
|
-
|
|
193
|
+
assertDecideSource(POLICY_REFUSAL_DECISION);
|
|
192
194
|
const to = claimTargetState(intent);
|
|
193
195
|
if (!canAskTransition("STREAM_PENDING", to))
|
|
194
196
|
throw new Error(`InMemoryApprovalAskStore.claimTerminal: illegal terminal claim STREAM_PENDING -> ${to} (askId=${askId})`);
|
|
@@ -390,7 +392,7 @@ export class InMemoryApprovalAskStore {
|
|
|
390
392
|
}
|
|
391
393
|
planReapSettledAsks(cutoffMs, limit, keepAnchors) {
|
|
392
394
|
assertReapBounds(cutoffMs, limit);
|
|
393
|
-
const anchored = (r) => keepAnchors.has(r.taskId) || keepAnchors.has(r.sessionId) || keepAnchors.has(r.
|
|
395
|
+
const anchored = (r) => keepAnchors.has(r.taskId) || keepAnchors.has(r.sessionId) || keepAnchors.has(r.sourceSessionId);
|
|
394
396
|
return [...this.asks.values()]
|
|
395
397
|
.filter((r) => ASK_TERMINAL.has(r.state) && !r.provisional && r.updatedAtMs < cutoffMs && !anchored(r))
|
|
396
398
|
.sort((a, b) => a.updatedAtMs - b.updatedAtMs || (a.askId < b.askId ? -1 : a.askId > b.askId ? 1 : 0))
|
|
@@ -42,6 +42,7 @@ import type { PgQueryFn } from "./pg-query.js";
|
|
|
42
42
|
import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
|
|
43
43
|
import { type AskState, type BatchState } from "../approval-ask-machine.js";
|
|
44
44
|
import type { HostSettledBy } from "../host-decision.js";
|
|
45
|
+
import { type DecidedVia } from "@sema-agent/core";
|
|
45
46
|
import { type AskRowEndingCause, type SettleCause, type ParkTriggerCause } from "../approval-deny-reasons.js";
|
|
46
47
|
import { type IndexSpec } from "./ensure-index.js";
|
|
47
48
|
import { type ExecutionLane } from "../execution-lane-caps.js";
|
|
@@ -49,7 +50,7 @@ export type AskDecision = "approve" | "deny";
|
|
|
49
50
|
export interface AskRow {
|
|
50
51
|
askId: string;
|
|
51
52
|
taskId: string;
|
|
52
|
-
|
|
53
|
+
sourceSessionId: string;
|
|
53
54
|
sessionId: string;
|
|
54
55
|
/** 租户 scope——哨兵归一在消费层(车2+),店里存原值(设计定稿 §3)。 */
|
|
55
56
|
owner: string | null;
|
|
@@ -69,7 +70,7 @@ export interface AskRow {
|
|
|
69
70
|
* 与下面 PARKED 坐标里的 `gateBoundInputHash` 是**两件不同的东西**,别混:
|
|
70
71
|
* - `boundInputHash`(本列)= ask **铸行时**的入参摘要,一次写定永不改;对账收敛器判据 1 用它跟
|
|
71
72
|
* checkpoint 行的同名列做**硬相等**——它是**身份三元组之外的第二道等式**,身份本身是
|
|
72
|
-
* (`
|
|
73
|
+
* (`sourceSessionId`, `toolCallId`, 因果下界)三维([ref] 件1 换轴,判据属主 = `approval-reconciler.ts`
|
|
73
74
|
* 的 `classifyGateMatch`;本注上一版写的「identity 四元组」是换轴前的旧口径,`sessionId` 从来不是
|
|
74
75
|
* 内存判据维)。任一侧缺席 ⇒ 判据 1 **不命中**;归因看走到哪一层:身份先判(候选集非空却没有同身份
|
|
75
76
|
* 的一条 ⇒ `identity_miss`),身份这层还够得着时缺席才记 `single_mint`(禁「能取到时才比」的可选谓词
|
|
@@ -90,7 +91,7 @@ export interface AskRow {
|
|
|
90
91
|
/**
|
|
91
92
|
* 🔴 [ref]([ref] 的持久半场)—— 这条决议的**结算来源**:`null` = 一个人答的(= 缺席律,core 对
|
|
92
93
|
* 不带这一格的 outcome 逐字按 `"human"` 读),非 null = 宿主自报的那个词([ref] 起写进来的只有决定体出处位
|
|
93
|
-
* `decidedBy:"policy"` 经 `
|
|
94
|
+
* `decidedBy:"policy"` 经 `liveHostSource` 映射来的 `"policy"`,allow 与 deny 两向都写;词表属主恒是 core —— 见 {@link HostSettledBy})。
|
|
94
95
|
*
|
|
95
96
|
* **为什么必须是行上的一格**(病灶,7.93.3 合并树 codex ① 验真):`settledBy` 此前只到达**当次**回决的
|
|
96
97
|
* 即时回调,行上不留痕 ⇒ 重放 / 再入 / 同 askId 的兄弟等待者 / 歧义提交恢复读回的是裸 `false`,
|
|
@@ -113,6 +114,14 @@ export interface AskRow {
|
|
|
113
114
|
* 三态收成两态是刻意的:`"human"` 与缺席在 core 那边逐字同义,故店只存「非人」的那些词,`null` 即人。
|
|
114
115
|
*/
|
|
115
116
|
settledBy: HostSettledBy | null;
|
|
117
|
+
/**
|
|
118
|
+
* core 7.61.0([ref])—— `settledBy = "policy"` 旁的**机器座位**:哪一个机器座位替这台部署答的(core `DECIDED_VIA`:`"policy"` 部署自己的策略 /
|
|
119
|
+
* `"permission_request_hook"` 一只 PermissionRequest 钩子)。`null` = 没陈述(旧行同读)= 部署自己的策略。它决定模型读哪一句拒句
|
|
120
|
+
* (「这一跑没有交互审批通道」vs「一只 PermissionRequest 钩子拒了」),所以与 {@link settledBy} 同一条理由住在行上、与决议同一条 UPDATE 落列:
|
|
121
|
+
* 活卡腿的结算输入只从行读,不在行上 = 重放 / 兄弟 / 再入 / 歧义恢复 / 迟到成功 / 跨副本广播六条路把钩子的拒读回成部署策略的拒。
|
|
122
|
+
* 只在 `settledBy = "policy"` 旁(core 同一条律;店门 {@link assertDecideSource} 守)。
|
|
123
|
+
*/
|
|
124
|
+
decidedVia: DecidedVia | null;
|
|
116
125
|
/**
|
|
117
126
|
* [ref]([ref] / [ref] 收口):人批准时带的 **ctrl+g 编辑实参**(wire `updatedInput`),与决议**同一条
|
|
118
127
|
* UPDATE** 原子落列 `updated_input`。语义三态,读法**严格**:
|
|
@@ -186,7 +195,7 @@ export interface AskRow {
|
|
|
186
195
|
export interface NewAskRow {
|
|
187
196
|
askId: string;
|
|
188
197
|
taskId: string;
|
|
189
|
-
|
|
198
|
+
sourceSessionId: string;
|
|
190
199
|
sessionId: string;
|
|
191
200
|
owner?: string | null;
|
|
192
201
|
/** 出处的执行车道(必填;语义见 {@link AskRow.executionLane})。 */
|
|
@@ -250,10 +259,12 @@ export interface DecideAskInput {
|
|
|
250
259
|
* 🔴 [ref]:本次回决的**结算来源**,与决议同一条 UPDATE 落列 `settled_by`(语义/两态读法/为什么是
|
|
251
260
|
* 自己一列,全在 {@link AskRow.settledBy})。缺席 ⇒ 列 NULL ⇒ 读回是「一个人答的」。
|
|
252
261
|
*
|
|
253
|
-
* 店**不判词**(词表属主是 core):写什么存什么。店只判**形** —— `
|
|
262
|
+
* 店**不判词**(词表属主是 core):写什么存什么。店只判**形** —— `assertDecideSource` 的那一条
|
|
254
263
|
* (长度 ≤ 列宽;[ref] 起 approve 行也可以带来源位),理由见那只门的顶注;三 twin 同判。
|
|
255
264
|
*/
|
|
256
265
|
settledBy?: HostSettledBy;
|
|
266
|
+
/** core 7.61.0([ref]):`settledBy:"policy"` 旁的机器座位,与决议同一条 UPDATE 落列 `decided_via`(语义见 {@link AskRow.decidedVia});缺席 ⇒ 列 NULL。 */
|
|
267
|
+
decidedVia?: DecidedVia;
|
|
257
268
|
/** [ref]:ctrl+g 编辑实参,与决议同一条 UPDATE 落列(语义三态见 {@link AskRow.updatedInput})。
|
|
258
269
|
* 缺席 / `undefined` ⇒ 列 NULL(没带编辑);**`null` 是值**(编辑成 null),照存 JSON `"null"`。
|
|
259
270
|
* 调用方纪律:deny 不传(店不替调用方判决议方向,写什么存什么)。 */
|
|
@@ -293,7 +304,7 @@ export declare function assertReapBounds(cutoffMs: number, limit: number): void;
|
|
|
293
304
|
export declare function assertIdempotencyKeyShape(key: string): void;
|
|
294
305
|
/** `settled_by` 列宽 = 这一格的唯一尺(两方言 DDL 与下面的形门读同一个常量,不手抄第二份)。 */
|
|
295
306
|
export declare const MAX_SETTLED_BY_CHARS = 16;
|
|
296
|
-
/** `settled_by` 这一列装得下这个词吗(1..{@link MAX_SETTLED_BY_CHARS})—— 店门口形门({@link
|
|
307
|
+
/** `settled_by` 这一列装得下这个词吗(1..{@link MAX_SETTLED_BY_CHARS})—— 店门口形门({@link assertDecideSource})的**谓词本身**。
|
|
297
308
|
* [ref] 起决定口不再读它:wire 上的出处位是闭集(`host-decision.ts` 的 `APPROVAL_DECIDED_BY`),映射到这一列的词恒在尺内。 */
|
|
298
309
|
export declare function settledByFitsColumn(v: string): boolean;
|
|
299
310
|
/**
|
|
@@ -307,9 +318,11 @@ export declare function settledByFitsColumn(v: string): boolean;
|
|
|
307
318
|
* 🔴 [ref]:此前的第二条「只与 `deny` 同行」**删了** —— 它的前提(「只有人能批准一个要执行的动作,core 对 `{allow:true,settledBy}`
|
|
308
319
|
* 一律 fail-closed 拒」)自 core 7.39.0 起不成立:策略的放行铸 `policy_allowed`,决定体的出处位 `decidedBy:"policy"` 在 allow 与 deny
|
|
309
320
|
* 上都落这一列,approve 行带来源位是一条合法的行(读回铸点 `outcomeOfDecidedRow` 两向都读它)。今天写这一列的只有两条路:决定口经
|
|
310
|
-
* `
|
|
321
|
+
* `liveHostSource` 映射来的 `"policy"`,与 `claimTerminal(refuse)` 的 {@link POLICY_REFUSAL_DECISION} —— 都是 core 在两向上都收的词。
|
|
311
322
|
*/
|
|
312
|
-
export declare function
|
|
323
|
+
export declare function assertDecideSource(decision: Pick<DecideAskInput, "settledBy" | "decidedVia">): void;
|
|
324
|
+
/** `decided_via` 列宽 = 这一格的唯一尺(两方言 DDL 读同一个常量)。core `DECIDED_VIA` 今天最长 23 字符(`permission_request_hook`)。 */
|
|
325
|
+
export declare const MAX_DECIDED_VIA_CHARS = 32;
|
|
313
326
|
/**
|
|
314
327
|
* [ref] 车3 刀 3b(§14.2 遗留记账):两条**重放读口**被调用方的 deadline 掐断时抛的具名错误。
|
|
315
328
|
*
|
|
@@ -353,7 +366,7 @@ export interface AskTerminalClaimOpts {
|
|
|
353
366
|
/**
|
|
354
367
|
* `refuse` 意图落行的决议(NP-2)—— **部署政策拒**:deny + 结算来源 `policy`。来源词的型取 core 的 `HostSettledBy`
|
|
355
368
|
* (core 改词 / 退词 ⇒ 这里 tsc 红);协调器交给 core 的那一只(`tool-approval.ts` 的 `UnattendedPolicyRefusal`)读的是
|
|
356
|
-
* **同一个**常量,于是「行上记的来源」与「首判交出去的来源」在构造上是一个词。三 twin 落行前都过 {@link
|
|
369
|
+
* **同一个**常量,于是「行上记的来源」与「首判交出去的来源」在构造上是一个词。三 twin 落行前都过 {@link assertDecideSource}
|
|
357
370
|
* (settled_by 的唯一形门:长度 ≤ 列宽)。
|
|
358
371
|
*/
|
|
359
372
|
export declare const POLICY_REFUSAL_DECISION: {
|
|
@@ -391,6 +404,8 @@ export type AskTerminalClaimCurrent = {
|
|
|
391
404
|
/** [ref]:赢家决议的**结算来源**(语义与两态读法见 {@link AskRow.settledBy})。**必填**(不是可选)——
|
|
392
405
|
* 可选会让投影漏挑这一列时静默编译过,而漏挑的后果恰是本批要修的病:CAS 输家把赢家的策略拒读成人拒。 */
|
|
393
406
|
settledBy: HostSettledBy | null;
|
|
407
|
+
/** core 7.61.0([ref]):赢家决议的机器座位(语义见 {@link AskRow.decidedVia});必填,理由与 `settledBy` 同。 */
|
|
408
|
+
decidedVia: DecidedVia | null;
|
|
394
409
|
/** 行上记着的终态成因(`ending_cause` 列,经唯一读口 `askRowEndingCause` 读出)。**只在读得出时在场**(没写 / 与 state 不配 ⇒ 缺席;DECIDED 的败方读数逐字不变)。
|
|
395
410
|
* CAS 输家据它按**行上那个成因**结算(而不是把别人撤的卡读成一次拒绝);投影的唯一函数是 {@link projectAskTerminalClaimTruth},三 twin 不各自挑列,原始列值不出店层。 */
|
|
396
411
|
endingCause?: AskRowEndingCause;
|
|
@@ -451,7 +466,7 @@ export interface BatchRow {
|
|
|
451
466
|
}
|
|
452
467
|
/** design 定稿 §4 的持久层接口。协调器接线(车2)、恢复扫描消费(车5)不在本车范围——本车只落这些方法。 */
|
|
453
468
|
/** 一次调用在一条腿上的坐标({@link ApprovalAskStore.listByCall} 的四列;型从 {@link AskRow} 派生,不手抄)。 */
|
|
454
|
-
export type AskCallCoordinate = Pick<AskRow, "taskId" | "legKey" | "toolCallId" | "
|
|
469
|
+
export type AskCallCoordinate = Pick<AskRow, "taskId" | "legKey" | "toolCallId" | "sourceSessionId">;
|
|
455
470
|
/** 「这一行在这次调用上」的唯一判据(四列逐字相等):InMemory twin 的筛子与 SQL twin 的出口对齐读同一只。 */
|
|
456
471
|
export declare function askRowOnCall(row: AskRow, call: AskCallCoordinate): boolean;
|
|
457
472
|
/**
|
|
@@ -535,12 +550,12 @@ export interface ApprovalAskStore {
|
|
|
535
550
|
* 后续件。 */
|
|
536
551
|
listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
|
|
537
552
|
/**
|
|
538
|
-
* 🔴 [ref] / SN-C1:**这次调用在这条腿上的全部行** —— 任意状态、任意 ask 身份(同一调用不同类集 = 不同 askId 的几只问、升级前按旧派生
|
|
553
|
+
* 🔴 [ref] / SN-C1:**这次调用在这条腿上的全部行** —— 任意状态、任意 ask 身份(同一调用不同类集 / 不同展示动作 = 不同 askId 的几只问、升级前按旧派生
|
|
539
554
|
* 写的行,都在其中)。坐标 = {@link AskCallCoordinate} 四列**精确**匹配;序 = `created_at_ms ASC, ask_id ASC`(确定性)。
|
|
540
555
|
*
|
|
541
556
|
* 唯一消费者 = 会话授权短路的争议判据(`tool-approval.ts` `sessionGrantUncontestedByRow`):「这次调用在腿内有行 ⇒ 一揽子放行不得代答」。
|
|
542
557
|
* 为什么按调用而不是按 askId 点读:争议的真源是**调用**(人对这次调用说过什么 / 这次调用还有一张卡挂着),ask 身份只是其中一问的主键;
|
|
543
|
-
* 身份可以加轴([ref]
|
|
558
|
+
* 身份可以加轴([ref] 加了类集,7.143.0 加了展示的动作),争议判据不得依赖身份 —— 按身份点读在身份加轴的那一刻就把同调用的别的问、旧派生的行读成「不存在」。
|
|
544
559
|
*
|
|
545
560
|
* 三 twin 同答:SQL twin 读后按四列逐字再滤一遍 —— MySQL 协议的 VARCHAR 等值带 PAD SPACE(`'tc'` = `'tc '`),PG(`COLLATE "C"`)与
|
|
546
561
|
* InMemory 严格比较;不在入口拒(调用 id 是引擎铸的原值,首尾空白不归本店判),在出口对齐。索引:走 `idx_ask_task_state`
|
|
@@ -568,7 +583,7 @@ export interface ApprovalAskStore {
|
|
|
568
583
|
/**
|
|
569
584
|
* **审批账本留存律的问账半场之一:问行**(律、节律与「还能动的 run」一处成文在 `approval-ask-retention.ts`;维护 tick 是唯一调用方)。
|
|
570
585
|
* 删至多 `limit` 只**结了**的问行 —— `state ∈ ASK_TERMINAL` ∧ `provisional = false`(provisional 行还能被 {@link resolveProvisional} 改判,没结)
|
|
571
|
-
* ∧ `updatedAtMs < cutoffMs` ∧ 行挂靠的三格(`taskId` / `sessionId` / `
|
|
586
|
+
* ∧ `updatedAtMs < cutoffMs` ∧ 行挂靠的三格(`taskId` / `sessionId` / `sourceSessionId`)都不在 `keepAnchors`(还能动的东西的挂靠集:
|
|
572
587
|
* 它还会回读这些行 —— 会话授权的争议判据 {@link listByCall} 把「这次调用在腿内没有行」读成可以代答)。非终态行多老都不删(它们归对账器
|
|
573
588
|
* 与起服收敛)。只删不改。
|
|
574
589
|
* 一条语句:要么删了并交回删掉的行数,要么一行没删就抛。三形同答(InMemory = 内核;File 形把这次调用照常落成一条账本动词,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
2
2
|
import { ASK_TERMINAL, canAskTransition } from "../approval-ask-machine.js";
|
|
3
|
+
import { DECIDED_VIA } from "@sema-agent/core";
|
|
3
4
|
import { APPROVAL_ENDING_CAUSES, askRowEndingCause, askRowParkTrigger } from "../approval-deny-reasons.js";
|
|
4
5
|
import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
|
|
5
6
|
import { describeProbedTableDrift } from "./schema-drift-sentence.js";
|
|
@@ -25,7 +26,8 @@ export const MAX_SETTLED_BY_CHARS = 16;
|
|
|
25
26
|
export function settledByFitsColumn(v) {
|
|
26
27
|
return v.length > 0 && v.length <= MAX_SETTLED_BY_CHARS;
|
|
27
28
|
}
|
|
28
|
-
export function
|
|
29
|
+
export function assertDecideSource(decision) {
|
|
30
|
+
assertDecidedViaBesidePolicy(decision);
|
|
29
31
|
const v = decision.settledBy;
|
|
30
32
|
if (v === undefined)
|
|
31
33
|
return;
|
|
@@ -33,6 +35,15 @@ export function assertDecideSettledBy(decision) {
|
|
|
33
35
|
throw new Error(`invalid settledBy: the settlement source is one word from the engine's closed set and lands in a VARCHAR(${MAX_SETTLED_BY_CHARS}) column — must be 1-${MAX_SETTLED_BY_CHARS} chars (got ${JSON.stringify(v.slice(0, 32))}${v.length > 32 ? "…" : ""}, length ${v.length})`);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
38
|
+
export const MAX_DECIDED_VIA_CHARS = 32;
|
|
39
|
+
function assertDecidedViaBesidePolicy(decision) {
|
|
40
|
+
const via = decision.decidedVia;
|
|
41
|
+
if (via === undefined)
|
|
42
|
+
return;
|
|
43
|
+
if (!DECIDED_VIA.some((w) => w === via) || via.length > MAX_DECIDED_VIA_CHARS || decision.settledBy !== "policy") {
|
|
44
|
+
throw new Error(`invalid decidedVia: the machine seat is one of ${DECIDED_VIA.map((w) => `"${w}"`).join(" / ")} and is stated only beside settledBy "policy" (got ${JSON.stringify(String(via).slice(0, 32))} beside ${JSON.stringify(decision.settledBy ?? null)})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
36
47
|
export class ApprovalAskReadAbortedError extends Error {
|
|
37
48
|
constructor(where) {
|
|
38
49
|
super(`approval-ask read ${where} aborted by caller`);
|
|
@@ -98,12 +109,13 @@ export function projectAskTerminalClaimTruth(row) {
|
|
|
98
109
|
decision: row.decision,
|
|
99
110
|
...(row.updatedInput !== undefined ? { updatedInput: row.updatedInput } : {}),
|
|
100
111
|
settledBy: row.settledBy,
|
|
112
|
+
decidedVia: row.decidedVia,
|
|
101
113
|
...(endingCause !== undefined ? { endingCause } : {}),
|
|
102
114
|
...(parkTrigger !== undefined ? { parkTrigger } : {}),
|
|
103
115
|
};
|
|
104
116
|
}
|
|
105
117
|
export function askRowOnCall(row, call) {
|
|
106
|
-
return row.taskId === call.taskId && row.legKey === call.legKey && row.toolCallId === call.toolCallId && row.
|
|
118
|
+
return row.taskId === call.taskId && row.legKey === call.legKey && row.toolCallId === call.toolCallId && row.sourceSessionId === call.sourceSessionId;
|
|
107
119
|
}
|
|
108
120
|
export function askOwnerAdmits(owner, principal) {
|
|
109
121
|
return owner === null || owner === principal;
|
|
@@ -179,9 +191,12 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
|
179
191
|
-- settled_by(S-558 / #1016 持久半场):这条决议的结算来源词(NULL = 人答的)。语义、两态读法与
|
|
180
192
|
-- 「为什么是自己一列而不是塞进 decision_actor」的全文见 AskRow.settledBy 顶注。
|
|
181
193
|
-- VARCHAR(16):词表属主是 core(AskOutcome.settledBy,今天最长 7 字符 "timeout"),宽度与同表
|
|
182
|
-
-- decision 同尺同理由 —— 闭集词列,不是自由文本。截断面由入店形门堵死(
|
|
194
|
+
-- decision 同尺同理由 —— 闭集词列,不是自由文本。截断面由入店形门堵死(assertDecideSource),
|
|
183
195
|
-- 不靠「列够宽就不会截」这种运气(MySQL 非严格模式静默截、PG 直接报错 = 两方言不同答)。
|
|
184
196
|
settled_by VARCHAR(16) NULL,
|
|
197
|
+
-- decided_via(core 7.61.0 #3169):settled_by = 'policy' 旁的机器座位词(NULL = 没陈述 = 部署自己的策略;旧行同读)。
|
|
198
|
+
-- 与 settled_by 同一条 UPDATE 落列、同一条理由住在行上(语义全文见 AskRow.decidedVia)。闭集词列,宽度 = MAX_DECIDED_VIA_CHARS。
|
|
199
|
+
decided_via VARCHAR(${MAX_DECIDED_VIA_CHARS}) NULL,
|
|
185
200
|
-- updated_input(S-26 / #304,A-054.15 收口):人批准时带的 ctrl+g 编辑实参,JSON 文本,与决议同一条
|
|
186
201
|
-- UPDATE 原子落列;NULL = 没带编辑(与 JSON "null" = 编辑成 null 分家)。LONGTEXT 照 card_json 先例:
|
|
187
202
|
-- 编辑实参是整只工具入参,VARCHAR/TEXT 会造静默截断面。语义三态与读法见 AskRow.updatedInput 顶注。
|
|
@@ -268,6 +283,8 @@ export async function ensurePgApprovalAskSchema(q) {
|
|
|
268
283
|
decision_note TEXT COLLATE "C",
|
|
269
284
|
-- settled_by(S-558)—— MySQL 孪生逐字同形(宽度/语义/形门的理由写在那侧的行内注)。
|
|
270
285
|
settled_by VARCHAR(16) COLLATE "C",
|
|
286
|
+
-- decided_via(#3169)—— MySQL 孪生逐字同形。
|
|
287
|
+
decided_via VARCHAR(${MAX_DECIDED_VIA_CHARS}) COLLATE "C",
|
|
271
288
|
-- updated_input(S-26 / #304)—— MySQL 孪生逐字同形(语义/类型理由见那侧行内注;PG TEXT 无长度上限)。
|
|
272
289
|
updated_input TEXT COLLATE "C",
|
|
273
290
|
decided_at_ms BIGINT,
|
|
@@ -317,7 +334,7 @@ function askCols(dialect) {
|
|
|
317
334
|
"ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state",
|
|
318
335
|
"provisional, rev, decision",
|
|
319
336
|
jsonColumnText(dialect, "decision_actor"),
|
|
320
|
-
"decision_note, settled_by",
|
|
337
|
+
"decision_note, settled_by, decided_via",
|
|
321
338
|
jsonColumnText(dialect, "updated_input"),
|
|
322
339
|
"decided_at_ms, ending_cause, park_trigger, gate_token",
|
|
323
340
|
"gate_bound_call_id, gate_bound_input_hash, idempotency_key",
|
|
@@ -349,7 +366,7 @@ function mapAskRowBody(r, askId) {
|
|
|
349
366
|
return {
|
|
350
367
|
askId: String(r.ask_id),
|
|
351
368
|
taskId: String(r.task_id),
|
|
352
|
-
|
|
369
|
+
sourceSessionId: String(r.source_task_id),
|
|
353
370
|
sessionId: String(r.session_id),
|
|
354
371
|
owner: strOrNull(r.owner),
|
|
355
372
|
executionLane: executionLaneColumnOf(r.execution_lane, askId),
|
|
@@ -365,6 +382,7 @@ function mapAskRowBody(r, askId) {
|
|
|
365
382
|
decisionActor: r.decision_actor == null ? null : decodeJsonColumn(r.decision_actor, readJsonObjectColumn, `${APPROVAL_ASK_TABLE}.decision_actor for ask ${askId}`),
|
|
366
383
|
decisionNote: strOrNull(r.decision_note),
|
|
367
384
|
settledBy: strOrNull(r.settled_by),
|
|
385
|
+
decidedVia: strOrNull(r.decided_via),
|
|
368
386
|
...(r.updated_input == null ? {} : { updatedInput: decodeJsonColumn(r.updated_input, readTriStateJsonValueColumn, `${APPROVAL_ASK_TABLE}.updated_input for ask ${askId}`) }),
|
|
369
387
|
decidedAtMs: numOrNull(r.decided_at_ms),
|
|
370
388
|
endingCause: strOrNull(r.ending_cause),
|
|
@@ -462,7 +480,7 @@ export class SqlApprovalAskStore {
|
|
|
462
480
|
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $12, $13, $14, $15, $16, $17, $18) ON CONFLICT (ask_id) DO NOTHING"), [
|
|
463
481
|
row.askId,
|
|
464
482
|
row.taskId,
|
|
465
|
-
row.
|
|
483
|
+
row.sourceSessionId,
|
|
466
484
|
row.sessionId,
|
|
467
485
|
row.owner ?? null,
|
|
468
486
|
row.executionLane,
|
|
@@ -516,10 +534,10 @@ export class SqlApprovalAskStore {
|
|
|
516
534
|
async decideAsk(askId, batchId, owner, decision) {
|
|
517
535
|
if (decision.idempotencyKey != null)
|
|
518
536
|
assertIdempotencyKeyShape(decision.idempotencyKey);
|
|
519
|
-
|
|
537
|
+
assertDecideSource(decision);
|
|
520
538
|
const now = Date.now();
|
|
521
539
|
const dialect = this.db.dialect;
|
|
522
|
-
const ownerWhere = decideOwnerWhere(dialect, owner,
|
|
540
|
+
const ownerWhere = decideOwnerWhere(dialect, owner, 12);
|
|
523
541
|
const conn = await this.db.connect();
|
|
524
542
|
try {
|
|
525
543
|
await conn.begin();
|
|
@@ -530,11 +548,12 @@ export class SqlApprovalAskStore {
|
|
|
530
548
|
}
|
|
531
549
|
let askRes;
|
|
532
550
|
try {
|
|
533
|
-
askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)}, settled_by=${ph(dialect, 4)},
|
|
551
|
+
askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=${ph(dialect, 1)}, decision_actor=${ph(dialect, 2)}, decision_note=${ph(dialect, 3)}, settled_by=${ph(dialect, 4)}, decided_via=${ph(dialect, 5)}, updated_input=${ph(dialect, 6)}, decided_at_ms=${ph(dialect, 7)}, idempotency_key=${ph(dialect, 8)}, rev=rev+1, updated_at_ms=${ph(dialect, 9)} WHERE ${pendingClaimWhere(dialect, 10)} AND ${ownerWhere.sql}`, [
|
|
534
552
|
decision.decision,
|
|
535
553
|
decision.decisionActor == null ? null : this.json(decision.decisionActor),
|
|
536
554
|
decision.decisionNote ?? null,
|
|
537
555
|
decision.settledBy ?? null,
|
|
556
|
+
decision.decidedVia ?? null,
|
|
538
557
|
decision.updatedInput === undefined ? null : this.json(decision.updatedInput),
|
|
539
558
|
now,
|
|
540
559
|
decision.idempotencyKey ?? null,
|
|
@@ -579,7 +598,7 @@ export class SqlApprovalAskStore {
|
|
|
579
598
|
throw new Error(`approval-ask-store.claimTerminal: illegal terminal claim STREAM_PENDING -> ${to} (askId=${askId})`);
|
|
580
599
|
}
|
|
581
600
|
if (intent === "refuse")
|
|
582
|
-
|
|
601
|
+
assertDecideSource(POLICY_REFUSAL_DECISION);
|
|
583
602
|
const now = Date.now();
|
|
584
603
|
const dialect = this.db.dialect;
|
|
585
604
|
const conn = await this.db.connect();
|
|
@@ -752,7 +771,7 @@ export class SqlApprovalAskStore {
|
|
|
752
771
|
return rows.map(mapAskRow);
|
|
753
772
|
}
|
|
754
773
|
async listByCall(call) {
|
|
755
|
-
const { rows } = await this.db.query(this.q(`SELECT ${askCols("mysql")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.
|
|
774
|
+
const { rows } = await this.db.query(this.q(`SELECT ${askCols("mysql")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.sourceSessionId]);
|
|
756
775
|
return rows.map(mapAskRow).filter((r) => askRowOnCall(r, call));
|
|
757
776
|
}
|
|
758
777
|
async listParkingBySession(sessionId) {
|
|
@@ -343,7 +343,7 @@ export interface CheckpointAskCandidate {
|
|
|
343
343
|
status: string;
|
|
344
344
|
createdAtMs: number;
|
|
345
345
|
/**
|
|
346
|
-
* 🔴 和解三元组的第一维([ref] 件1,黑板 [ref]③①)—— checkpoint 自己带的 `
|
|
346
|
+
* 🔴 和解三元组的第一维([ref] 件1,黑板 [ref]③①)—— checkpoint 自己带的 `sourceSessionId`(core 在
|
|
347
347
|
* suspendAsk 铸行时恒填该腿的 `sessionId`)。**没有对应的列**,只能从 blob 窄读;读不出/缺席 = null。
|
|
348
348
|
*
|
|
349
349
|
* 为什么必须进候选集:读口按 `(scope, session_id, tool_call_id)` 查,而委派子代那条腿的 ask 落行时
|
|
@@ -351,7 +351,7 @@ export interface CheckpointAskCandidate {
|
|
|
351
351
|
* (toolCallId, hash) 两维,一条根腿的 park 会被一只子代的 ask 认领(摘要相同的两次同 args 调用完全
|
|
352
352
|
* 正常)。三元组全同才算同一件事。
|
|
353
353
|
*/
|
|
354
|
-
|
|
354
|
+
sourceSessionId: string | null;
|
|
355
355
|
/** 该 park 绑定的 tool call(= `pendingAction.toolCallId`)。`unparseable` 行为 null。 */
|
|
356
356
|
boundCallId: string | null;
|
|
357
357
|
/** 服务端铸的入参摘要——判据 1 的第二道硬等式(与 `approval_ask.bound_input_hash` 相等才算命中)。 */
|
|
@@ -444,7 +444,7 @@ export declare function pendingActionToolCallId(body: unknown): {
|
|
|
444
444
|
toolCallId: string | null;
|
|
445
445
|
boundInputHash: string | null;
|
|
446
446
|
kind: string | null;
|
|
447
|
-
|
|
447
|
+
sourceSessionId: string | null;
|
|
448
448
|
} | {
|
|
449
449
|
readable: false;
|
|
450
450
|
};
|
|
@@ -781,7 +781,7 @@ export declare class SqlCheckpointStore implements CheckpointStore, KindlessBatc
|
|
|
781
781
|
* 🔴 **没有「列命中即零解析」的快路径**(原注写过,已作废,别照它优化):函数体对**每一行**无条件解
|
|
782
782
|
* blob,原因有二 ——(a) 前向兼容门与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
|
|
783
783
|
* 列长得对不代表 blob 读得出,放行一条读不出的行去 `bindBatch` 会把 ask 钉成 PARKED + 一张本进程读不出
|
|
784
|
-
* 的 resume 坐标,而 PARKED 不可回滚);(b) `[ref]` 之后 blob 顶层的 `
|
|
784
|
+
* 的 resume 坐标,而 PARKED 不可回滚);(b) `[ref]` 之后 blob 顶层的 `sourceSessionId` 是和解三元组的第一
|
|
785
785
|
* 维,列命中行结构上也必须解 blob 才拿得到它。
|
|
786
786
|
*/
|
|
787
787
|
findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
|
|
@@ -239,12 +239,14 @@ export function approvalDecisionOfWinner(winner, boundCallId) {
|
|
|
239
239
|
export function pendingActionToolCallId(body) {
|
|
240
240
|
if (typeof body !== "object" || body === null)
|
|
241
241
|
return { readable: false };
|
|
242
|
-
|
|
242
|
+
if ("sourceTaskId" in body)
|
|
243
|
+
return { readable: false };
|
|
244
|
+
const rawSource = "sourceSessionId" in body ? body.sourceSessionId : null;
|
|
243
245
|
if (rawSource !== null && rawSource !== undefined && typeof rawSource !== "string")
|
|
244
246
|
return { readable: false };
|
|
245
|
-
const
|
|
247
|
+
const sourceSessionId = typeof rawSource === "string" ? rawSource : null;
|
|
246
248
|
if (!("pendingAction" in body))
|
|
247
|
-
return { readable: true, toolCallId: null, boundInputHash: null, kind: null,
|
|
249
|
+
return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceSessionId };
|
|
248
250
|
const pa = body.pendingAction;
|
|
249
251
|
if (typeof pa !== "object" || pa === null)
|
|
250
252
|
return { readable: false };
|
|
@@ -252,7 +254,7 @@ export function pendingActionToolCallId(body) {
|
|
|
252
254
|
if (kind !== null && typeof kind !== "string")
|
|
253
255
|
return { readable: false };
|
|
254
256
|
if (!("toolCallId" in pa))
|
|
255
|
-
return { readable: true, toolCallId: null, boundInputHash: null, kind,
|
|
257
|
+
return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceSessionId };
|
|
256
258
|
const id = pa.toolCallId;
|
|
257
259
|
if (typeof id !== "string")
|
|
258
260
|
return { readable: false };
|
|
@@ -261,7 +263,7 @@ export function pendingActionToolCallId(body) {
|
|
|
261
263
|
const bih = "boundInputHash" in pa ? pa.boundInputHash : null;
|
|
262
264
|
if (bih !== null && typeof bih !== "string")
|
|
263
265
|
return { readable: false };
|
|
264
|
-
return { readable: true, toolCallId: id, boundInputHash: bih, kind,
|
|
266
|
+
return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceSessionId };
|
|
265
267
|
}
|
|
266
268
|
function pendingActionOfColumn(cell, where) {
|
|
267
269
|
let body;
|
|
@@ -650,7 +652,7 @@ export class SqlCheckpointStore {
|
|
|
650
652
|
status: String(r.status),
|
|
651
653
|
createdAtMs: Number(r.created_at_ms),
|
|
652
654
|
boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
|
|
653
|
-
|
|
655
|
+
sourceSessionId: null,
|
|
654
656
|
};
|
|
655
657
|
if (Number(r.version) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
656
658
|
out.push({ ...base, boundCallId: null, unparseable: true });
|
|
@@ -673,7 +675,7 @@ export class SqlCheckpointStore {
|
|
|
673
675
|
const effectiveHash = base.boundInputHash ?? derived.boundInputHash;
|
|
674
676
|
const effective = col ?? derived.toolCallId;
|
|
675
677
|
if (effective === toolCallId)
|
|
676
|
-
out.push({ ...base, boundInputHash: effectiveHash,
|
|
678
|
+
out.push({ ...base, boundInputHash: effectiveHash, sourceSessionId: derived.sourceSessionId, boundCallId: effective });
|
|
677
679
|
}
|
|
678
680
|
return out;
|
|
679
681
|
}
|
|
@@ -2,7 +2,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
|
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
3
|
import { type FileHistoryRetentionPolicy, type ExecutionEnv, type FileHistoryStore, type FileHistoryResult, type FileHistoryTrackResult, type FileHistoryRestoreResult, type FileHistoryExport, type FileHistoryExportResult } from "@sema-agent/core";
|
|
4
4
|
import { type BlobBackend } from "./blob-backend.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ServiceBlobExchange } from "./store-backend.js";
|
|
6
6
|
import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
|
|
7
7
|
/** The file-history blob domain's SQL table (BlobBackend `table`/MinIO index binding + the A7 assert). */
|
|
8
8
|
export declare const FILE_HISTORY_BLOB_TABLE = "file_history_blob";
|
|
@@ -24,12 +24,10 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
|
|
|
24
24
|
/** RAW native pool — handed to blob-backend.ts (outside the SqlDriver seam, dialect-switches on
|
|
25
25
|
* its own string). Not used for this class's own statements (those go through `db`). */
|
|
26
26
|
protected readonly pool: MySqlPool | PgPool;
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
|
|
31
|
-
* undefined. Presence of this literal = the full typed putBlob/getBlob/hasBlobs face exists. */
|
|
32
|
-
readonly syncBlobFaces: true;
|
|
27
|
+
/** The blob exchange (core's `FileHistoryStore.blobExchange` group, see {@link ServiceBlobExchange}): how another machine's bytes
|
|
28
|
+
* come in ahead of an `importHistory`, go out to a puller, and are asked about. Its presence IS the capability — read through
|
|
29
|
+
* `blobExchangeOf` (`session-sync.ts`), which defers to core's judge; there is no second word for it on this class. */
|
|
30
|
+
readonly blobExchange: ServiceBlobExchange;
|
|
33
31
|
protected readonly blobs: BlobBackend;
|
|
34
32
|
/** §10 S-6 in-process lease state (design v1 D2: NOT persisted). Keys = `${scope}\x00${entryId}`. */
|
|
35
33
|
private readonly spentAttempts;
|
|
@@ -58,15 +56,25 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
|
|
|
58
56
|
/**
|
|
59
57
|
* Per-scope WRITE-SERIALIZATION lock (codex R1-critical): `SELECT … FOR UPDATE` over an EMPTY result
|
|
60
58
|
* locks nothing on PG, so neither the empty-target law (import/adopt) nor a multi-statement graph
|
|
61
|
-
* write can be serialized by the data rows alone. Every graph
|
|
59
|
+
* write can be serialized by the data rows alone. Every graph transaction therefore first ensures +
|
|
62
60
|
* row-locks the scope's sentinel row — one lock, both dialects, and disjoint-key writers can no
|
|
63
|
-
* longer interleave into a merged/torn graph.
|
|
64
|
-
* the sentinel: a waiter woken by that delete sees zero rows and must re-ensure.
|
|
61
|
+
* longer interleave into a merged/torn graph.
|
|
65
62
|
*
|
|
66
63
|
* Shape A ([ref]): the MySQL ensure verb is `ON DUPLICATE KEY UPDATE scope = scope`, NOT `INSERT IGNORE`.
|
|
67
64
|
* On InnoDB a duplicate-key IGNORE takes an **S** lock on the existing sentinel and the `FOR UPDATE` right
|
|
68
65
|
* below has to upgrade it to X — two concurrent scope writers each holding an S deadlock ([ref]). ON
|
|
69
66
|
* DUPLICATE takes X on the duplicate key, the same mode the locking read wants, so they serialize instead.
|
|
67
|
+
*
|
|
68
|
+
* **This is the transaction's first statement** (every caller: `begin()` and then this — nothing of the
|
|
69
|
+
* transaction exists before it), and a lost arbitration here restarts the transaction from the top
|
|
70
|
+
* (rollback + begin + the same ensure-and-lock), through the repo's one bound ({@link retryOnDeadlock}). Two shapes:
|
|
71
|
+
* · the engine picked this transaction as a deadlock victim. InnoDB only (REPEATABLE READ): when the sentinel
|
|
72
|
+
* physically disappears while ≥ 2 transactions are queued on it — its minter rolled back, or a whole-scope
|
|
73
|
+
* removal ({@link deleteBySession}) committed and purge took the delete-marked row — the queued locks are
|
|
74
|
+
* inherited as GAP locks on the next record, and each queued ensure's insert-intention waits on the others'
|
|
75
|
+
* gap locks. The engine kills all but one; the survivor's mint goes through and the others restart into it;
|
|
76
|
+
* · the sentinel was gone at the locking read ({@link ScopeSentinelVanishedError}; PG, see there).
|
|
77
|
+
* The lock order itself stays single: sentinels first (sorted), then the scope's rows.
|
|
70
78
|
*/
|
|
71
79
|
private lockScope;
|
|
72
80
|
/** Returns the UPDATE's affected count — 0 = the record is gone (annulled) or was already observed;
|
|
@@ -147,14 +155,12 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
|
|
|
147
155
|
/** Async byte-GC: blobs referenced by NO version row (any scope), past the created_at grace window.
|
|
148
156
|
* Domain-isolated by construction (this store's blob table only — [ref] D1). Reaper-driven. */
|
|
149
157
|
sweepOrphanBlobs(): Promise<number>;
|
|
150
|
-
/**
|
|
151
|
-
*
|
|
152
|
-
* The SQL blob judge's refusal ({@link BlobTooLargeError}, judged on the connection that would have written the blob)
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
/** The subset of `hashes` already present — one SQL INDEX probe (the import presence pre-check). */
|
|
157
|
-
hasBlobs(hashes: string[]): Promise<Set<string>>;
|
|
158
|
+
/** Store ONE content-addressed blob ahead of an importHistory (§5 streamed negotiation). The content address is judged HERE and
|
|
159
|
+
* nowhere else (core's `put` law: bytes whose sha256 is not `hash` are refused `adopt_failed` / `terminal`, nothing stored).
|
|
160
|
+
* Never-throw. The SQL blob judge's refusal ({@link BlobTooLargeError}, judged on the connection that would have written the blob)
|
|
161
|
+
* is a terminal refusal that carries `blobCap` — the route answers it 413 with the judge's sentence; every other write failure is
|
|
162
|
+
* `snapshot_failed` with the store's judgment of the fault (the route answers 502, "retry"). */
|
|
163
|
+
private putExchangedBlob;
|
|
158
164
|
adoptScope(fromScope: string, toScope: string): Promise<FileHistoryResult>;
|
|
159
165
|
private scopeEmpty;
|
|
160
166
|
/** `forUpdate` — see {@link loadTracked}. Every in-transaction caller passes `true`: the four reads are
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { captureFileBackupViaEnv, applyFileRestoreViaEnv, fileHistoryModeProvenDifferent, worldStillMatchesMint, annulProofFailure, boundaryPublishVerdict, fileHistoryDescribeTrackKey, fileHistoryTrackKeyOf, fileHistoryResolveTrackKey, fileHistoryExportDigest, validateFileHistoryExport, resolveFileHistoryRetention, fileHistoryBoundariesToKeep, } from "@sema-agent/core";
|
|
3
3
|
import { BlobTooLargeError, SqlBlobBackend } from "./blob-backend.js";
|
|
4
4
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
5
|
-
import { isMissingColumnError } from "./sql-errors.js";
|
|
5
|
+
import { isMissingColumnError, retryOnDeadlock } from "./sql-errors.js";
|
|
6
6
|
import { describeProbedTableDrift } from "./schema-drift-sentence.js";
|
|
7
7
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
8
8
|
import { foldCaughtFailure } from "../file-history-disclosure.js";
|
|
@@ -12,6 +12,8 @@ const sha256hex = (s) => createHash("sha256").update(s).digest("hex");
|
|
|
12
12
|
const truthy = (v) => v === true || v === 1 || v === "1";
|
|
13
13
|
class StagedRefGoneError extends Error {
|
|
14
14
|
}
|
|
15
|
+
class ScopeSentinelVanishedError extends Error {
|
|
16
|
+
}
|
|
15
17
|
function reapedEntryRefusal(entryId) {
|
|
16
18
|
return {
|
|
17
19
|
ok: false,
|
|
@@ -24,7 +26,11 @@ function reapedEntryRefusal(entryId) {
|
|
|
24
26
|
export class SqlFileHistoryStore {
|
|
25
27
|
db;
|
|
26
28
|
pool;
|
|
27
|
-
|
|
29
|
+
blobExchange = {
|
|
30
|
+
put: (hash, bytes) => this.putExchangedBlob(hash, bytes),
|
|
31
|
+
get: (hash) => this.blobs.getBlob(hash),
|
|
32
|
+
has: (hashes) => this.blobs.hasBlobs([...hashes]),
|
|
33
|
+
};
|
|
28
34
|
blobs;
|
|
29
35
|
spentAttempts = new Set();
|
|
30
36
|
activeAttempts = new Set();
|
|
@@ -48,16 +54,21 @@ export class SqlFileHistoryStore {
|
|
|
48
54
|
async beforeObserve(_scope, _keyHash) { }
|
|
49
55
|
async beforePublish(_scope) { }
|
|
50
56
|
async lockScope(conn, ...scopes) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
const ordered = [...new Set(scopes)].sort();
|
|
58
|
+
let restart = false;
|
|
59
|
+
await retryOnDeadlock(this.db.dialect, async () => {
|
|
60
|
+
if (restart) {
|
|
61
|
+
await conn.rollback().catch(() => undefined);
|
|
62
|
+
await conn.begin();
|
|
63
|
+
}
|
|
64
|
+
restart = true;
|
|
65
|
+
for (const scope of ordered) {
|
|
54
66
|
await conn.query(this.q("INSERT INTO file_history_scope (scope, created_at_ms) VALUES (?,?) ON DUPLICATE KEY UPDATE scope = scope", "INSERT INTO file_history_scope (scope, created_at_ms) VALUES ($1,$2) ON CONFLICT (scope) DO NOTHING"), [scope, Date.now()]);
|
|
55
67
|
const r = await conn.query(this.q("SELECT scope FROM file_history_scope WHERE scope = ? FOR UPDATE", "SELECT scope FROM file_history_scope WHERE scope = $1 FOR UPDATE"), [scope]);
|
|
56
|
-
|
|
68
|
+
if (!r.rows[0])
|
|
69
|
+
throw new ScopeSentinelVanishedError(`could not acquire the scope write lock for "${scope}" (sentinel row kept vanishing)`);
|
|
57
70
|
}
|
|
58
|
-
|
|
59
|
-
throw new Error(`could not acquire the scope write lock for "${scope}" (sentinel row kept vanishing)`);
|
|
60
|
-
}
|
|
71
|
+
}, (err) => err instanceof ScopeSentinelVanishedError);
|
|
61
72
|
}
|
|
62
73
|
async markObserved(scope, keyHash) {
|
|
63
74
|
const r = await this.db.query(this.q("UPDATE file_history_tracked SET observed_later = 1 WHERE scope = ? AND key_hash = ? AND observed_later = 0", "UPDATE file_history_tracked SET observed_later = TRUE WHERE scope = $1 AND key_hash = $2 AND observed_later = FALSE"), [scope, keyHash]);
|
|
@@ -585,9 +596,9 @@ export class SqlFileHistoryStore {
|
|
|
585
596
|
return 0;
|
|
586
597
|
return this.blobs.deleteBlobs(orphans);
|
|
587
598
|
}
|
|
588
|
-
async
|
|
599
|
+
async putExchangedBlob(hash, bytes) {
|
|
589
600
|
if (sha256hex(bytes) !== hash) {
|
|
590
|
-
return { ok: false, error: { code: "
|
|
601
|
+
return { ok: false, error: { code: "adopt_failed", message: `blob ${hash}: the bytes do not hash to this address — nothing stored`, permanence: "terminal" } };
|
|
591
602
|
}
|
|
592
603
|
try {
|
|
593
604
|
await this.blobs.putBlob(hash, bytes);
|
|
@@ -595,17 +606,11 @@ export class SqlFileHistoryStore {
|
|
|
595
606
|
}
|
|
596
607
|
catch (err) {
|
|
597
608
|
if (err instanceof BlobTooLargeError)
|
|
598
|
-
return { ok: false, error: { code:
|
|
609
|
+
return { ok: false, error: { code: "adopt_failed", message: err.message, permanence: "terminal", blobCap: { wallSource: err.cap.source } } };
|
|
599
610
|
const f = foldCaughtFailure(err);
|
|
600
|
-
return { ok: false, error: { code: "snapshot_failed", message: `
|
|
611
|
+
return { ok: false, error: { code: "snapshot_failed", message: `blob ${hash} write/upload failed: ${f.text}`, ...f.fault } };
|
|
601
612
|
}
|
|
602
613
|
}
|
|
603
|
-
async getBlob(hash) {
|
|
604
|
-
return this.blobs.getBlob(hash);
|
|
605
|
-
}
|
|
606
|
-
async hasBlobs(hashes) {
|
|
607
|
-
return this.blobs.hasBlobs(hashes);
|
|
608
|
-
}
|
|
609
614
|
async adoptScope(fromScope, toScope) {
|
|
610
615
|
try {
|
|
611
616
|
if (fromScope === toScope)
|