@abelxiaoxing/cadence 1.5.3 → 1.7.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 +61 -1
- package/config/plan-draft.example.json +3 -0
- package/config/plan-draft.multiple-tasks.example.json +30 -0
- package/package.json +1 -1
- package/prompts/abel-design.md +33 -3
- package/prompts/abel-implement.md +36 -4
- package/src/apply-transaction.ts +1 -9
- package/src/artifact-store.ts +1 -9
- package/src/change-verification.ts +758 -65
- package/src/contracts.ts +8 -0
- package/src/delivery-compiler.ts +452 -9
- package/src/design-control.ts +2 -2
- package/src/design-diagnostics.ts +6 -4
- package/src/directory-sync.ts +36 -0
- package/src/durable-contracts.ts +19 -3
- package/src/durable-workflow.ts +47 -38
- package/src/implement-continuation.ts +559 -0
- package/src/implement-graph.ts +114 -0
- package/src/implement-plan.ts +2 -0
- package/src/index.ts +179 -1
- package/src/operator-cli.mjs +12 -7
- package/src/package-state.ts +309 -0
- package/src/package-verification.ts +17 -1
- package/src/package-workflow.ts +219 -171
- package/src/phase-execution.ts +7 -1
- package/src/plan-draft-summary.ts +385 -7
- package/src/plan-draft.ts +9 -0
- package/src/run-store.ts +39 -15
- package/src/storage-schema.ts +14 -6
- package/src/subagent-activity.ts +3 -1
- package/src/verification-capability.ts +17 -5
- package/src/verification-environment.ts +20 -2
- package/src/workflow-policy.ts +296 -5
- package/src/workflow-recovery-policy.ts +2 -0
- package/src/workflow-state-machine.ts +400 -131
- package/src/workflow-status.ts +251 -59
- package/src/workspace-io-worker.mjs +48 -42
- package/src/workspace-store.ts +1 -9
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
projectDesignDiagnostic,
|
|
5
5
|
type SafeDesignDiagnostic,
|
|
6
6
|
} from "./design-diagnostics.ts";
|
|
7
|
+
import { PackageStateError } from "./package-state.ts";
|
|
7
8
|
import { openPackageWorkflowService } from "./package-workflow.ts";
|
|
8
9
|
import { packageContext } from "./pi-adapter.ts";
|
|
9
10
|
|
|
@@ -47,6 +48,11 @@ import {
|
|
|
47
48
|
DesignFinalizationError,
|
|
48
49
|
validateDesignControlRequest,
|
|
49
50
|
} from "./design-control.ts";
|
|
51
|
+
import {
|
|
52
|
+
ImplementContinuationDriver,
|
|
53
|
+
implementChangeFromInvocation,
|
|
54
|
+
implementPromptBindsChange,
|
|
55
|
+
} from "./implement-continuation.ts";
|
|
50
56
|
import { canonicalJson } from "./implement-graph.ts";
|
|
51
57
|
import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
|
|
52
58
|
|
|
@@ -457,6 +463,14 @@ function classifyDesignFailure(
|
|
|
457
463
|
field?: string;
|
|
458
464
|
} {
|
|
459
465
|
const message = error instanceof Error ? error.message : "";
|
|
466
|
+
if (error instanceof PackageStateError) {
|
|
467
|
+
return {
|
|
468
|
+
code: error.code,
|
|
469
|
+
category: "storage",
|
|
470
|
+
retryable: false,
|
|
471
|
+
field: "packageVersion",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
460
474
|
if (error instanceof DesignControlValidationError) {
|
|
461
475
|
return {
|
|
462
476
|
code: "invalid-design-control-request",
|
|
@@ -698,7 +712,9 @@ export function registerWorkflowControl(
|
|
|
698
712
|
const engines = new Map<string, Promise<WorkflowControlEngine>>();
|
|
699
713
|
const designOperations = new Set<Promise<void>>();
|
|
700
714
|
const designFailures = new Map<string, SafeDesignFailure>();
|
|
715
|
+
const implementContinuation = new ImplementContinuationDriver();
|
|
701
716
|
let pendingPrompt: EligiblePrompt | undefined;
|
|
717
|
+
let pendingImplementChange: string | undefined;
|
|
702
718
|
let pendingInit = false;
|
|
703
719
|
let exitingStage = false;
|
|
704
720
|
let activePrompt: EligiblePrompt | undefined;
|
|
@@ -788,6 +804,7 @@ export function registerWorkflowControl(
|
|
|
788
804
|
};
|
|
789
805
|
const deactivateStage = async () => {
|
|
790
806
|
activePrompt = undefined;
|
|
807
|
+
implementContinuation.deactivate();
|
|
791
808
|
await packetRuntime.drain();
|
|
792
809
|
deactivate();
|
|
793
810
|
};
|
|
@@ -1229,6 +1246,25 @@ export function registerWorkflowControl(
|
|
|
1229
1246
|
if (exitingStage || !activation.isActive())
|
|
1230
1247
|
throw new Error("stage-control-mismatch");
|
|
1231
1248
|
} catch (error) {
|
|
1249
|
+
if (error instanceof PackageStateError) {
|
|
1250
|
+
const payload = {
|
|
1251
|
+
stage: "abel-implement",
|
|
1252
|
+
change: validation.value.change,
|
|
1253
|
+
state: "paused",
|
|
1254
|
+
durable: false,
|
|
1255
|
+
completed: false,
|
|
1256
|
+
pause: { code: error.code },
|
|
1257
|
+
legalCommands: ["status", "start"],
|
|
1258
|
+
tasks: [],
|
|
1259
|
+
queue: [],
|
|
1260
|
+
};
|
|
1261
|
+
return {
|
|
1262
|
+
content: [
|
|
1263
|
+
{ type: "text" as const, text: JSON.stringify(payload) },
|
|
1264
|
+
],
|
|
1265
|
+
details: payload,
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1232
1268
|
if (!(error instanceof RunStoreFormatError)) throw error;
|
|
1233
1269
|
const payload = runStoreUnavailableStatus(error, validation.value);
|
|
1234
1270
|
return {
|
|
@@ -1321,6 +1357,13 @@ export function registerWorkflowControl(
|
|
|
1321
1357
|
registerDispatchTool("packet");
|
|
1322
1358
|
|
|
1323
1359
|
pi.on("tool_result", (event) => {
|
|
1360
|
+
implementContinuation.noteToolResult({
|
|
1361
|
+
toolName: event.toolName,
|
|
1362
|
+
input: event.input,
|
|
1363
|
+
content: event.content,
|
|
1364
|
+
details: event.details,
|
|
1365
|
+
isError: event.isError,
|
|
1366
|
+
});
|
|
1324
1367
|
if (
|
|
1325
1368
|
event.toolName !== DISPATCH_TOOL ||
|
|
1326
1369
|
!event.isError ||
|
|
@@ -1357,7 +1400,9 @@ export function registerWorkflowControl(
|
|
|
1357
1400
|
};
|
|
1358
1401
|
});
|
|
1359
1402
|
pi.on("input", (event) => {
|
|
1403
|
+
implementContinuation.noteInput();
|
|
1360
1404
|
pendingPrompt = undefined;
|
|
1405
|
+
pendingImplementChange = undefined;
|
|
1361
1406
|
pendingInit = false;
|
|
1362
1407
|
const prompt = invokedPrompt(event.text);
|
|
1363
1408
|
const init = /^\/abel-init(?:\s|$)/u.test(event.text);
|
|
@@ -1367,6 +1412,10 @@ export function registerWorkflowControl(
|
|
|
1367
1412
|
return { action: prompt || init ? "handled" : "continue" };
|
|
1368
1413
|
}
|
|
1369
1414
|
pendingPrompt = prompt;
|
|
1415
|
+
pendingImplementChange =
|
|
1416
|
+
prompt === "abel-implement"
|
|
1417
|
+
? implementChangeFromInvocation(event.text)
|
|
1418
|
+
: undefined;
|
|
1370
1419
|
pendingInit = init && hasPackageProvenance(pi, "abel-init");
|
|
1371
1420
|
if (
|
|
1372
1421
|
activePrompt &&
|
|
@@ -1380,11 +1429,13 @@ export function registerWorkflowControl(
|
|
|
1380
1429
|
}
|
|
1381
1430
|
return { action: "continue" };
|
|
1382
1431
|
});
|
|
1383
|
-
pi.on("before_agent_start", (event) => {
|
|
1432
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
1384
1433
|
const prompt = pendingPrompt;
|
|
1434
|
+
const implementChange = pendingImplementChange;
|
|
1385
1435
|
const init =
|
|
1386
1436
|
pendingInit && hasExpandedPromptMarker(event.prompt, "abel-init");
|
|
1387
1437
|
pendingPrompt = undefined;
|
|
1438
|
+
pendingImplementChange = undefined;
|
|
1388
1439
|
pendingInit = false;
|
|
1389
1440
|
const verified =
|
|
1390
1441
|
prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
|
|
@@ -1394,10 +1445,30 @@ export function registerWorkflowControl(
|
|
|
1394
1445
|
}
|
|
1395
1446
|
registerDispatchTool(prompt === "abel-implement" ? "command" : "packet");
|
|
1396
1447
|
activePrompt = prompt;
|
|
1448
|
+
if (prompt === "abel-implement") {
|
|
1449
|
+
if (
|
|
1450
|
+
implementChange &&
|
|
1451
|
+
implementPromptBindsChange(event.prompt, implementChange) &&
|
|
1452
|
+
typeof ctx.cwd === "string"
|
|
1453
|
+
)
|
|
1454
|
+
implementContinuation.activate({
|
|
1455
|
+
cwd: ctx.cwd,
|
|
1456
|
+
change: implementChange,
|
|
1457
|
+
});
|
|
1458
|
+
else implementContinuation.deactivate();
|
|
1459
|
+
} else {
|
|
1460
|
+
implementContinuation.deactivate();
|
|
1461
|
+
}
|
|
1397
1462
|
}
|
|
1398
1463
|
if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
|
|
1399
1464
|
if (activation.isActive() && activePrompt === "abel-design")
|
|
1400
1465
|
enforceDesignTools();
|
|
1466
|
+
if (
|
|
1467
|
+
activation.isActive() &&
|
|
1468
|
+
activePrompt === "abel-implement" &&
|
|
1469
|
+
typeof ctx.cwd === "string"
|
|
1470
|
+
)
|
|
1471
|
+
implementContinuation.beginParentTurn(ctx.cwd);
|
|
1401
1472
|
const boundary = activePrompt
|
|
1402
1473
|
? `Abel stage ${activePrompt} is active only for the invoked task and its direct follow-ups. If the user ends it or requests an unrelated task, first call abel_dispatch with {"action":"finish"}, then handle that task normally with the restored tools. A successful finish ends stage authority immediately, including within this turn. Do not extend Gates or workflow rules to that task. A direct Gate answer or same-task continuation stays in this stage. Never invoke another Abel stage automatically.`
|
|
1403
1474
|
: init
|
|
@@ -1417,10 +1488,113 @@ export function registerWorkflowControl(
|
|
|
1417
1488
|
}
|
|
1418
1489
|
return { ...event.payload, parallel_tool_calls: true };
|
|
1419
1490
|
});
|
|
1491
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
1492
|
+
// The host may replace its current signal after abort/settlement. Retain
|
|
1493
|
+
// this run's signal so a late status result cannot revive an aborted turn.
|
|
1494
|
+
const runSignal = ctx.signal;
|
|
1495
|
+
const aborted = () => runSignal?.aborted || ctx.signal?.aborted;
|
|
1496
|
+
if (
|
|
1497
|
+
aborted() ||
|
|
1498
|
+
exitingStage ||
|
|
1499
|
+
!activation.isActive() ||
|
|
1500
|
+
activePrompt !== "abel-implement"
|
|
1501
|
+
) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (
|
|
1505
|
+
typeof ctx.hasPendingMessages === "function" &&
|
|
1506
|
+
ctx.hasPendingMessages()
|
|
1507
|
+
) {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const probe = implementContinuation.prepareSettlement({
|
|
1511
|
+
cwd: ctx.cwd,
|
|
1512
|
+
messages: event.messages,
|
|
1513
|
+
});
|
|
1514
|
+
if (!probe) return;
|
|
1515
|
+
|
|
1516
|
+
let status: Record<string, unknown>;
|
|
1517
|
+
try {
|
|
1518
|
+
const engine = await engineFor(ctx);
|
|
1519
|
+
if (
|
|
1520
|
+
aborted() ||
|
|
1521
|
+
exitingStage ||
|
|
1522
|
+
!activation.isActive() ||
|
|
1523
|
+
activePrompt !== "abel-implement"
|
|
1524
|
+
) {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
status = await engine.execute(
|
|
1528
|
+
{
|
|
1529
|
+
command: "status",
|
|
1530
|
+
stage: "abel-implement",
|
|
1531
|
+
change: probe.change,
|
|
1532
|
+
},
|
|
1533
|
+
ctx,
|
|
1534
|
+
runSignal,
|
|
1535
|
+
);
|
|
1536
|
+
if (
|
|
1537
|
+
aborted() ||
|
|
1538
|
+
(typeof ctx.hasPendingMessages === "function" &&
|
|
1539
|
+
ctx.hasPendingMessages())
|
|
1540
|
+
) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
} catch {
|
|
1544
|
+
// A host reminder cannot replace a trustworthy local status read.
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const decision = implementContinuation.finishSettlement(probe, status);
|
|
1549
|
+
if (decision?.kind === "terminal") {
|
|
1550
|
+
await deactivateStage();
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (!decision) return;
|
|
1554
|
+
if (
|
|
1555
|
+
aborted() ||
|
|
1556
|
+
(typeof ctx.hasPendingMessages === "function" && ctx.hasPendingMessages())
|
|
1557
|
+
) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if (decision.kind === "stalled") {
|
|
1562
|
+
pi.sendMessage(
|
|
1563
|
+
{
|
|
1564
|
+
customType: "abel-implement-continuation-stalled",
|
|
1565
|
+
content: [{ type: "text", text: decision.message }],
|
|
1566
|
+
display: true,
|
|
1567
|
+
details: {
|
|
1568
|
+
code: decision.code,
|
|
1569
|
+
change: decision.change,
|
|
1570
|
+
state: decision.state,
|
|
1571
|
+
statusFingerprint: decision.statusFingerprint,
|
|
1572
|
+
},
|
|
1573
|
+
},
|
|
1574
|
+
{ triggerTurn: false },
|
|
1575
|
+
);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
pi.sendMessage(
|
|
1580
|
+
{
|
|
1581
|
+
customType: "abel-implement-continuation",
|
|
1582
|
+
content: [{ type: "text", text: decision.message }],
|
|
1583
|
+
display: false,
|
|
1584
|
+
details: {
|
|
1585
|
+
change: decision.change,
|
|
1586
|
+
statusFingerprint: decision.statusFingerprint,
|
|
1587
|
+
},
|
|
1588
|
+
},
|
|
1589
|
+
{ triggerTurn: true, deliverAs: "followUp" },
|
|
1590
|
+
);
|
|
1591
|
+
});
|
|
1420
1592
|
pi.on("session_start", async (_event, ctx) => {
|
|
1421
1593
|
pendingPrompt = undefined;
|
|
1594
|
+
pendingImplementChange = undefined;
|
|
1422
1595
|
pendingInit = false;
|
|
1423
1596
|
activePrompt = undefined;
|
|
1597
|
+
implementContinuation.deactivate();
|
|
1424
1598
|
designFailures.clear();
|
|
1425
1599
|
activity.detach();
|
|
1426
1600
|
await packetRuntime.drain();
|
|
@@ -1429,7 +1603,11 @@ export function registerWorkflowControl(
|
|
|
1429
1603
|
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
1430
1604
|
});
|
|
1431
1605
|
pi.on("session_shutdown", async () => {
|
|
1606
|
+
pendingPrompt = undefined;
|
|
1607
|
+
pendingImplementChange = undefined;
|
|
1608
|
+
pendingInit = false;
|
|
1432
1609
|
activePrompt = undefined;
|
|
1610
|
+
implementContinuation.deactivate();
|
|
1433
1611
|
designFailures.clear();
|
|
1434
1612
|
activity.detach();
|
|
1435
1613
|
await packetRuntime.drain();
|
package/src/operator-cli.mjs
CHANGED
|
@@ -652,9 +652,6 @@ function observeSafePath(root, relative) {
|
|
|
652
652
|
}
|
|
653
653
|
return { kind: "unsafe", reason: "invalid-path" };
|
|
654
654
|
}
|
|
655
|
-
function isSafeRegularFile(root, relative) {
|
|
656
|
-
return observeSafePath(root, relative).kind === "file";
|
|
657
|
-
}
|
|
658
655
|
|
|
659
656
|
// src/file-snapshot.ts
|
|
660
657
|
function sha256(bytes) {
|
|
@@ -697,10 +694,16 @@ function capabilitySuccess(verificationId, runnerBindings = []) {
|
|
|
697
694
|
}))
|
|
698
695
|
};
|
|
699
696
|
}
|
|
700
|
-
function adapterFailure(verificationId, code, message) {
|
|
697
|
+
function adapterFailure(verificationId, code, message, inputObservation) {
|
|
701
698
|
return {
|
|
702
699
|
ok: false,
|
|
703
|
-
diagnostic: {
|
|
700
|
+
diagnostic: {
|
|
701
|
+
kind: "verification-adapter",
|
|
702
|
+
code,
|
|
703
|
+
verificationId,
|
|
704
|
+
message,
|
|
705
|
+
...inputObservation ? { inputObservation } : {}
|
|
706
|
+
}
|
|
704
707
|
};
|
|
705
708
|
}
|
|
706
709
|
function fileStatus(root, relative) {
|
|
@@ -1223,8 +1226,10 @@ function validateCapability(root, value, options, requireInputAvailability) {
|
|
|
1223
1226
|
function validateAcceptedVerificationCapability(root, verification, dependencyOwner, runnerEnvironment, requireInputAvailability, executionWritePaths) {
|
|
1224
1227
|
if (requireInputAvailability) {
|
|
1225
1228
|
for (const input of verificationInputPaths(verification)) {
|
|
1226
|
-
|
|
1227
|
-
|
|
1229
|
+
const status = fileStatus(root, input);
|
|
1230
|
+
if (status !== "regular") {
|
|
1231
|
+
const inputObservation = { path: input, kind: status };
|
|
1232
|
+
return adapterFailure(verification.id, status === "absent" ? "input-missing" : "input-unsafe", `verification input ${input} is ${status}`, inputObservation);
|
|
1228
1233
|
}
|
|
1229
1234
|
}
|
|
1230
1235
|
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
fsyncSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { DatabaseSync } from "node:sqlite";
|
|
17
|
+
import { syncDirectory } from "./directory-sync.ts";
|
|
18
|
+
import { RunStore } from "./run-store.ts";
|
|
19
|
+
import { prepareStateRoot, type ResolvedStateRoot } from "./state-root.ts";
|
|
20
|
+
|
|
21
|
+
export class PackageStateError extends Error {
|
|
22
|
+
readonly code: string;
|
|
23
|
+
constructor(code: string) {
|
|
24
|
+
super(code);
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export interface PackageStateNotice {
|
|
29
|
+
reason:
|
|
30
|
+
| "package-upgrade"
|
|
31
|
+
| "unversioned-store"
|
|
32
|
+
| "unreadable-store"
|
|
33
|
+
| "missing-store";
|
|
34
|
+
previousVersion: string | null;
|
|
35
|
+
currentVersion: string;
|
|
36
|
+
backupPath: string;
|
|
37
|
+
oldRunsAbandoned: true;
|
|
38
|
+
workspaceRestored: false;
|
|
39
|
+
warning: string;
|
|
40
|
+
}
|
|
41
|
+
interface ResetIntent
|
|
42
|
+
extends Omit<
|
|
43
|
+
PackageStateNotice,
|
|
44
|
+
"oldRunsAbandoned" | "workspaceRestored" | "warning"
|
|
45
|
+
> {
|
|
46
|
+
backupName: string;
|
|
47
|
+
}
|
|
48
|
+
const fail = (code: string): never => {
|
|
49
|
+
throw new PackageStateError(code);
|
|
50
|
+
};
|
|
51
|
+
const versionParts = (version: string): number[] => {
|
|
52
|
+
if (!/^\d+\.\d+\.\d+$/u.test(version)) fail("package-state-version-invalid");
|
|
53
|
+
const parts = version.split(".").map(Number);
|
|
54
|
+
if (parts.some((part) => !Number.isSafeInteger(part)))
|
|
55
|
+
fail("package-state-version-invalid");
|
|
56
|
+
return parts;
|
|
57
|
+
};
|
|
58
|
+
function compareVersions(left: string, right: string): number {
|
|
59
|
+
const a = versionParts(left),
|
|
60
|
+
b = versionParts(right);
|
|
61
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1;
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
function regular(file: string): boolean {
|
|
65
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
66
|
+
if (!stat) return false;
|
|
67
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
68
|
+
fail("package-state-path-unsafe");
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
function table(db: DatabaseSync, name: string): boolean {
|
|
72
|
+
return !!db
|
|
73
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
74
|
+
.get(name);
|
|
75
|
+
}
|
|
76
|
+
function storedVersion(db: DatabaseSync): string | null {
|
|
77
|
+
if (!table(db, "cadence_package_version")) return null;
|
|
78
|
+
const rows = db.prepare("SELECT version FROM cadence_package_version").all();
|
|
79
|
+
if (rows.length !== 1 || typeof rows[0].version !== "string")
|
|
80
|
+
fail("package-state-version-invalid");
|
|
81
|
+
const version = String(rows[0].version);
|
|
82
|
+
versionParts(version);
|
|
83
|
+
return version;
|
|
84
|
+
}
|
|
85
|
+
// Only inspect the version. Old task/lease/apply records are deliberately not
|
|
86
|
+
// interpreted: this policy abandons them, including malformed or unreadable facts.
|
|
87
|
+
function inspectVersion(file: string): {
|
|
88
|
+
version: string | null;
|
|
89
|
+
unreadable: boolean;
|
|
90
|
+
} {
|
|
91
|
+
let db: DatabaseSync | undefined;
|
|
92
|
+
try {
|
|
93
|
+
db = new DatabaseSync(file, { readOnly: true });
|
|
94
|
+
return { version: storedVersion(db), unreadable: false };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const failure = error as { errcode?: number; code?: string };
|
|
97
|
+
// An actual SQLite lock is not a stale durable lease record. Do not move
|
|
98
|
+
// storage while an uncoordinated old opener is observably using it.
|
|
99
|
+
if (failure.errcode !== undefined && [5, 6].includes(failure.errcode & 255))
|
|
100
|
+
fail("package-state-in-use");
|
|
101
|
+
if (
|
|
102
|
+
(error instanceof PackageStateError &&
|
|
103
|
+
error.code === "package-state-version-invalid") ||
|
|
104
|
+
(failure.errcode !== undefined &&
|
|
105
|
+
[1, 11, 26].includes(failure.errcode & 255))
|
|
106
|
+
)
|
|
107
|
+
return { version: null, unreadable: true };
|
|
108
|
+
// Schema errors / SQLITE_CORRUPT / SQLITE_NOTADB mean opaque old bytes.
|
|
109
|
+
// Permissions, disk I/O, resource exhaustion and unknown errors do not.
|
|
110
|
+
return fail("package-state-reset-unavailable");
|
|
111
|
+
} finally {
|
|
112
|
+
db?.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasOldResources(state: ResolvedStateRoot): boolean {
|
|
117
|
+
// Cover the production run-data layout and unknown historical resources.
|
|
118
|
+
// Never descend through resource links; the whole root is renamed as-is.
|
|
119
|
+
const emptyScaffolds = new Set(["artifacts", "workspaces", "transactions"]);
|
|
120
|
+
return readdirSync(state.rootDir).some((name) => {
|
|
121
|
+
const file = path.join(state.rootDir, name);
|
|
122
|
+
const stat = lstatSync(file);
|
|
123
|
+
return (
|
|
124
|
+
!emptyScaffolds.has(name) ||
|
|
125
|
+
!stat.isDirectory() ||
|
|
126
|
+
stat.isSymbolicLink() ||
|
|
127
|
+
readdirSync(file).length > 0
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const RESET_WARNING =
|
|
133
|
+
"Old runs and approval evidence are abandoned, not resumed. Stop all old hosts and execution processes before resetting; the lock cannot fence older versions or detached descendants. The main workspace is not restored: interrupted apply may have left partial changes. Inspect Git diff before starting new work.";
|
|
134
|
+
|
|
135
|
+
function persistIntent(file: string, intent: ResetIntent): void {
|
|
136
|
+
const temp = `${file}.tmp`;
|
|
137
|
+
regular(temp);
|
|
138
|
+
const fd = openSync(temp, "w", 0o600);
|
|
139
|
+
try {
|
|
140
|
+
writeFileSync(fd, JSON.stringify(intent));
|
|
141
|
+
fsyncSync(fd);
|
|
142
|
+
} finally {
|
|
143
|
+
closeSync(fd);
|
|
144
|
+
}
|
|
145
|
+
renameSync(temp, file);
|
|
146
|
+
syncDirectory(path.dirname(file));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Package boundary only: low-level storage and read-only operator inspection never reset state.
|
|
150
|
+
* The external SQLite write lock is held until every service connection has closed.
|
|
151
|
+
* SQLite releases it on process death; no PID guessing or stale-lock deletion is used.
|
|
152
|
+
*/
|
|
153
|
+
export function acquirePackageState(
|
|
154
|
+
unresolved: ResolvedStateRoot,
|
|
155
|
+
version = String(
|
|
156
|
+
JSON.parse(
|
|
157
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
158
|
+
).version,
|
|
159
|
+
),
|
|
160
|
+
): { notice?: PackageStateNotice; close(): void } {
|
|
161
|
+
versionParts(version);
|
|
162
|
+
const state = prepareStateRoot(unresolved);
|
|
163
|
+
for (const suffix of ["-wal", "-shm", "-journal"])
|
|
164
|
+
regular(state.databasePath + suffix);
|
|
165
|
+
const coordinator = path.join(
|
|
166
|
+
state.rootsDir,
|
|
167
|
+
`${state.consumerRootHash}.package-state`,
|
|
168
|
+
);
|
|
169
|
+
const stat = lstatSync(coordinator, { throwIfNoEntry: false });
|
|
170
|
+
if (stat && (!stat.isDirectory() || stat.isSymbolicLink()))
|
|
171
|
+
fail("package-state-path-unsafe");
|
|
172
|
+
mkdirSync(coordinator, { recursive: true, mode: 0o700 });
|
|
173
|
+
const lockFile = path.join(coordinator, "lock.sqlite3");
|
|
174
|
+
for (const suffix of ["", "-journal", "-wal", "-shm"])
|
|
175
|
+
regular(lockFile + suffix);
|
|
176
|
+
if (!existsSync(lockFile)) {
|
|
177
|
+
try {
|
|
178
|
+
closeSync(openSync(lockFile, "wx", 0o600));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const lock = new DatabaseSync(lockFile);
|
|
184
|
+
let closed = false;
|
|
185
|
+
const close = () => {
|
|
186
|
+
if (!closed) {
|
|
187
|
+
closed = true;
|
|
188
|
+
lock.close();
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
lock.exec("PRAGMA busy_timeout = 1; BEGIN IMMEDIATE");
|
|
193
|
+
} catch {
|
|
194
|
+
close();
|
|
195
|
+
fail("package-state-in-use");
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const intentFile = path.join(coordinator, "reset.json");
|
|
199
|
+
let intent: ResetIntent | undefined;
|
|
200
|
+
if (regular(intentFile)) {
|
|
201
|
+
intent = JSON.parse(readFileSync(intentFile, "utf8")) as ResetIntent;
|
|
202
|
+
if (
|
|
203
|
+
!intent ||
|
|
204
|
+
!/^backup-[a-f0-9-]{36}$/u.test(intent.backupName) ||
|
|
205
|
+
intent.backupPath !== path.join(coordinator, intent.backupName) ||
|
|
206
|
+
![
|
|
207
|
+
"package-upgrade",
|
|
208
|
+
"unversioned-store",
|
|
209
|
+
"unreadable-store",
|
|
210
|
+
"missing-store",
|
|
211
|
+
].includes(intent.reason) ||
|
|
212
|
+
(intent.previousVersion !== null &&
|
|
213
|
+
typeof intent.previousVersion !== "string")
|
|
214
|
+
)
|
|
215
|
+
fail("package-state-reset-intent-invalid");
|
|
216
|
+
if (intent.currentVersion !== version)
|
|
217
|
+
fail("package-state-reset-version-mismatch");
|
|
218
|
+
}
|
|
219
|
+
let fresh = !regular(state.databasePath);
|
|
220
|
+
if (!intent) {
|
|
221
|
+
const observed = fresh
|
|
222
|
+
? { version: null, unreadable: false }
|
|
223
|
+
: inspectVersion(state.databasePath);
|
|
224
|
+
if (observed.version === version) return { close };
|
|
225
|
+
if (
|
|
226
|
+
observed.version !== null &&
|
|
227
|
+
compareVersions(observed.version, version) > 0
|
|
228
|
+
)
|
|
229
|
+
fail("package-state-downgrade-blocked");
|
|
230
|
+
if (!fresh || hasOldResources(state)) {
|
|
231
|
+
const backupName = `backup-${randomUUID()}`;
|
|
232
|
+
intent = {
|
|
233
|
+
backupName,
|
|
234
|
+
backupPath: path.join(coordinator, backupName),
|
|
235
|
+
previousVersion: observed.version,
|
|
236
|
+
currentVersion: version,
|
|
237
|
+
reason: fresh
|
|
238
|
+
? "missing-store"
|
|
239
|
+
: observed.unreadable
|
|
240
|
+
? "unreadable-store"
|
|
241
|
+
: observed.version === null
|
|
242
|
+
? "unversioned-store"
|
|
243
|
+
: "package-upgrade",
|
|
244
|
+
};
|
|
245
|
+
persistIntent(intentFile, intent);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (intent) {
|
|
249
|
+
const backup = lstatSync(intent.backupPath, { throwIfNoEntry: false });
|
|
250
|
+
if (backup && (!backup.isDirectory() || backup.isSymbolicLink()))
|
|
251
|
+
fail("package-state-path-unsafe");
|
|
252
|
+
if (!backup) {
|
|
253
|
+
// Abandon the old namespace as opaque bytes, including run-data and
|
|
254
|
+
// SQLite sidecars. No transaction replay or main-workspace rollback.
|
|
255
|
+
renameSync(state.rootDir, intent.backupPath);
|
|
256
|
+
syncDirectory(coordinator);
|
|
257
|
+
syncDirectory(state.rootsDir);
|
|
258
|
+
}
|
|
259
|
+
prepareStateRoot(state);
|
|
260
|
+
fresh = true;
|
|
261
|
+
}
|
|
262
|
+
if (fresh) {
|
|
263
|
+
RunStore.open(state).close();
|
|
264
|
+
const db = new DatabaseSync(state.databasePath);
|
|
265
|
+
try {
|
|
266
|
+
db.exec(
|
|
267
|
+
"PRAGMA synchronous = FULL; BEGIN IMMEDIATE; CREATE TABLE IF NOT EXISTS cadence_package_version (version TEXT NOT NULL) STRICT",
|
|
268
|
+
);
|
|
269
|
+
const existing = db
|
|
270
|
+
.prepare("SELECT version FROM cadence_package_version")
|
|
271
|
+
.all();
|
|
272
|
+
if (existing.length === 0)
|
|
273
|
+
db.prepare("INSERT INTO cadence_package_version VALUES (?)").run(
|
|
274
|
+
version,
|
|
275
|
+
);
|
|
276
|
+
else if (existing.length !== 1 || existing[0].version !== version)
|
|
277
|
+
fail("package-state-reset-version-mismatch");
|
|
278
|
+
db.exec("COMMIT");
|
|
279
|
+
} finally {
|
|
280
|
+
db.close();
|
|
281
|
+
}
|
|
282
|
+
syncDirectory(state.rootDir);
|
|
283
|
+
}
|
|
284
|
+
if (intent) {
|
|
285
|
+
unlinkSync(intentFile);
|
|
286
|
+
syncDirectory(coordinator);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
close,
|
|
290
|
+
...(intent
|
|
291
|
+
? {
|
|
292
|
+
notice: {
|
|
293
|
+
reason: intent.reason,
|
|
294
|
+
previousVersion: intent.previousVersion,
|
|
295
|
+
currentVersion: version,
|
|
296
|
+
backupPath: intent.backupPath,
|
|
297
|
+
oldRunsAbandoned: true,
|
|
298
|
+
workspaceRestored: false,
|
|
299
|
+
warning: RESET_WARNING,
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
: {}),
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
close();
|
|
306
|
+
if (error instanceof PackageStateError) throw error;
|
|
307
|
+
throw new PackageStateError("package-state-reset-unavailable");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import {
|
|
5
5
|
type AtomicVerificationContract,
|
|
6
6
|
type StructuredVerificationContract,
|
|
7
|
+
type VerificationInputObservation,
|
|
7
8
|
type VerificationObservation,
|
|
8
9
|
verificationSteps,
|
|
9
10
|
} from "./contracts.ts";
|
|
@@ -321,11 +322,13 @@ export async function executePackageVerification(input: {
|
|
|
321
322
|
const unavailable = (
|
|
322
323
|
code: string,
|
|
323
324
|
category: "environment" | "adapter" | "resource" = "adapter",
|
|
325
|
+
inputObservation?: VerificationInputObservation,
|
|
324
326
|
): VerificationObservation => ({
|
|
325
327
|
kind: "unavailable",
|
|
326
328
|
category,
|
|
327
329
|
code,
|
|
328
330
|
verificationId: input.verification.id,
|
|
331
|
+
...(inputObservation ? { inputObservation } : {}),
|
|
329
332
|
...(executionDiagnostic
|
|
330
333
|
? {
|
|
331
334
|
diagnostic: {
|
|
@@ -349,7 +352,14 @@ export async function executePackageVerification(input: {
|
|
|
349
352
|
executionWritePaths: input.executionWritePaths,
|
|
350
353
|
},
|
|
351
354
|
);
|
|
352
|
-
if (!capability.ok)
|
|
355
|
+
if (!capability.ok)
|
|
356
|
+
return unavailable(
|
|
357
|
+
capability.diagnostic.code,
|
|
358
|
+
"adapter",
|
|
359
|
+
capability.diagnostic.kind === "verification-adapter"
|
|
360
|
+
? capability.diagnostic.inputObservation
|
|
361
|
+
: undefined,
|
|
362
|
+
);
|
|
353
363
|
let environment: Awaited<ReturnType<typeof prepareVerificationEnvironment>>;
|
|
354
364
|
try {
|
|
355
365
|
environment = await prepareVerificationEnvironment(
|
|
@@ -552,6 +562,9 @@ export function phaseVerificationResult(result: VerificationObservation) {
|
|
|
552
562
|
kind: "paused" as const,
|
|
553
563
|
code: result.code,
|
|
554
564
|
category: result.category,
|
|
565
|
+
...(result.inputObservation
|
|
566
|
+
? { inputObservation: result.inputObservation }
|
|
567
|
+
: {}),
|
|
555
568
|
};
|
|
556
569
|
}
|
|
557
570
|
|
|
@@ -583,5 +596,8 @@ export function changeVerificationResult(result: VerificationObservation) {
|
|
|
583
596
|
? ("verification-adapter" as const)
|
|
584
597
|
: ("environment" as const),
|
|
585
598
|
code: result.code,
|
|
599
|
+
...(result.inputObservation
|
|
600
|
+
? { inputObservation: result.inputObservation }
|
|
601
|
+
: {}),
|
|
586
602
|
};
|
|
587
603
|
}
|