@abelxiaoxing/cadence 1.1.0 → 1.2.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/README.md +19 -3
- package/agents/implementation-worker.md +17 -11
- package/config/routes.example.json +0 -2
- package/package.json +2 -2
- package/prompts/abel-design.md +77 -17
- package/prompts/abel-diagnose.md +2 -0
- package/prompts/abel-implement.md +23 -11
- package/skills/abel-workflow/SKILL.md +42 -5
- package/src/agent-registry.ts +0 -1
- package/src/candidate-patch.ts +250 -0
- package/src/child-session.ts +27 -24
- package/src/contracts.ts +19 -10
- package/src/control-contracts.ts +15 -62
- package/src/delivery-compiler.ts +47 -17
- package/src/design-control.ts +1053 -0
- package/src/design-journal.ts +1616 -0
- package/src/index.ts +626 -196
- package/src/packet-runtime.ts +6 -6
- package/src/route-policy.ts +45 -17
- package/src/run-state.ts +43 -4
- package/src/run-store.ts +103 -15
- package/src/subagent-activity.ts +1 -1
- package/src/submit-tool.ts +149 -49
- package/src/verification-capability.ts +43 -15
- package/src/worker-broker.ts +0 -1
- package/src/workflow-engine.ts +416 -250
- package/agents/contract-reviewer.md +0 -11
package/src/packet-runtime.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { runtimeForWorkerRoute } from "./parent-provider.ts";
|
|
|
20
20
|
import {
|
|
21
21
|
loadRoutePolicy,
|
|
22
22
|
type RoutePolicy,
|
|
23
|
-
unavailableRoutePolicy,
|
|
24
23
|
type WorkerRole,
|
|
25
24
|
} from "./route-policy.ts";
|
|
26
25
|
import { type BrokerActivityUpdate, RunWorkerBroker } from "./worker-broker.ts";
|
|
@@ -319,7 +318,7 @@ export class PacketRuntime {
|
|
|
319
318
|
if (!agent) {
|
|
320
319
|
throw new Error("packet agent definition is unavailable");
|
|
321
320
|
}
|
|
322
|
-
const broker = this.#brokerFor(context
|
|
321
|
+
const broker = this.#brokerFor(context);
|
|
323
322
|
const routed = await broker.run({
|
|
324
323
|
runId: `${packet.stage}:${packet.id}`,
|
|
325
324
|
operationId: packet.id,
|
|
@@ -405,8 +404,8 @@ export class PacketRuntime {
|
|
|
405
404
|
return routed.value.child;
|
|
406
405
|
}
|
|
407
406
|
|
|
408
|
-
#brokerFor(
|
|
409
|
-
const root = path.resolve(cwd);
|
|
407
|
+
#brokerFor(context: PacketContext): RunWorkerBroker {
|
|
408
|
+
const root = path.resolve(context.cwd);
|
|
410
409
|
const existing = this.#brokers.get(root);
|
|
411
410
|
let policy = this.#routePolicy;
|
|
412
411
|
if (policy && existing) return existing;
|
|
@@ -414,12 +413,13 @@ export class PacketRuntime {
|
|
|
414
413
|
const resolution = loadRoutePolicy({
|
|
415
414
|
cwd: root,
|
|
416
415
|
...(this.#routePolicyHome ? { home: this.#routePolicyHome } : {}),
|
|
416
|
+
...(context.model ? { parentModel: context.model } : {}),
|
|
417
417
|
});
|
|
418
|
-
if (!resolution.ok
|
|
418
|
+
if (!resolution.ok) {
|
|
419
419
|
const code = resolution.diagnostics[0]?.code ?? "policy-invalid";
|
|
420
420
|
throw new Error(`route-policy-unavailable:${code}`);
|
|
421
421
|
}
|
|
422
|
-
policy = resolution.
|
|
422
|
+
policy = resolution.policy;
|
|
423
423
|
}
|
|
424
424
|
const serializedPolicy = JSON.stringify(policy);
|
|
425
425
|
if (existing && this.#brokerPolicies.get(root) === serializedPolicy) {
|
package/src/route-policy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { ROLES } from "./contracts.ts";
|
|
@@ -21,6 +21,11 @@ export interface RouteCapabilities {
|
|
|
21
21
|
maxTokens: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface ParentModelCapabilities {
|
|
25
|
+
contextWindow: number;
|
|
26
|
+
maxTokens: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
interface RouteBase {
|
|
25
30
|
id: string;
|
|
26
31
|
kind: "inherited" | "custom";
|
|
@@ -50,10 +55,9 @@ export interface RoutePolicy {
|
|
|
50
55
|
|
|
51
56
|
export type RoutePolicySource =
|
|
52
57
|
| { kind: "project" | "user"; path: string }
|
|
53
|
-
| { kind: "
|
|
58
|
+
| { kind: "default" };
|
|
54
59
|
|
|
55
60
|
export type RoutePolicyDiagnosticCode =
|
|
56
|
-
| "policy-missing"
|
|
57
61
|
| "policy-unreadable"
|
|
58
62
|
| "policy-invalid-json"
|
|
59
63
|
| "policy-invalid"
|
|
@@ -77,7 +81,7 @@ export type ParsedRoutePolicy =
|
|
|
77
81
|
export type RoutePolicyResolution =
|
|
78
82
|
| {
|
|
79
83
|
ok: true;
|
|
80
|
-
source:
|
|
84
|
+
source: RoutePolicySource;
|
|
81
85
|
policy: RoutePolicy;
|
|
82
86
|
}
|
|
83
87
|
| {
|
|
@@ -87,8 +91,8 @@ export type RoutePolicyResolution =
|
|
|
87
91
|
};
|
|
88
92
|
|
|
89
93
|
/**
|
|
90
|
-
* Internal fail-closed policy used while
|
|
91
|
-
*
|
|
94
|
+
* Internal fail-closed policy used while a selected explicit policy is
|
|
95
|
+
* invalid. It deliberately contains no inherited-parent route:
|
|
92
96
|
* the broker therefore reports endpoint-unavailable without making a Worker
|
|
93
97
|
* request, while the local durable control plane remains available.
|
|
94
98
|
*
|
|
@@ -351,8 +355,16 @@ export function parseRoutePolicy(value: unknown): ParsedRoutePolicy {
|
|
|
351
355
|
};
|
|
352
356
|
}
|
|
353
357
|
|
|
354
|
-
/**
|
|
355
|
-
export function parentRoutePolicy(
|
|
358
|
+
/** Default parent-model route used when no explicit policy file exists. */
|
|
359
|
+
export function parentRoutePolicy(
|
|
360
|
+
parentModel?: ParentModelCapabilities,
|
|
361
|
+
): RoutePolicy {
|
|
362
|
+
const contextWindow = positiveInteger(parentModel?.contextWindow)
|
|
363
|
+
? parentModel.contextWindow
|
|
364
|
+
: 1;
|
|
365
|
+
const maxTokens = positiveInteger(parentModel?.maxTokens)
|
|
366
|
+
? Math.min(parentModel.maxTokens, contextWindow)
|
|
367
|
+
: 1;
|
|
356
368
|
const parsed = parseRoutePolicy({
|
|
357
369
|
version: ROUTE_POLICY_VERSION,
|
|
358
370
|
routes: {
|
|
@@ -361,8 +373,8 @@ export function parentRoutePolicy(): RoutePolicy {
|
|
|
361
373
|
capabilities: {
|
|
362
374
|
roles: [...ROLES],
|
|
363
375
|
dialects: [...ROUTE_DIALECTS],
|
|
364
|
-
contextWindow
|
|
365
|
-
maxTokens
|
|
376
|
+
contextWindow,
|
|
377
|
+
maxTokens,
|
|
366
378
|
},
|
|
367
379
|
},
|
|
368
380
|
},
|
|
@@ -372,25 +384,41 @@ export function parentRoutePolicy(): RoutePolicy {
|
|
|
372
384
|
return parsed.policy;
|
|
373
385
|
}
|
|
374
386
|
|
|
375
|
-
function selectedSource(
|
|
387
|
+
function selectedSource(
|
|
388
|
+
cwd: string,
|
|
389
|
+
home: string,
|
|
390
|
+
): { kind: "project" | "user"; path: string } | { kind: "none" } {
|
|
391
|
+
const mayExist = (candidate: string): boolean => {
|
|
392
|
+
try {
|
|
393
|
+
return lstatSync(candidate, { throwIfNoEntry: false }) !== undefined;
|
|
394
|
+
} catch {
|
|
395
|
+
// Permission and path-observation failures are explicit unreadable
|
|
396
|
+
// policy candidates, not proof that the policy is absent.
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
376
400
|
const projectPath = path.join(cwd, ".pi", "cadence", "routes.json");
|
|
377
|
-
if (
|
|
401
|
+
if (mayExist(projectPath)) return { kind: "project", path: projectPath };
|
|
378
402
|
const userPath = path.join(home, ".pi", "agent", "cadence", "routes.json");
|
|
379
|
-
if (
|
|
403
|
+
if (mayExist(userPath)) return { kind: "user", path: userPath };
|
|
380
404
|
return { kind: "none" };
|
|
381
405
|
}
|
|
382
406
|
|
|
383
407
|
export function loadRoutePolicy(
|
|
384
|
-
options: {
|
|
408
|
+
options: {
|
|
409
|
+
cwd?: string;
|
|
410
|
+
home?: string;
|
|
411
|
+
parentModel?: ParentModelCapabilities;
|
|
412
|
+
} = {},
|
|
385
413
|
): RoutePolicyResolution {
|
|
386
414
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
387
415
|
const home = path.resolve(options.home ?? homedir());
|
|
388
416
|
const source = selectedSource(cwd, home);
|
|
389
417
|
if (source.kind === "none") {
|
|
390
418
|
return {
|
|
391
|
-
ok:
|
|
392
|
-
source,
|
|
393
|
-
|
|
419
|
+
ok: true,
|
|
420
|
+
source: { kind: "default" },
|
|
421
|
+
policy: parentRoutePolicy(options.parentModel),
|
|
394
422
|
};
|
|
395
423
|
}
|
|
396
424
|
let bytes: string;
|
package/src/run-state.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
export const RUN_STAGES = ["abel-design", "abel-implement"] as const;
|
|
4
|
+
export type RunStage = (typeof RUN_STAGES)[number];
|
|
3
5
|
|
|
4
6
|
export const RUN_STATES = [
|
|
5
7
|
"created",
|
|
@@ -47,12 +49,17 @@ export interface DeliveryBindingProjection {
|
|
|
47
49
|
gate: DeliveryGate;
|
|
48
50
|
revision: number;
|
|
49
51
|
receiptHash: string;
|
|
52
|
+
approvalProof?: {
|
|
53
|
+
revision: number;
|
|
54
|
+
contractHash: string;
|
|
55
|
+
recordHash: string;
|
|
56
|
+
};
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
export interface RunProjection {
|
|
53
60
|
runId: string;
|
|
54
61
|
rootHash: string;
|
|
55
|
-
stage:
|
|
62
|
+
stage: RunStage;
|
|
56
63
|
change?: string;
|
|
57
64
|
provisionalKey?: string;
|
|
58
65
|
state: RunState;
|
|
@@ -133,6 +140,7 @@ const LEGAL_TRANSITIONS: Readonly<Record<RunState, readonly RunState[]>> = {
|
|
|
133
140
|
"running",
|
|
134
141
|
"approval-needed",
|
|
135
142
|
"recovering",
|
|
143
|
+
"completed",
|
|
136
144
|
"discarded",
|
|
137
145
|
"rejected",
|
|
138
146
|
],
|
|
@@ -263,12 +271,31 @@ function applyVerifiedEvent(
|
|
|
263
271
|
const gate = event.payload.gate;
|
|
264
272
|
const revision = event.payload.revision;
|
|
265
273
|
const receiptHash = event.payload.receiptHash;
|
|
274
|
+
const approvalProof = event.payload.approvalProof;
|
|
266
275
|
if (
|
|
267
276
|
(gate !== "gate-a" && gate !== "gate-b") ||
|
|
268
277
|
!Number.isSafeInteger(revision) ||
|
|
269
278
|
(revision as number) < 1 ||
|
|
270
279
|
typeof receiptHash !== "string" ||
|
|
271
|
-
!SHA256.test(receiptHash)
|
|
280
|
+
!SHA256.test(receiptHash) ||
|
|
281
|
+
(approvalProof !== undefined &&
|
|
282
|
+
(!approvalProof ||
|
|
283
|
+
typeof approvalProof !== "object" ||
|
|
284
|
+
Array.isArray(approvalProof) ||
|
|
285
|
+
!Number.isSafeInteger(
|
|
286
|
+
(approvalProof as Record<string, unknown>).revision,
|
|
287
|
+
) ||
|
|
288
|
+
Number((approvalProof as Record<string, unknown>).revision) < 1 ||
|
|
289
|
+
typeof (approvalProof as Record<string, unknown>).contractHash !==
|
|
290
|
+
"string" ||
|
|
291
|
+
!SHA256.test(
|
|
292
|
+
String((approvalProof as Record<string, unknown>).contractHash),
|
|
293
|
+
) ||
|
|
294
|
+
typeof (approvalProof as Record<string, unknown>).recordHash !==
|
|
295
|
+
"string" ||
|
|
296
|
+
!SHA256.test(
|
|
297
|
+
String((approvalProof as Record<string, unknown>).recordHash),
|
|
298
|
+
)))
|
|
272
299
|
) {
|
|
273
300
|
throw new Error("event-integrity-delivery-binding");
|
|
274
301
|
}
|
|
@@ -276,13 +303,25 @@ function applyVerifiedEvent(
|
|
|
276
303
|
gate: gate as DeliveryGate,
|
|
277
304
|
revision: revision as number,
|
|
278
305
|
receiptHash,
|
|
306
|
+
...(approvalProof
|
|
307
|
+
? {
|
|
308
|
+
approvalProof: structuredClone(
|
|
309
|
+
approvalProof as DeliveryBindingProjection["approvalProof"],
|
|
310
|
+
),
|
|
311
|
+
}
|
|
312
|
+
: {}),
|
|
279
313
|
};
|
|
280
314
|
const existing = projection.deliveryBindings.find(
|
|
281
315
|
(candidate) =>
|
|
282
316
|
candidate.gate === binding.gate &&
|
|
283
317
|
candidate.revision === binding.revision,
|
|
284
318
|
);
|
|
285
|
-
if (
|
|
319
|
+
if (
|
|
320
|
+
existing &&
|
|
321
|
+
(existing.receiptHash !== binding.receiptHash ||
|
|
322
|
+
canonicalJson(existing.approvalProof) !==
|
|
323
|
+
canonicalJson(binding.approvalProof))
|
|
324
|
+
) {
|
|
286
325
|
throw new Error("event-integrity-delivery-conflict");
|
|
287
326
|
}
|
|
288
327
|
const deliveryBindings = existing
|
package/src/run-store.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { chmodSync, closeSync, lstatSync, openSync } from "node:fs";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
|
-
import type { ControlStage } from "./control-contracts.ts";
|
|
5
4
|
import {
|
|
6
5
|
createRunEvent,
|
|
7
6
|
type DeliveryGate,
|
|
8
7
|
type RunEvent,
|
|
9
8
|
type RunProjection,
|
|
9
|
+
type RunStage,
|
|
10
10
|
type RunState,
|
|
11
11
|
reduceRunEvents,
|
|
12
12
|
ZERO_EVENT_HASH,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
StateRootError,
|
|
18
18
|
} from "./state-root.ts";
|
|
19
19
|
|
|
20
|
-
export const RUN_STORE_SCHEMA_VERSION =
|
|
20
|
+
export const RUN_STORE_SCHEMA_VERSION = 4 as const;
|
|
21
21
|
|
|
22
22
|
const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
|
|
23
23
|
const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
@@ -37,6 +37,7 @@ interface EventRow {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
interface OperationRow {
|
|
40
|
+
kind: string;
|
|
40
41
|
state: string;
|
|
41
42
|
outcome_json: string | null;
|
|
42
43
|
lease_token: string | null;
|
|
@@ -49,7 +50,7 @@ export interface RunStoreOptions {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface StartRunInput {
|
|
52
|
-
stage:
|
|
53
|
+
stage: RunStage;
|
|
53
54
|
change?: string;
|
|
54
55
|
provisionalKey?: string;
|
|
55
56
|
operationId: string;
|
|
@@ -60,6 +61,11 @@ export interface BindDeliveryInput {
|
|
|
60
61
|
gate: DeliveryGate;
|
|
61
62
|
revision: number;
|
|
62
63
|
receiptHash: string;
|
|
64
|
+
approvalProof?: {
|
|
65
|
+
revision: number;
|
|
66
|
+
contractHash: string;
|
|
67
|
+
recordHash: string;
|
|
68
|
+
};
|
|
63
69
|
operationId: string;
|
|
64
70
|
lease?: OperationLease;
|
|
65
71
|
}
|
|
@@ -79,7 +85,7 @@ export interface OperationLeaseStatus {
|
|
|
79
85
|
|
|
80
86
|
const SCHEMA = `
|
|
81
87
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
82
|
-
version INTEGER PRIMARY KEY CHECK (version =
|
|
88
|
+
version INTEGER PRIMARY KEY CHECK (version = 4)
|
|
83
89
|
) STRICT;
|
|
84
90
|
|
|
85
91
|
CREATE TABLE IF NOT EXISTS runs (
|
|
@@ -103,6 +109,9 @@ const SCHEMA = `
|
|
|
103
109
|
gate TEXT NOT NULL,
|
|
104
110
|
revision INTEGER NOT NULL,
|
|
105
111
|
receipt_hash TEXT NOT NULL,
|
|
112
|
+
approval_revision INTEGER,
|
|
113
|
+
contract_hash TEXT,
|
|
114
|
+
record_hash TEXT,
|
|
106
115
|
operation_id TEXT NOT NULL,
|
|
107
116
|
PRIMARY KEY (run_id, gate, revision),
|
|
108
117
|
UNIQUE (run_id, operation_id)
|
|
@@ -330,7 +339,7 @@ export class RunStore {
|
|
|
330
339
|
#operation(runId: string, operationId: string): OperationRow | undefined {
|
|
331
340
|
return this.#database
|
|
332
341
|
.prepare(
|
|
333
|
-
`SELECT state, outcome_json, lease_token, lease_expires_at
|
|
342
|
+
`SELECT kind, state, outcome_json, lease_token, lease_expires_at
|
|
334
343
|
FROM operations WHERE run_id = ? AND operation_id = ?`,
|
|
335
344
|
)
|
|
336
345
|
.get(runId, operationId) as OperationRow | undefined;
|
|
@@ -339,8 +348,10 @@ export class RunStore {
|
|
|
339
348
|
#operationOutcome(
|
|
340
349
|
runId: string,
|
|
341
350
|
operationId: string,
|
|
351
|
+
kind: string,
|
|
342
352
|
): RunProjection | undefined {
|
|
343
353
|
const row = this.#operation(runId, operationId);
|
|
354
|
+
if (row && row.kind !== kind) throw new Error("operation-id-conflict");
|
|
344
355
|
if (row?.state !== "committed" || row.outcome_json === null)
|
|
345
356
|
return undefined;
|
|
346
357
|
return parseProjection(row.outcome_json);
|
|
@@ -433,7 +444,7 @@ export class RunStore {
|
|
|
433
444
|
}
|
|
434
445
|
const key = lookupKey(input);
|
|
435
446
|
return this.#transaction(() => {
|
|
436
|
-
|
|
447
|
+
let existing = this.#database
|
|
437
448
|
.prepare(
|
|
438
449
|
`SELECT run_id, projection_json FROM runs
|
|
439
450
|
WHERE root_hash = ? AND stage = ? AND lookup_key = ?`,
|
|
@@ -441,10 +452,23 @@ export class RunStore {
|
|
|
441
452
|
.get(this.stateRoot.consumerRootHash, input.stage, key) as
|
|
442
453
|
| RunRow
|
|
443
454
|
| undefined;
|
|
455
|
+
if (existing) {
|
|
456
|
+
const projection = parseProjection(existing.projection_json);
|
|
457
|
+
if (
|
|
458
|
+
input.stage === "abel-design" &&
|
|
459
|
+
["completed", "discarded", "rejected"].includes(projection.state)
|
|
460
|
+
) {
|
|
461
|
+
this.#database
|
|
462
|
+
.prepare("UPDATE runs SET lookup_key = ? WHERE run_id = ?")
|
|
463
|
+
.run(`terminal:${existing.run_id}`, existing.run_id);
|
|
464
|
+
existing = undefined;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
444
467
|
if (existing) {
|
|
445
468
|
const replay = this.#operationOutcome(
|
|
446
469
|
existing.run_id,
|
|
447
470
|
input.operationId,
|
|
471
|
+
"start",
|
|
448
472
|
);
|
|
449
473
|
if (replay) return replay;
|
|
450
474
|
const projection = parseProjection(existing.projection_json);
|
|
@@ -519,7 +543,11 @@ export class RunStore {
|
|
|
519
543
|
requireIdentifier(input.operationId, "operation-id");
|
|
520
544
|
if (!CHANGE_NAME.test(input.change)) throw new Error("invalid-change");
|
|
521
545
|
return this.#transaction(() => {
|
|
522
|
-
const replay = this.#operationOutcome(
|
|
546
|
+
const replay = this.#operationOutcome(
|
|
547
|
+
input.runId,
|
|
548
|
+
input.operationId,
|
|
549
|
+
"change-bound",
|
|
550
|
+
);
|
|
523
551
|
if (replay) return replay;
|
|
524
552
|
const current = this.status(input.runId);
|
|
525
553
|
if (current.stage !== "abel-design" || !current.provisionalKey) {
|
|
@@ -561,7 +589,12 @@ export class RunStore {
|
|
|
561
589
|
(input.gate !== "gate-a" && input.gate !== "gate-b") ||
|
|
562
590
|
!Number.isSafeInteger(input.revision) ||
|
|
563
591
|
input.revision < 1 ||
|
|
564
|
-
!SHA256.test(input.receiptHash)
|
|
592
|
+
!SHA256.test(input.receiptHash) ||
|
|
593
|
+
(input.approvalProof !== undefined &&
|
|
594
|
+
(!Number.isSafeInteger(input.approvalProof.revision) ||
|
|
595
|
+
input.approvalProof.revision < 1 ||
|
|
596
|
+
!SHA256.test(input.approvalProof.contractHash) ||
|
|
597
|
+
!SHA256.test(input.approvalProof.recordHash)))
|
|
565
598
|
) {
|
|
566
599
|
throw new Error("invalid-delivery-binding");
|
|
567
600
|
}
|
|
@@ -572,18 +605,35 @@ export class RunStore {
|
|
|
572
605
|
if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
|
|
573
606
|
this.#assertLease(input.lease);
|
|
574
607
|
}
|
|
575
|
-
const replay = this.#operationOutcome(
|
|
608
|
+
const replay = this.#operationOutcome(
|
|
609
|
+
input.runId,
|
|
610
|
+
input.operationId,
|
|
611
|
+
"delivery-bound",
|
|
612
|
+
);
|
|
576
613
|
if (replay) return replay;
|
|
577
614
|
const existing = this.#database
|
|
578
615
|
.prepare(
|
|
579
|
-
`SELECT receipt_hash
|
|
616
|
+
`SELECT receipt_hash, approval_revision, contract_hash, record_hash
|
|
617
|
+
FROM delivery_bindings
|
|
580
618
|
WHERE run_id = ? AND gate = ? AND revision = ?`,
|
|
581
619
|
)
|
|
582
620
|
.get(input.runId, input.gate, input.revision) as
|
|
583
|
-
| {
|
|
621
|
+
| {
|
|
622
|
+
receipt_hash: string;
|
|
623
|
+
approval_revision: number | null;
|
|
624
|
+
contract_hash: string | null;
|
|
625
|
+
record_hash: string | null;
|
|
626
|
+
}
|
|
584
627
|
| undefined;
|
|
585
628
|
if (existing) {
|
|
586
|
-
if (
|
|
629
|
+
if (
|
|
630
|
+
existing.receipt_hash !== input.receiptHash ||
|
|
631
|
+
existing.approval_revision !==
|
|
632
|
+
(input.approvalProof?.revision ?? null) ||
|
|
633
|
+
existing.contract_hash !==
|
|
634
|
+
(input.approvalProof?.contractHash ?? null) ||
|
|
635
|
+
existing.record_hash !== (input.approvalProof?.recordHash ?? null)
|
|
636
|
+
) {
|
|
587
637
|
throw new Error("delivery-revision-conflict");
|
|
588
638
|
}
|
|
589
639
|
const projection = this.status(input.runId);
|
|
@@ -599,20 +649,27 @@ export class RunStore {
|
|
|
599
649
|
this.#database
|
|
600
650
|
.prepare(
|
|
601
651
|
`INSERT INTO delivery_bindings(
|
|
602
|
-
run_id, gate, revision, receipt_hash,
|
|
603
|
-
|
|
652
|
+
run_id, gate, revision, receipt_hash, approval_revision,
|
|
653
|
+
contract_hash, record_hash, operation_id
|
|
654
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
604
655
|
)
|
|
605
656
|
.run(
|
|
606
657
|
input.runId,
|
|
607
658
|
input.gate,
|
|
608
659
|
input.revision,
|
|
609
660
|
input.receiptHash,
|
|
661
|
+
input.approvalProof?.revision ?? null,
|
|
662
|
+
input.approvalProof?.contractHash ?? null,
|
|
663
|
+
input.approvalProof?.recordHash ?? null,
|
|
610
664
|
input.operationId,
|
|
611
665
|
);
|
|
612
666
|
const projection = this.#appendEvent(input.runId, "delivery-bound", {
|
|
613
667
|
gate: input.gate,
|
|
614
668
|
revision: input.revision,
|
|
615
669
|
receiptHash: input.receiptHash,
|
|
670
|
+
...(input.approvalProof
|
|
671
|
+
? { approvalProof: structuredClone(input.approvalProof) }
|
|
672
|
+
: {}),
|
|
616
673
|
});
|
|
617
674
|
this.#recordOperation(
|
|
618
675
|
input.runId,
|
|
@@ -643,6 +700,33 @@ export class RunStore {
|
|
|
643
700
|
});
|
|
644
701
|
}
|
|
645
702
|
|
|
703
|
+
bindDeliveriesAtomically(
|
|
704
|
+
inputs: readonly BindDeliveryInput[],
|
|
705
|
+
write: (database: DatabaseSync) => void,
|
|
706
|
+
): RunProjection {
|
|
707
|
+
if (inputs.length === 0) throw new Error("delivery-binding-empty");
|
|
708
|
+
for (const input of inputs) this.#requireDeliveryBinding(input);
|
|
709
|
+
const runId = inputs[0]?.runId;
|
|
710
|
+
if (
|
|
711
|
+
!runId ||
|
|
712
|
+
inputs.some(
|
|
713
|
+
(input) =>
|
|
714
|
+
input.runId !== runId ||
|
|
715
|
+
(input.lease?.token ?? null) !== (inputs[0]?.lease?.token ?? null),
|
|
716
|
+
) ||
|
|
717
|
+
typeof write !== "function"
|
|
718
|
+
) {
|
|
719
|
+
throw new Error("delivery-binding-write-invalid");
|
|
720
|
+
}
|
|
721
|
+
return this.#transaction(() => {
|
|
722
|
+
let projection: RunProjection | undefined;
|
|
723
|
+
for (const input of inputs) projection = this.#bindDelivery(input);
|
|
724
|
+
write(this.#database);
|
|
725
|
+
if (!projection) throw new Error("delivery-binding-empty");
|
|
726
|
+
return projection;
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
646
730
|
acquireLease(input: {
|
|
647
731
|
runId: string;
|
|
648
732
|
operationId: string;
|
|
@@ -839,7 +923,11 @@ export class RunStore {
|
|
|
839
923
|
if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
|
|
840
924
|
this.#assertLease(input.lease);
|
|
841
925
|
}
|
|
842
|
-
const replay = this.#operationOutcome(
|
|
926
|
+
const replay = this.#operationOutcome(
|
|
927
|
+
input.runId,
|
|
928
|
+
input.operationId,
|
|
929
|
+
"state-transitioned",
|
|
930
|
+
);
|
|
843
931
|
if (replay) return replay;
|
|
844
932
|
const current = this.status(input.runId);
|
|
845
933
|
const projection = this.#appendEvent(input.runId, "state-transitioned", {
|
package/src/subagent-activity.ts
CHANGED
|
@@ -1320,7 +1320,7 @@ export function renderActivityCall(
|
|
|
1320
1320
|
context?: ActivityRenderContext,
|
|
1321
1321
|
): Component {
|
|
1322
1322
|
const value = asRecord(args);
|
|
1323
|
-
if (typeof value?.command === "string"
|
|
1323
|
+
if (typeof value?.command === "string") {
|
|
1324
1324
|
if (context?.executionStarted || context?.isError) return new Text("");
|
|
1325
1325
|
const projected = projectWorkflowActivity(value, { state: "queued" }, 0);
|
|
1326
1326
|
const display: ActivitySnapshot = {
|