@sema-agent/server 7.4.0 → 7.6.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/LICENSE +1 -1
- package/README.md +18 -3
- package/README.zh-CN.md +14 -3
- package/USAGE.md +80 -1
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +109 -12
- package/dist/approval-reconciler.js +152 -24
- package/dist/boot/config-center.js +15 -2
- package/dist/boot/coordinators.js +10 -2
- package/dist/boot/execution-env.js +1 -1
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/parked-revive-gate.d.ts +78 -0
- package/dist/boot/parked-revive-gate.js +114 -0
- package/dist/boot/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -19
- package/dist/boot/resolve-spec.js +73 -67
- package/dist/boot/runner-deps.d.ts +23 -1
- package/dist/boot/runner-deps.js +8 -11
- package/dist/boot/workflow-orchestration.d.ts +8 -3
- package/dist/boot/workflow-orchestration.js +23 -1
- package/dist/budget.d.ts +1 -1
- package/dist/budget.js +1 -1
- package/dist/capabilities/repo-tools.d.ts +1 -1
- package/dist/capabilities/repo-tools.js +8 -2
- package/dist/config-center/apply-effective.js +33 -10
- package/dist/config-provider.d.ts +1 -0
- package/dist/config-provider.js +23 -3
- package/dist/config-types.d.ts +27 -9
- package/dist/config.d.ts +6 -1
- package/dist/config.js +61 -15
- package/dist/deployment-governance.d.ts +168 -0
- package/dist/deployment-governance.js +206 -0
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +17 -2
- package/dist/fleet/fleet-bus.js +68 -3
- package/dist/fleet/fleet-terminal-window.d.ts +98 -0
- package/dist/fleet/fleet-terminal-window.js +316 -0
- package/dist/governance-ask-marks.d.ts +31 -0
- package/dist/governance-ask-marks.js +122 -0
- package/dist/hooks/hook-runner.d.ts +28 -0
- package/dist/hooks/hook-runner.js +149 -25
- package/dist/http/routes/approvals-assistant.js +6 -7
- package/dist/http/routes/diagnostics.js +10 -5
- package/dist/http/routes/fleet.js +160 -14
- package/dist/http/routes/memory-policy.d.ts +2 -1
- package/dist/http/routes/memory-policy.js +77 -13
- package/dist/http/routes/runs.js +6 -2
- package/dist/http/routes/tasks.js +59 -22
- package/dist/http/routes/trace-usage.js +3 -4
- package/dist/http/send.d.ts +23 -0
- package/dist/http/send.js +23 -0
- package/dist/http/server.d.ts +9 -0
- package/dist/http/server.js +28 -14
- package/dist/http/sse-log.js +3 -4
- package/dist/http/wire-types.d.ts +7 -2
- package/dist/leader/diffout.d.ts +10 -0
- package/dist/leader/diffout.js +14 -2
- package/dist/leader/diffup.js +3 -2
- package/dist/leader/planner.js +7 -0
- package/dist/main.js +39 -31
- package/dist/observability/fail-open.d.ts +17 -2
- package/dist/observability/fail-open.js +19 -4
- package/dist/observability/prompt-manifest.d.ts +5 -1
- package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
- package/dist/orchestration/workflow-notify-journal.js +130 -45
- package/dist/parked-decide.js +9 -4
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
- package/dist/plugins/approval-ask-store-memory.js +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
- package/dist/plugins/approval-ask-store-sql.js +75 -35
- package/dist/plugins/background-agent-store-sql.js +16 -16
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/breaker-state-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
- package/dist/plugins/checkpoint-store-sql.js +76 -13
- package/dist/plugins/image-bake-store-sql.d.ts +1 -1
- package/dist/plugins/image-bake-store-sql.js +27 -27
- package/dist/plugins/image-index-sql.js +15 -15
- package/dist/plugins/local-checkpoint-store.d.ts +20 -1
- package/dist/plugins/local-checkpoint-store.js +19 -0
- package/dist/plugins/mailbox-store-sql.d.ts +4 -10
- package/dist/plugins/mailbox-store-sql.js +59 -6
- package/dist/plugins/memory-engine-pg.js +9 -9
- package/dist/plugins/memory-engine-tidb.js +7 -7
- package/dist/plugins/memory-sync-store-pg.js +13 -13
- package/dist/plugins/memory-sync-store-tidb.js +5 -5
- package/dist/plugins/outcome-ledger-sql.js +7 -7
- package/dist/plugins/pg-cost-quota.js +3 -3
- package/dist/plugins/pg-pool.js +84 -75
- package/dist/plugins/pg-rate-limiter.js +3 -3
- package/dist/plugins/pg-session-storage.d.ts +1 -1
- package/dist/plugins/pg-session-storage.js +12 -13
- package/dist/plugins/remote-env-host.js +3 -1
- package/dist/plugins/remote-env-local-docker.js +6 -3
- package/dist/plugins/remote-env-ssh.d.ts +13 -1
- package/dist/plugins/roster-store-sql.js +8 -8
- package/dist/plugins/store-contracts.d.ts +19 -0
- package/dist/plugins/store-contracts.js +42 -0
- package/dist/plugins/task-attachment-store.js +5 -5
- package/dist/plugins/task-list-store-sql.js +1 -1
- package/dist/plugins/tidb-cost-quota.js +1 -1
- package/dist/plugins/tidb-pool.js +83 -60
- package/dist/plugins/tidb-rate-limiter.js +1 -1
- package/dist/plugins/tidb-session-store.js +2 -5
- package/dist/plugins/tool-result-store-sql.js +2 -2
- package/dist/plugins/usage-window-store-sql.js +13 -13
- package/dist/plugins/write-behind-counter.d.ts +10 -2
- package/dist/plugins/write-behind-counter.js +13 -3
- package/dist/resource-suspend.d.ts +3 -1
- package/dist/resource-suspend.js +3 -1
- package/dist/run-local.d.ts +73 -1
- package/dist/run-local.js +146 -5
- package/dist/runs.d.ts +11 -1
- package/dist/runs.js +18 -3
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- package/dist/security.d.ts +12 -0
- package/dist/security.js +12 -0
- package/dist/session-sync-kernel.d.ts +13 -0
- package/dist/session-sync-kernel.js +13 -0
- package/dist/task-settings.d.ts +3 -9
- package/dist/task-settings.js +16 -13
- package/dist/tool-approval.d.ts +33 -6
- package/dist/tool-approval.js +80 -23
- package/dist/trace/core-keyset-guard.d.ts +18 -4
- package/dist/trace/project.d.ts +10 -1
- package/dist/trace/project.js +31 -0
- package/package.json +3 -3
- package/dist/boot/lexical-path-env.d.ts +0 -10
- package/dist/boot/lexical-path-env.js +0 -88
- package/dist/capabilities/oa-tools.d.ts +0 -15
- package/dist/capabilities/oa-tools.js +0 -54
- package/dist/finance/cost-taxonomy.d.ts +0 -34
- package/dist/finance/cost-taxonomy.js +0 -26
- package/dist/plugins/approval-store-sql.d.ts +0 -116
- package/dist/plugins/approval-store-sql.js +0 -151
- package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
- package/dist/plugins/file-workflow-journal-store.js +0 -12
- package/dist/plugins/pg-approval-store.d.ts +0 -9
- package/dist/plugins/pg-approval-store.js +0 -9
- package/dist/plugins/pg-breaker-state.d.ts +0 -8
- package/dist/plugins/pg-breaker-state.js +0 -8
- package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
- package/dist/plugins/pg-checkpoint-store.js +0 -10
- package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/pg-file-snapshot-store.js +0 -8
- package/dist/plugins/pg-image-bake.d.ts +0 -12
- package/dist/plugins/pg-image-bake.js +0 -11
- package/dist/plugins/pg-image-index.d.ts +0 -12
- package/dist/plugins/pg-image-index.js +0 -11
- package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
- package/dist/plugins/pg-outcome-ledger.js +0 -11
- package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/pg-resume-anchor-store.js +0 -7
- package/dist/plugins/pg-run-store.d.ts +0 -9
- package/dist/plugins/pg-run-store.js +0 -9
- package/dist/plugins/pg-session-policy-store.d.ts +0 -7
- package/dist/plugins/pg-session-policy-store.js +0 -7
- package/dist/plugins/pg-session-store.d.ts +0 -12
- package/dist/plugins/pg-session-store.js +0 -12
- package/dist/plugins/pg-tool-result-store.d.ts +0 -9
- package/dist/plugins/pg-tool-result-store.js +0 -9
- package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-journal-store.js +0 -9
- package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-run-store.js +0 -9
- package/dist/plugins/tidb-approval-store.d.ts +0 -8
- package/dist/plugins/tidb-approval-store.js +0 -8
- package/dist/plugins/tidb-breaker-state.d.ts +0 -7
- package/dist/plugins/tidb-breaker-state.js +0 -7
- package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
- package/dist/plugins/tidb-checkpoint-store.js +0 -9
- package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/tidb-file-snapshot-store.js +0 -8
- package/dist/plugins/tidb-image-bake.d.ts +0 -12
- package/dist/plugins/tidb-image-bake.js +0 -11
- package/dist/plugins/tidb-image-index.d.ts +0 -12
- package/dist/plugins/tidb-image-index.js +0 -11
- package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
- package/dist/plugins/tidb-outcome-ledger.js +0 -12
- package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/tidb-resume-anchor-store.js +0 -7
- package/dist/plugins/tidb-run-store.d.ts +0 -10
- package/dist/plugins/tidb-run-store.js +0 -9
- package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
- package/dist/plugins/tidb-session-policy-store.js +0 -7
- package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
- package/dist/plugins/tidb-tool-result-store.js +0 -10
- package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/tidb-workflow-journal-store.js +0 -9
- package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
- package/dist/plugins/tidb-workflow-run-store.js +0 -10
- package/dist/plugins/workflow-journal-limits.d.ts +0 -12
- package/dist/plugins/workflow-journal-limits.js +0 -12
- package/dist/sema-registry.d.ts +0 -41
- package/dist/sema-registry.js +0 -40
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WorkflowRun, WorkflowRunStore, WorkflowRunStatus, WorkflowRunSummary } from "@sema-agent/core";
|
|
2
|
-
import type
|
|
2
|
+
import { type FleetEventBus } from "../fleet/fleet-bus.js";
|
|
3
3
|
/** A workflow completion notification's terminal payload — the bounded, redacted shape core's notifier seam
|
|
4
4
|
* carries, re-derivable from a durable {@link import("@sema-agent/core").WorkflowRun} on the recovery path. */
|
|
5
5
|
export interface WorkflowCompletionPayload {
|
|
@@ -87,7 +87,21 @@ export declare class WorkflowNotifyGate {
|
|
|
87
87
|
* recovery-sweep error. */
|
|
88
88
|
onError?: (stage: "record" | "deliver" | "recover", runId: string, err: unknown) => void;
|
|
89
89
|
});
|
|
90
|
+
/**
|
|
91
|
+
* The runIds whose journal entry THIS process recorded — the boot-orphan judgment's authoritative anchor
|
|
92
|
+
* (round-1 review, MEDIUM). A wall-clock cutoff alone is not sound: after a BACKWARD clock step the entries
|
|
93
|
+
* this process records land BELOW the boot cutoff, and the sweep would finalize runs whose in-process
|
|
94
|
+
* executor is alive and running (a false `failed` notify for a workflow that then keeps going — worse than
|
|
95
|
+
* the absent row it was fixing). Membership here is a fact about THIS incarnation, unforgeable by any clock.
|
|
96
|
+
* Retired on ack AND the moment a sweep observes the run terminal (the anchor only ever gates the `running`
|
|
97
|
+
* arm), so a delivery/ack outage cannot pile up entries for runs that are no longer executing.
|
|
98
|
+
*/
|
|
99
|
+
private readonly recordedThisIncarnation;
|
|
90
100
|
private now;
|
|
101
|
+
/** Record a journal entry AND remember that this incarnation is the one that recorded it (see the field). */
|
|
102
|
+
private recordPending;
|
|
103
|
+
/** Ack an entry + drop its incarnation mark (acked ⇒ never scanned again, so the mark has no further use). */
|
|
104
|
+
private ackDelivered;
|
|
91
105
|
/**
|
|
92
106
|
* Journal a STARTED run as pending-notify. Call this with the synchronous `runId` from `startWorkflow` /
|
|
93
107
|
* `run_workflow` — BEFORE the workflow can reach terminal — so a crash mid-run still leaves a recoverable
|
|
@@ -129,14 +143,50 @@ export declare class WorkflowNotifyGate {
|
|
|
129
143
|
* workflow died) + ack, instead of leaking the entry forever. This closes the exact crash topology SVC-1
|
|
130
144
|
* exists for (a replica SIGKILLed mid-run).
|
|
131
145
|
* - `running` and FRESH (within the grace window): genuinely in flight → leave pending (the owning process
|
|
132
|
-
* delivers its terminal notify, or the next sweep catches it once it goes terminal or stale).
|
|
146
|
+
* delivers its terminal notify, or the next sweep catches it once it goes terminal or stale). The sweep
|
|
147
|
+
* publishes NOTHING here — see the single-writer invariant below.
|
|
133
148
|
* - MISSING (reaped / never persisted): ack-as-abandoned so the journal doesn't chase a ghost forever.
|
|
134
149
|
* `orphanGraceMs` MUST exceed the max expected workflow runtime (the run store has no cross-replica liveness
|
|
135
150
|
* signal, so age is the only orphan proxy). Returns a tally. A per-entry throw is isolated so one bad entry
|
|
136
151
|
* can't abort the sweep.
|
|
152
|
+
*
|
|
153
|
+
* BOTH abandoned arms (boot-orphan + stale-past-grace) flip the DURABLE row to `failed` FIRST (CAS on rev),
|
|
154
|
+
* so the run store, the fleet panel and the delivered notify give ONE answer — previously the notify said
|
|
155
|
+
* `failed` while `/workflows` kept saying `running` forever ([2999]: republishing without finalizing would
|
|
156
|
+
* have turned "panel empty" into "panel shows a row that never moves"). A lost CAS means the run moved under
|
|
157
|
+
* the sweep (e.g. its real terminal landed concurrently) — skip this pass; the entry stays pending and the
|
|
158
|
+
* next sweep handles the NEW state.
|
|
159
|
+
*
|
|
160
|
+
* 🔴 SINGLE-WRITER INVARIANT for the fleet row (round-1 review, two HIGH findings): the sweep only ever
|
|
161
|
+
* publishes a **TERMINAL** fleet frame (the flip above, via `publishTerminalFleetRow` → final frame + remove).
|
|
162
|
+
* It NEVER publishes a `running` row. Two reasons, both "a row we mint here can become one nobody can retire":
|
|
163
|
+
* 1. cross-replica (SQL journal): a pending `running` entry may belong to ANOTHER replica. Its terminal
|
|
164
|
+
* update lands on that replica's own (replica-local) fleet bus, and its ack removes the entry from the
|
|
165
|
+
* shared journal — so this replica would never see the run again and its minted row would sit `running`
|
|
166
|
+
* forever. (The terminal-redelivery arm deliberately doesn't publish either — it is a notify path.)
|
|
167
|
+
* 2. same-replica: `runStore.get` is a READ-TIME SNAPSHOT. If the live run commits its terminal (and the
|
|
168
|
+
* wrapper removes the row) between that read and the publish, a `running` republish resurrects a row
|
|
169
|
+
* that will never be removed again.
|
|
170
|
+
* A live row's ONE writer is this replica's `put`/`update` observation point ({@link JournalingWorkflowRunStore}),
|
|
171
|
+
* which by construction sees every transition including the terminal one.
|
|
137
172
|
*/
|
|
138
173
|
recover(opts?: {
|
|
139
174
|
orphanGraceMs?: number;
|
|
175
|
+
/** Replica-local (File/in-memory) journal ONLY: a pending entry recorded BEFORE this timestamp belongs to
|
|
176
|
+
* a previous incarnation of THIS process — workflow executors are in-process, so they died with it and a
|
|
177
|
+
* still-`running` run is a boot-orphan: finalize it NOW instead of leaving the user's "Waiting for
|
|
178
|
+
* workflow" pointed at a run that can never finish for the whole grace window ([2995] 顺带①/[3000]§二).
|
|
179
|
+
* NEVER pass this for a cross-replica (SQL) journal — there a pending running run may be genuinely alive
|
|
180
|
+
* on another replica, and age (`orphanGraceMs`) is the only sound orphan proxy.
|
|
181
|
+
* ⚠️ This wall-clock cutoff is a NECESSARY, not sufficient, condition — {@link recordedThisIncarnation}
|
|
182
|
+
* is the authoritative one (a clock rollback after boot would otherwise stamp THIS process's own new
|
|
183
|
+
* entries below the cutoff and finalize live runs). */
|
|
184
|
+
finalizeStartedBeforeMs?: number;
|
|
185
|
+
/** Retire a fleet row for a run this sweep just finalized ([2995]/[2999]): called ONLY with a TERMINAL run,
|
|
186
|
+
* so the {@link JournalingWorkflowRunStore} projection publishes the final frame and then removes the row —
|
|
187
|
+
* the store, the panel and the notify end up saying the same thing. Never called with a `running` run (see
|
|
188
|
+
* the single-writer invariant above). Late-bound because the wrapper is constructed after this gate. */
|
|
189
|
+
publishTerminalFleetRow?: (id: string, run: WorkflowRun) => void;
|
|
140
190
|
}): Promise<{
|
|
141
191
|
scanned: number;
|
|
142
192
|
redelivered: number;
|
|
@@ -234,6 +284,12 @@ export declare class JournalingWorkflowRunStore implements WorkflowRunStore {
|
|
|
234
284
|
/** Derive + publish the MF-Fleet workflow row from a WorkflowRun (doneCount/totalCount from agents, tokens from
|
|
235
285
|
* stats); a terminal workflow LEAVES the fleet (publish terminal then remove), a running one stays/updates. */
|
|
236
286
|
private publishFleet;
|
|
287
|
+
/** [2995] recovery projection seam: the same fleet projection as the put/update observation points, exposed so
|
|
288
|
+
* the recovery sweep can RETIRE a row for a run it just finalized (terminal input ⇒ final frame, then remove).
|
|
289
|
+
* Wire it as `recover`'s `publishTerminalFleetRow` hook — and only ever hand it a TERMINAL run: a `running`
|
|
290
|
+
* row published from outside the wrapper's own write path has no guaranteed retirement (see the invariant on
|
|
291
|
+
* {@link WorkflowNotifyGate.recover}). */
|
|
292
|
+
republishFleet(id: string, run: WorkflowRun): void;
|
|
237
293
|
put(id: string, run: WorkflowRun): Promise<void>;
|
|
238
294
|
update(id: string, scope: string, run: WorkflowRun, expect?: {
|
|
239
295
|
rev: number;
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
*/
|
|
42
42
|
import { mkdirSync, readFileSync, existsSync, openSync, writeSync, fsyncSync, closeSync } from "node:fs";
|
|
43
43
|
import { join } from "node:path";
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
44
|
+
import { buildFleetWorkflowRow } from "../fleet/fleet-bus.js";
|
|
45
|
+
import { isTerminalWorkflowRowStatus, seedTerminalWorkflowRow } from "../fleet/fleet-terminal-window.js";
|
|
46
46
|
/** Build a bounded + redacted completion summary from a durable run's terminal state (the recovery path — the
|
|
47
47
|
* live path uses core's own `boundedSummary`). We never have the raw result here (the run store keeps the run,
|
|
48
48
|
* not the originator's payload), so we synthesize a conservative one: status + the run's already-bounded error.
|
|
@@ -86,9 +86,29 @@ export class WorkflowNotifyGate {
|
|
|
86
86
|
this.deliver = deliver;
|
|
87
87
|
this.opts = opts;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The runIds whose journal entry THIS process recorded — the boot-orphan judgment's authoritative anchor
|
|
91
|
+
* (round-1 review, MEDIUM). A wall-clock cutoff alone is not sound: after a BACKWARD clock step the entries
|
|
92
|
+
* this process records land BELOW the boot cutoff, and the sweep would finalize runs whose in-process
|
|
93
|
+
* executor is alive and running (a false `failed` notify for a workflow that then keeps going — worse than
|
|
94
|
+
* the absent row it was fixing). Membership here is a fact about THIS incarnation, unforgeable by any clock.
|
|
95
|
+
* Retired on ack AND the moment a sweep observes the run terminal (the anchor only ever gates the `running`
|
|
96
|
+
* arm), so a delivery/ack outage cannot pile up entries for runs that are no longer executing.
|
|
97
|
+
*/
|
|
98
|
+
recordedThisIncarnation = new Set();
|
|
89
99
|
now() {
|
|
90
100
|
return this.opts.now ? this.opts.now() : Date.now();
|
|
91
101
|
}
|
|
102
|
+
/** Record a journal entry AND remember that this incarnation is the one that recorded it (see the field). */
|
|
103
|
+
async recordPending(entry) {
|
|
104
|
+
await this.journal.record(entry);
|
|
105
|
+
this.recordedThisIncarnation.add(entry.runId);
|
|
106
|
+
}
|
|
107
|
+
/** Ack an entry + drop its incarnation mark (acked ⇒ never scanned again, so the mark has no further use). */
|
|
108
|
+
async ackDelivered(runId) {
|
|
109
|
+
await this.journal.ack(runId, this.now());
|
|
110
|
+
this.recordedThisIncarnation.delete(runId);
|
|
111
|
+
}
|
|
92
112
|
/**
|
|
93
113
|
* Journal a STARTED run as pending-notify. Call this with the synchronous `runId` from `startWorkflow` /
|
|
94
114
|
* `run_workflow` — BEFORE the workflow can reach terminal — so a crash mid-run still leaves a recoverable
|
|
@@ -97,7 +117,7 @@ export class WorkflowNotifyGate {
|
|
|
97
117
|
*/
|
|
98
118
|
async onWorkflowStart(input) {
|
|
99
119
|
try {
|
|
100
|
-
await this.
|
|
120
|
+
await this.recordPending({
|
|
101
121
|
runId: input.runId,
|
|
102
122
|
scope: input.scope,
|
|
103
123
|
...(input.sourceTaskId ? { sourceTaskId: input.sourceTaskId } : {}),
|
|
@@ -142,7 +162,7 @@ export class WorkflowNotifyGate {
|
|
|
142
162
|
if (!existing) {
|
|
143
163
|
// The start hook didn't journal it (e.g. a run started before this gate existed, or the hook was skipped).
|
|
144
164
|
// Record it now so the deliver-then-ack ordering still holds and a crash mid-delivery is recoverable.
|
|
145
|
-
await this.
|
|
165
|
+
await this.recordPending({
|
|
146
166
|
runId: p.runId,
|
|
147
167
|
scope,
|
|
148
168
|
...(p.sourceTaskId ? { sourceTaskId: p.sourceTaskId } : {}),
|
|
@@ -158,7 +178,7 @@ export class WorkflowNotifyGate {
|
|
|
158
178
|
this.opts.onError?.("deliver", p.runId, err);
|
|
159
179
|
return;
|
|
160
180
|
}
|
|
161
|
-
await this.
|
|
181
|
+
await this.ackDelivered(p.runId);
|
|
162
182
|
}
|
|
163
183
|
/**
|
|
164
184
|
* RECOVERY sweep — run at boot AND PERIODICALLY (wired into the service reaper), BEFORE/while serving traffic.
|
|
@@ -171,11 +191,32 @@ export class WorkflowNotifyGate {
|
|
|
171
191
|
* workflow died) + ack, instead of leaking the entry forever. This closes the exact crash topology SVC-1
|
|
172
192
|
* exists for (a replica SIGKILLed mid-run).
|
|
173
193
|
* - `running` and FRESH (within the grace window): genuinely in flight → leave pending (the owning process
|
|
174
|
-
* delivers its terminal notify, or the next sweep catches it once it goes terminal or stale).
|
|
194
|
+
* delivers its terminal notify, or the next sweep catches it once it goes terminal or stale). The sweep
|
|
195
|
+
* publishes NOTHING here — see the single-writer invariant below.
|
|
175
196
|
* - MISSING (reaped / never persisted): ack-as-abandoned so the journal doesn't chase a ghost forever.
|
|
176
197
|
* `orphanGraceMs` MUST exceed the max expected workflow runtime (the run store has no cross-replica liveness
|
|
177
198
|
* signal, so age is the only orphan proxy). Returns a tally. A per-entry throw is isolated so one bad entry
|
|
178
199
|
* can't abort the sweep.
|
|
200
|
+
*
|
|
201
|
+
* BOTH abandoned arms (boot-orphan + stale-past-grace) flip the DURABLE row to `failed` FIRST (CAS on rev),
|
|
202
|
+
* so the run store, the fleet panel and the delivered notify give ONE answer — previously the notify said
|
|
203
|
+
* `failed` while `/workflows` kept saying `running` forever ([2999]: republishing without finalizing would
|
|
204
|
+
* have turned "panel empty" into "panel shows a row that never moves"). A lost CAS means the run moved under
|
|
205
|
+
* the sweep (e.g. its real terminal landed concurrently) — skip this pass; the entry stays pending and the
|
|
206
|
+
* next sweep handles the NEW state.
|
|
207
|
+
*
|
|
208
|
+
* 🔴 SINGLE-WRITER INVARIANT for the fleet row (round-1 review, two HIGH findings): the sweep only ever
|
|
209
|
+
* publishes a **TERMINAL** fleet frame (the flip above, via `publishTerminalFleetRow` → final frame + remove).
|
|
210
|
+
* It NEVER publishes a `running` row. Two reasons, both "a row we mint here can become one nobody can retire":
|
|
211
|
+
* 1. cross-replica (SQL journal): a pending `running` entry may belong to ANOTHER replica. Its terminal
|
|
212
|
+
* update lands on that replica's own (replica-local) fleet bus, and its ack removes the entry from the
|
|
213
|
+
* shared journal — so this replica would never see the run again and its minted row would sit `running`
|
|
214
|
+
* forever. (The terminal-redelivery arm deliberately doesn't publish either — it is a notify path.)
|
|
215
|
+
* 2. same-replica: `runStore.get` is a READ-TIME SNAPSHOT. If the live run commits its terminal (and the
|
|
216
|
+
* wrapper removes the row) between that read and the publish, a `running` republish resurrects a row
|
|
217
|
+
* that will never be removed again.
|
|
218
|
+
* A live row's ONE writer is this replica's `put`/`update` observation point ({@link JournalingWorkflowRunStore}),
|
|
219
|
+
* which by construction sees every transition including the terminal one.
|
|
179
220
|
*/
|
|
180
221
|
async recover(opts = {}) {
|
|
181
222
|
const orphanGraceMs = opts.orphanGraceMs ?? 24 * 60 * 60 * 1000; // 24h default — well beyond any normal workflow
|
|
@@ -183,12 +224,28 @@ export class WorkflowNotifyGate {
|
|
|
183
224
|
let redelivered = 0;
|
|
184
225
|
let stillRunning = 0;
|
|
185
226
|
let abandoned = 0;
|
|
227
|
+
/** Was this entry recorded by a PREVIOUS incarnation of this process? Only meaningful when the caller granted
|
|
228
|
+
* the replica-local finalize authority (a shared/SQL journal carries other replicas' entries). Both halves
|
|
229
|
+
* are required — see `finalizeStartedBeforeMs` + {@link recordedThisIncarnation}. */
|
|
230
|
+
const isPreBootEntry = (entry) => opts.finalizeStartedBeforeMs !== undefined &&
|
|
231
|
+
!this.recordedThisIncarnation.has(entry.runId) &&
|
|
232
|
+
entry.createdAt < opts.finalizeStartedBeforeMs;
|
|
186
233
|
for (const entry of pending) {
|
|
187
234
|
try {
|
|
188
235
|
const run = await this.runStore.get(entry.runId);
|
|
189
236
|
if (!run) {
|
|
190
|
-
// No durable run — reaped or
|
|
191
|
-
|
|
237
|
+
// No durable run — USUALLY a ghost (the row was reaped, or its `put` threw). But it is ALSO the
|
|
238
|
+
// transient shape of a run being started RIGHT NOW: JournalingWorkflowRunStore journals BEFORE it
|
|
239
|
+
// persists (deliberately — a persist that throws must still leave a recoverable entry), so there is a
|
|
240
|
+
// window where the entry exists and the row does not. Acking that window is unrecoverable: the owner's
|
|
241
|
+
// own terminal `deliverOnce` then sees `acked` and drops the completion for good — the exact loss SVC-1
|
|
242
|
+
// exists to prevent (round-2 review, HIGH). So only ack a ghost that PROVABLY isn't that window: one
|
|
243
|
+
// that predates this incarnation, or one older than the grace the whole sweep already trusts.
|
|
244
|
+
if (!isPreBootEntry(entry) && this.now() - entry.createdAt <= orphanGraceMs) {
|
|
245
|
+
stillRunning++; // "left pending this pass" — a start-in-progress is exactly that
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
await this.ackDelivered(entry.runId);
|
|
192
249
|
abandoned++;
|
|
193
250
|
continue;
|
|
194
251
|
}
|
|
@@ -207,15 +264,43 @@ export class WorkflowNotifyGate {
|
|
|
207
264
|
const originatingSessionId = run.originatingSessionId;
|
|
208
265
|
if (run.status === "running") {
|
|
209
266
|
const startedAt = run.startedAt || entry.createdAt;
|
|
210
|
-
|
|
267
|
+
// [2995] boot-orphan judgment (replica-local journal only): the entry predates this process, so the
|
|
268
|
+
// in-process executor that owned it died with the previous incarnation — nothing will ever flip it.
|
|
269
|
+
// (Conditions in `isPreBootEntry`: replica-local authority + not recorded by US + predates boot.)
|
|
270
|
+
const bootOrphan = isPreBootEntry(entry);
|
|
271
|
+
if (!bootOrphan && this.now() - startedAt <= orphanGraceMs) {
|
|
211
272
|
stillRunning++;
|
|
212
|
-
|
|
273
|
+
// fresh → genuinely in flight; the owner (or a later sweep) delivers terminal. Publish NOTHING here:
|
|
274
|
+
// a `running` row minted from a read-time snapshot can outlive every chance to retire it (the
|
|
275
|
+
// single-writer invariant on the doc-comment above).
|
|
276
|
+
continue;
|
|
213
277
|
}
|
|
214
|
-
//
|
|
278
|
+
// Orphaned `running` — boot-orphan (executor died with the previous process) or stale past grace
|
|
279
|
+
// (nothing will ever flip it terminal). Finalize as abandoned/failed: flip the DURABLE row first
|
|
280
|
+
// ([2999] honesty half — store/fleet/notify must give one answer), then deliver + ack.
|
|
281
|
+
const abandonSummary = bootOrphan
|
|
282
|
+
? "workflow abandoned — the engine restarted while it was running (its in-process executor did not survive the restart)"
|
|
283
|
+
: "workflow abandoned — still `running` past the orphan grace window (replica likely crashed mid-run)";
|
|
284
|
+
const flipped = { ...run, status: "failed", error: abandonSummary, endedAt: this.now() };
|
|
285
|
+
const flippedOk = await this.runStore.update(entry.runId, run.scope, flipped, run.rev !== undefined ? { rev: run.rev } : undefined);
|
|
286
|
+
if (!flippedOk)
|
|
287
|
+
continue; // the run moved under the sweep — leave pending; next sweep sees the NEW state
|
|
288
|
+
this.recordedThisIncarnation.delete(entry.runId); // terminal now — the anchor only ever gates `running`
|
|
289
|
+
// ORDER: retire the fleet row FIRST — immediately after the durable flip, before anything is awaited.
|
|
290
|
+
// Same order as the live leg (the wrapper publishes the terminal frame, then core fires its notify), and
|
|
291
|
+
// it is the only order where "a finalized run leaves the active fleet" survives every delivery outcome
|
|
292
|
+
// (rounds 4+5): the delivery may throw at its ack, or never settle at all, and a `finally` cannot help
|
|
293
|
+
// with the latter — while the NEXT sweep takes the terminal-redelivery arm, which deliberately publishes
|
|
294
|
+
// nothing, so a row skipped here would be stranded `running` with nobody left to retire it.
|
|
295
|
+
// The converse risk is bounded and self-healing: if the projection throws, this pass delivers nothing
|
|
296
|
+
// and the entry stays pending — the next sweep's terminal arm delivers it. Deferred by one sweep, never
|
|
297
|
+
// lost. (Subscriber throws are already isolated inside the fleet bus — per-callback try/catch with a
|
|
298
|
+
// registered fail-open tag — so a projection throw here means the publish machinery itself broke.)
|
|
299
|
+
opts.publishTerminalFleetRow?.(entry.runId, flipped); // final frame then remove
|
|
215
300
|
await this.deliverOnce({
|
|
216
301
|
runId: entry.runId,
|
|
217
302
|
status: "failed",
|
|
218
|
-
summary:
|
|
303
|
+
summary: abandonSummary,
|
|
219
304
|
...(sourceTaskId ? { sourceTaskId } : {}),
|
|
220
305
|
...(principal ? { principal } : {}),
|
|
221
306
|
...(originatingSessionId ? { originatingSessionId } : {}),
|
|
@@ -225,6 +310,10 @@ export class WorkflowNotifyGate {
|
|
|
225
310
|
continue;
|
|
226
311
|
}
|
|
227
312
|
// Terminal but not acked ⇒ the crash dropped its notify. Re-derive the bounded summary + re-deliver.
|
|
313
|
+
// Drop the incarnation mark FIRST (round-2 review, MEDIUM): the anchor only ever gates the `running` arm,
|
|
314
|
+
// so a terminal run has no use for it — retiring here (not only on a successful ack) keeps the registry
|
|
315
|
+
// tracking runs that are actually in flight even through a prolonged delivery outage.
|
|
316
|
+
this.recordedThisIncarnation.delete(entry.runId);
|
|
228
317
|
await this.deliverOnce({
|
|
229
318
|
runId: entry.runId,
|
|
230
319
|
status: run.status,
|
|
@@ -411,40 +500,36 @@ export class JournalingWorkflowRunStore {
|
|
|
411
500
|
publishFleet(id, run) {
|
|
412
501
|
if (!this.fleetBus)
|
|
413
502
|
return;
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
//
|
|
421
|
-
// (
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
id,
|
|
428
|
-
// [WF2-A parity] redact the workflow label surfaces for parity with the run + subagent-child names (fleet-bus.ts):
|
|
429
|
-
// a tool-launched (LLM-authored) workflow's meta.name/description is task-controlled and could carry a secret shape.
|
|
430
|
-
name: redactSecrets(run.name ?? "Dynamic workflow"),
|
|
431
|
-
...(run.description ? { description: redactSecrets(run.description) } : {}),
|
|
432
|
-
scope: run.scope,
|
|
433
|
-
// codex-6 F2:sessionId 必须随行——streamFleet 对无 sessionId 的行按「同 principal 全会话可见」
|
|
434
|
-
// 兜底,漏发=A 会话的 workflow 名/进度/token 泄进 B 会话的 ?session= 过滤流。
|
|
435
|
-
...(run.originatingSessionId ? { sessionId: run.originatingSessionId } : {}),
|
|
436
|
-
status: run.status,
|
|
437
|
-
doneCount: done,
|
|
438
|
-
totalCount: agents.length,
|
|
439
|
-
failedCount: failed,
|
|
440
|
-
startedCount: started,
|
|
441
|
-
tokens: (run.stats?.tokens ?? 0) + (run.stats?.nested?.tokens ?? 0),
|
|
442
|
-
// [1294]:跑动中也带时长(1.232 只在 endedAt 后带——clay 验收轮实锚面板恒显 0s)。终态用
|
|
443
|
-
// endedAt 定格,活跑用 now-startedAt(每次 put/update 观察点刷新,壳侧读帧即当前时长)。
|
|
444
|
-
elapsedMs: (run.endedAt ?? Date.now()) - run.startedAt,
|
|
445
|
-
});
|
|
503
|
+
// A-002.3(#194 病族 F1 同源性断裂):终态判据走 fleet-terminal-window 的共享谓词(其穷举表对
|
|
504
|
+
// core WorkflowRunStatus 编译期闭合)——此前手写两值比较是第二份独立定义,core 加终态词时穷举表
|
|
505
|
+
// 红、这里恒 false ⇒ 行既不 seed 进终态窗也不 removeWorkflow,面板留幽灵 running 行。
|
|
506
|
+
const terminal = isTerminalWorkflowRowStatus(run.status);
|
|
507
|
+
// 撤行**不挂在发帧成功上**(复审第 2 轮 MEDIUM):「终态行必须离场」是不变量、发终帧是尽力而为。
|
|
508
|
+
// try/finally ⇒ 发布本体抛错照样撤行(异常仍向上抛,不吞:吞了就是一条未登记的 fail-open 臂)。
|
|
509
|
+
// 订阅方抛错已在 fleet bus 的 subscribe 隔离层被吞并记 fail-open——本 finally 守的是发布机器本身
|
|
510
|
+
// 的抛错(理论缝级,fresh 复审盘点后判近死防御但保留:防未来发布本体改动回退)。两腿一处收口。
|
|
511
|
+
// 行投影本体 = {@link buildFleetWorkflowRow}(#189:快照的终态行窗与本写路径共用同一份投影,
|
|
512
|
+
// 单源不漂;本函数的发布/撤行语义逐字不变)。
|
|
513
|
+
// [3156] 轮2 修:终态行**写路径顺手 seed** 进程内终态窗缓存——重启-重连剧本里(boot recover 判死),
|
|
514
|
+
// fleetWide 连接的 durable pull 没有正确的查询键可用(caller principal ≠ 引擎铸行 scope),seed 是
|
|
515
|
+
// 唯一同源可达径。放在 publish 之前:seed 是纯内存 push,不会抛;终态行入窗与发帧同拍。
|
|
446
516
|
if (terminal)
|
|
447
|
-
|
|
517
|
+
seedTerminalWorkflowRow(buildFleetWorkflowRow(id, run));
|
|
518
|
+
try {
|
|
519
|
+
this.fleetBus.publishWorkflow(buildFleetWorkflowRow(id, run));
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
if (terminal)
|
|
523
|
+
this.fleetBus.removeWorkflow(id); // terminal workflow leaves the active fleet (the shell saw the final frame)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/** [2995] recovery projection seam: the same fleet projection as the put/update observation points, exposed so
|
|
527
|
+
* the recovery sweep can RETIRE a row for a run it just finalized (terminal input ⇒ final frame, then remove).
|
|
528
|
+
* Wire it as `recover`'s `publishTerminalFleetRow` hook — and only ever hand it a TERMINAL run: a `running`
|
|
529
|
+
* row published from outside the wrapper's own write path has no guaranteed retirement (see the invariant on
|
|
530
|
+
* {@link WorkflowNotifyGate.recover}). */
|
|
531
|
+
republishFleet(id, run) {
|
|
532
|
+
this.publishFleet(id, run);
|
|
448
533
|
}
|
|
449
534
|
async put(id, run) {
|
|
450
535
|
// Journal FIRST (best-effort, swallowed inside onWorkflowStart) so a started run is always recoverable, THEN
|
package/dist/parked-decide.js
CHANGED
|
@@ -81,10 +81,15 @@ export async function decideParkedAgent(deps, req) {
|
|
|
81
81
|
// RB-459(core 5.7.0,F1/F6 翻案后唯一剩下的 claim 前拒):问题门的 approve 必须携 answer——
|
|
82
82
|
// parked 腿无 live answering face,消费 claim 后只会空跑一次 revive;claim 前拒是免损前置。带 answer 的
|
|
83
83
|
// approve 与任何 deny 都放行透传,core 是语义权威(header 逐字匹配等)。
|
|
84
|
-
// 🔴 #152
|
|
85
|
-
// deps.onQuestion` 为 undefined 或 QUESTION_AWAITS_RESUME
|
|
86
|
-
//
|
|
87
|
-
//
|
|
84
|
+
// 🔴 #152 当年写这条时的机制理由是:core 的 pre-CAS 拒条件(「`taskConfig.onQuestion ??
|
|
85
|
+
// deps.onQuestion` 为 undefined 或 QUESTION_AWAITS_RESUME」)在 ASK_QUESTION_ENABLED 部署上恒不成立
|
|
86
|
+
// ⇒ core 放行,而赎回腿会拿 coordinator 的**空答**当人答并消费掉 checkpoint。
|
|
87
|
+
// ⚠️ 那个理由已被后续两处证伪,别再据它推理:①#166 起 QuestionCoordinator 的每一条无人应答臂(无
|
|
88
|
+
// ALS ctx / abort / 限流 / 呈现不了 / TTL)一律返 `{kind:"unavailable"}`,类头逐字承诺「It NEVER
|
|
89
|
+
// synthesizes an empty answer set」——赎回腿无 ctx,拿到的是 unavailable 而不是空答;②core 5.16 对
|
|
90
|
+
// 赎回中的 ask 走 redeemsApproval 臂返 isError(「will not silently self-answer」)。
|
|
91
|
+
// 本判据**保留**,理由换成免损前置:放过去也只会空跑一次 revive(claim 已被消费),claim 前拒是
|
|
92
|
+
// 更便宜、错误信息也更准的那一手(任务级孪生:src/http/server.ts 的 decide 路由)。
|
|
88
93
|
return { status: 400, body: { error: "pending action is AskUserQuestion — approve requires body.answer (the operator's answers[]); deny needs none", errorCode: "decide.parked_answer_required", taskId: match.handle } };
|
|
89
94
|
}
|
|
90
95
|
if (row.name === undefined) {
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* - resolveProvisional 故意不经 canAskTransition(同 SQL twin 头注:这是版本化补偿的例外通道)。
|
|
13
13
|
*/
|
|
14
14
|
import { type AskState, type BatchState } from "../approval-ask-machine.js";
|
|
15
|
-
import type { AskDecision, AskRow, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
|
|
15
|
+
import type { AskDecision, AskRow, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, EnsureAskResult, DecideResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
|
|
16
16
|
export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
|
|
17
17
|
private readonly asks;
|
|
18
18
|
private readonly batches;
|
|
19
|
-
ensureAsk(row: NewAskRow): Promise<
|
|
19
|
+
ensureAsk(row: NewAskRow): Promise<EnsureAskResult>;
|
|
20
20
|
transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
21
21
|
decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
|
|
22
22
|
expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
|
|
@@ -50,8 +50,9 @@ export class InMemoryApprovalAskStore {
|
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
const existing = this.asks.get(row.askId);
|
|
53
|
+
// 幂等命中:行是**别人**插的 ⇒ `inserted: false`(收尾权限判别位,语义见 `EnsureAskResult` 顶注)。
|
|
53
54
|
if (existing)
|
|
54
|
-
return { ...existing };
|
|
55
|
+
return { row: { ...existing }, inserted: false };
|
|
55
56
|
// 🔴 车4 §12-E:`idempotency_key` 已归**回决专用**,`NewAskRow` 的同名字段随之摘除 ⇒ 铸行不再有
|
|
56
57
|
// 唯一性可撞(列恒 NULL)。原先在这里镜像 UNIQUE 的那段扫描也随之下车;撞键判定搬到 `decideAsk`
|
|
57
58
|
// (SQL twin 那侧是 `(task_id, idempotency_key)` 索引在库层拒 ⇒ typed `idempotency_conflict`)。
|
|
@@ -85,7 +86,7 @@ export class InMemoryApprovalAskStore {
|
|
|
85
86
|
updatedAtMs: row.createdAtMs,
|
|
86
87
|
};
|
|
87
88
|
this.asks.set(row.askId, created);
|
|
88
|
-
return { ...created };
|
|
89
|
+
return { row: { ...created }, inserted: true };
|
|
89
90
|
}
|
|
90
91
|
async transitionAsk(askId, from, to, patch) {
|
|
91
92
|
if (!canAskTransition(from, to)) {
|
|
@@ -59,9 +59,13 @@ export interface AskRow {
|
|
|
59
59
|
* 🔴 车5 §9 C2:铸卡时呈给人看的那份 input 的**服务端摘要**(`AskRequest.boundInputHash`,core 侧已在场)。
|
|
60
60
|
* 与下面 PARKED 坐标里的 `gateBoundInputHash` 是**两件不同的东西**,别混:
|
|
61
61
|
* - `boundInputHash`(本列)= ask **铸行时**的入参摘要,一次写定永不改;对账收敛器判据 1 用它跟
|
|
62
|
-
* checkpoint
|
|
63
|
-
* (
|
|
64
|
-
*
|
|
62
|
+
* checkpoint 行的同名列做**硬相等**——它是**身份三元组之外的第二道等式**,身份本身是
|
|
63
|
+
* (`sourceTaskId`, `toolCallId`, 因果下界)三维(#168 件1 换轴,判据属主 = `approval-reconciler.ts`
|
|
64
|
+
* 的 `classifyGateMatch`;本注上一版写的「identity 四元组」是换轴前的旧口径,`sessionId` 从来不是
|
|
65
|
+
* 内存判据维)。任一侧缺席 ⇒ 判据 1 **不命中**;归因看走到哪一层:身份先判(候选集非空却没有同身份
|
|
66
|
+
* 的一条 ⇒ `identity_miss`),身份这层还够得着时缺席才记 `single_mint`(禁「能取到时才比」的可选谓词
|
|
67
|
+
* ——同 session 内 toolCallId 会被网关重用,只靠身份会把旧 ask PARK 到别人的 resume 坐标上,而
|
|
68
|
+
* PARKED 是不可回滚的终态)。
|
|
65
69
|
* - `gateBoundInputHash`(下面)= 真 **PARK 成功那一刻**从 checkpoint 抄回来的坐标之一,bindBatch 才写。
|
|
66
70
|
* 本车只落店面承载(列 + 行形 + 读写),铸行调用点的供值归车2/3b。
|
|
67
71
|
*/
|
|
@@ -114,6 +118,23 @@ export interface NewAskRow {
|
|
|
114
118
|
expiresAtMs: number;
|
|
115
119
|
createdAtMs: number;
|
|
116
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* `ensureAsk` 的产出 —— 行 **+ 这一次调用到底插没插**(#168 件2)。
|
|
123
|
+
*
|
|
124
|
+
* 🔴 为什么判别位必须由**店**给:`ensureAsk` 是幂等 upsert(`askId` 是确定性派生,重试 / failover /
|
|
125
|
+
* 闭包再入天然指向同一行),所以「拿到一条 STREAM_PENDING 行」有两种成因 —— 本次插的,或幂等命中了
|
|
126
|
+
* **别人正持有**的那条活行。两者的收尾权限完全相反:前者本次可以收(放弃时把孤儿行 VOID 掉),后者
|
|
127
|
+
* 一个字都不许动(动了就是把真属主正在等的那张卡作废)。调用侧此前只能拿 `createdAtMs === 本次传入值`
|
|
128
|
+
* 去**猜**归属,而那把尺在同一毫秒的两次并发插入上会给出假阳性(旧注里如实记着的残余)。
|
|
129
|
+
* `INSERT IGNORE` / `ON CONFLICT DO NOTHING` 的 affected 行数是引擎对同一个问题的**权威**回答,
|
|
130
|
+
* 两方言都有;把它如实带出来,猜就退休了。
|
|
131
|
+
*
|
|
132
|
+
* `inserted: true` ⇒ 这条行是本次调用写下的(可收尾);`false` ⇒ 幂等命中既有行(只读,不许收尾)。
|
|
133
|
+
*/
|
|
134
|
+
export interface EnsureAskResult {
|
|
135
|
+
row: AskRow;
|
|
136
|
+
inserted: boolean;
|
|
137
|
+
}
|
|
117
138
|
/** `transitionAsk`/`resolveProvisional` 的可选补丁——只有出现的字段才落 SQL(未出现 = 该列不变),
|
|
118
139
|
* `updatedAtMs` 恒必填(调用方是未来的协调器,时间戳由它按事件时钟决定,店不偷偷用 `Date.now()`)。 */
|
|
119
140
|
export interface AskTransitionPatch {
|
|
@@ -206,7 +227,8 @@ export interface BatchRow {
|
|
|
206
227
|
}
|
|
207
228
|
/** design 定稿 §4 的持久层接口。协调器接线(车2)、恢复扫描消费(车5)不在本车范围——本车只落这些方法。 */
|
|
208
229
|
export interface ApprovalAskStore {
|
|
209
|
-
|
|
230
|
+
/** 幂等 upsert。返回**行 + 本次是否真插入**(判别位语义见 {@link EnsureAskResult})。 */
|
|
231
|
+
ensureAsk(row: NewAskRow): Promise<EnsureAskResult>;
|
|
210
232
|
transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
211
233
|
decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
|
|
212
234
|
expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
|
|
@@ -275,6 +297,39 @@ export declare const APPROVAL_BATCHES_TABLE = "approval_batches";
|
|
|
275
297
|
*
|
|
276
298
|
* SCHEMA POLICY 同 `tidb-pool.ts` 头注:纯 CREATE,禁 ALTER 增量 seam,改列直接改这里 + 重建库
|
|
277
299
|
* (`docs/schema/baseline-mysql.sql` 是由这些文本机器录制出的**产物**,永不手改)。
|
|
300
|
+
*
|
|
301
|
+
* ── 列宽的**测量依据**(#192 A10;U2 逐列取证在档)──────────────────────────────────────────────────
|
|
302
|
+
* 本表家族原先一律 VARCHAR(255),是"先建表后想"的产物。按"每列的值到底由谁铸、有没有入口上限"重新裁:
|
|
303
|
+
*
|
|
304
|
+
* 收窄(有硬上限背书,证据即上限本身):
|
|
305
|
+
* · `ask_id` / `batch_id` / `bound_ask_id` → 64:`deriveAskId`/`deriveBatchId` 恒是 sha256 hex 取前
|
|
306
|
+
* 64 字符(`approval-ask-machine.ts` deterministicId),按构造不可能更长。
|
|
307
|
+
* · `task_id` → 64:入口 `UUIDV7_RE`(`security.ts:134`)只放行 36 字符的规范 uuidv7;与兄弟列
|
|
308
|
+
* `task_run.task_id VARCHAR(64)` 同宽(此前 255 是本家族独有的偏离,无依据)。
|
|
309
|
+
* · `session_id` → 64:提交入口硬拒 >64(`http/server.ts` "sessionId must be at most 64 characters"),
|
|
310
|
+
* 与 `session_meta`/`task_run` 同宽。
|
|
311
|
+
* · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts:350`)硬拒更长者;
|
|
312
|
+
* 全仓每一根 owner/scope 轴都是 190,此前 255 同样是无依据偏离。
|
|
313
|
+
* · `gate_token` → 120:它抄的是 checkpoint 的 token(core `mintCheckpointToken` = 16 字节 hex = 32 字符),
|
|
314
|
+
* 取与**被抄那一列** `checkpoint.token VARCHAR(120)` 同宽 —— 同一个值在两张表上宽度必须一致,
|
|
315
|
+
* 否则抄的那一步就是一道静默截断。512 是原先的 16 倍冗余。
|
|
316
|
+
*
|
|
317
|
+
* 🔑 上面四条收窄之所以**不引入任何新暴露面**,靠的不是入口断言而是一条更硬的事实:同一个 session_id /
|
|
318
|
+
* task_id / owner **早就**同时躺在 `session_meta.session_id VARCHAR(64)` / `task_run.task_id VARCHAR(64)` /
|
|
319
|
+
* `task_run.owner VARCHAR(190)` 里。任何长到能撑爆本表新宽度的值,在写到那几张**更中心**的表时就已经
|
|
320
|
+
* 先炸了 —— 本表此前的 255 从来不是一道额外的安全余量,只是一处与全仓不一致的偏离。
|
|
321
|
+
* ⇒ 收窄的风险上界 = 0;真正要担心的是反过来:留着 255 会让人误以为这里可以存更长的 id。
|
|
322
|
+
*
|
|
323
|
+
* 🔴 **刻意不收窄**(测量结果不支持,记在这里免得下一个人以为是漏了):
|
|
324
|
+
* · `source_task_id` / `tool_call_id` / `parent_tool_call_id` / `gate_bound_call_id` —— 这四列的值是
|
|
325
|
+
* **模型/引擎铸的原始 id**,服务端在写行这一步**没有任何长度断言**(`approval-card.ts` 的
|
|
326
|
+
* `MAX_IDENT` 只裁了给人看的那份 card 投影,不是本列)。没有入口上限就收窄 = 把"存不下"这件事
|
|
327
|
+
* 推迟到 INSERT 才炸(MySQL 更糟:静默截断),换来的只是几十字节。要收窄,先补入口断言
|
|
328
|
+
* (照 `assertPrincipalShape`/`assertIdempotencyKeyShape` 的姿势),那是另一件事。
|
|
329
|
+
* · `bound_input_hash` / `gate_bound_input_hash` —— 按约定是 sha256 hex(64),但 `readBoundInputHash`
|
|
330
|
+
* 只校验"非空字符串",没校长度/字形 ⇒ 同上,先有断言再谈收窄。
|
|
331
|
+
* · `idempotency_key` —— 255 **正是**入口断言的上限本身(zod `.max(255)` + 店内 `assertIdempotencyKeyShape`
|
|
332
|
+
* 双执法),宽度与上限同源,恰好正确,动它反而制造截断面。
|
|
278
333
|
*/
|
|
279
334
|
export declare const TIDB_APPROVAL_ASK_STATEMENTS: readonly string[];
|
|
280
335
|
/** {@link TIDB_APPROVAL_ASK_STATEMENTS} 的遍历壳——生产路径走 `tidb-pool.ts` 的中央 `ensureSchema`
|
|
@@ -303,7 +358,7 @@ export declare class SqlApprovalAskStore implements ApprovalAskStore {
|
|
|
303
358
|
private getAskOn;
|
|
304
359
|
/** {@link getAskOn} 的批侧同形(同一条纪律:失败臂回读走本连接)。 */
|
|
305
360
|
private getBatchOn;
|
|
306
|
-
ensureAsk(row: NewAskRow): Promise<
|
|
361
|
+
ensureAsk(row: NewAskRow): Promise<EnsureAskResult>;
|
|
307
362
|
transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
308
363
|
decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
|
|
309
364
|
expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
|