@abelxiaoxing/cadence 1.3.0 → 1.4.1
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 +172 -13
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/package.json +9 -5
- package/prompts/abel-design.md +141 -38
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/activity-contracts.ts +82 -0
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +33 -12
- package/src/candidate-workspace.ts +74 -0
- package/src/canonical.ts +16 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +273 -0
- package/src/change-verification.ts +896 -0
- package/src/child-model.ts +129 -0
- package/src/child-session.ts +296 -328
- package/src/contracts.ts +325 -109
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +241 -141
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +182 -15
- package/src/design-diagnostics.ts +150 -0
- package/src/design-journal.ts +216 -72
- package/src/durable-contracts.ts +210 -0
- package/src/durable-workflow.ts +963 -0
- package/src/evidence-draft.ts +122 -0
- package/src/implement-graph.ts +14 -16
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +341 -1405
- package/src/isolation-backend.ts +84 -23
- package/src/model-source.ts +22 -0
- package/src/package-candidate.ts +224 -0
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/package-workflow.ts +253 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +146 -0
- package/src/plan-draft.ts +493 -0
- package/src/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +46 -67
- package/src/submit-tool.ts +40 -61
- package/src/task-ledger.ts +14 -55
- package/src/transport-budget.ts +32 -0
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +134 -91
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1388 -0
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +4086 -0
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +991 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- package/skills/abel-workflow/SKILL.md +0 -205
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
package/src/subagent-activity.ts
CHANGED
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
import type {
|
|
13
13
|
PacketActivityEvent,
|
|
14
14
|
PacketActivityObserver,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
} from "./activity-contracts.ts";
|
|
16
|
+
import {
|
|
17
|
+
projectDesignDiagnostic,
|
|
18
|
+
type SafeDesignDiagnostic,
|
|
19
|
+
} from "./design-diagnostics.ts";
|
|
17
20
|
|
|
18
21
|
export const ACTIVITY_DETAILS_KEY = "activityDisplay" as const;
|
|
19
22
|
export const ACTIVITY_WIDGET_KEY = "abel-subagents" as const;
|
|
@@ -22,51 +25,20 @@ export const ACTIVITY_REFRESH_MS = 100;
|
|
|
22
25
|
export const ACTIVITY_WIDGET_MAX_LINES = 12;
|
|
23
26
|
export const ACTIVITY_WIDGET_MIN_WIDTH = 3;
|
|
24
27
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"rejected",
|
|
40
|
-
"completed",
|
|
41
|
-
] as const;
|
|
42
|
-
|
|
43
|
-
export type WorkflowActivityState = (typeof WORKFLOW_ACTIVITY_STATES)[number];
|
|
44
|
-
export type ActivityState = PacketActivityState | WorkflowActivityState;
|
|
45
|
-
export type TerminalActivityState =
|
|
46
|
-
| Exclude<
|
|
47
|
-
PacketActivityState,
|
|
48
|
-
| "queued"
|
|
49
|
-
| "connecting"
|
|
50
|
-
| "waiting-first-response"
|
|
51
|
-
| "running"
|
|
52
|
-
| "retrying"
|
|
53
|
-
>
|
|
54
|
-
| WorkflowActivityState;
|
|
55
|
-
|
|
56
|
-
export interface WorkflowActivityUpdate {
|
|
57
|
-
state: WorkflowActivityState;
|
|
58
|
-
stage?: "abel-design" | "abel-implement";
|
|
59
|
-
runId?: string;
|
|
60
|
-
change?: string;
|
|
61
|
-
taskId?: string;
|
|
62
|
-
phase?: string;
|
|
63
|
-
objective?: string;
|
|
64
|
-
code?: string;
|
|
65
|
-
attempt?: number;
|
|
66
|
-
maxAttempts?: number;
|
|
67
|
-
wait?: string;
|
|
68
|
-
legalCommands?: string[];
|
|
69
|
-
}
|
|
28
|
+
export type {
|
|
29
|
+
ActivityState,
|
|
30
|
+
TerminalActivityState,
|
|
31
|
+
WorkflowActivityState,
|
|
32
|
+
WorkflowActivityUpdate,
|
|
33
|
+
} from "./activity-contracts.ts";
|
|
34
|
+
export { WORKFLOW_ACTIVITY_STATES } from "./activity-contracts.ts";
|
|
35
|
+
|
|
36
|
+
import type {
|
|
37
|
+
ActivityState,
|
|
38
|
+
TerminalActivityState,
|
|
39
|
+
WorkflowActivityState,
|
|
40
|
+
WorkflowActivityUpdate,
|
|
41
|
+
} from "./activity-contracts.ts";
|
|
70
42
|
|
|
71
43
|
export interface EvidenceActivitySummary {
|
|
72
44
|
kind: "evidence";
|
|
@@ -224,7 +196,7 @@ export function sanitizeFailureReason(value: unknown, maxLength = 240): string {
|
|
|
224
196
|
function safeDesignFailure(value: unknown): {
|
|
225
197
|
operation: string;
|
|
226
198
|
code: string;
|
|
227
|
-
diagnostics:
|
|
199
|
+
diagnostics: SafeDesignDiagnostic[];
|
|
228
200
|
} | null {
|
|
229
201
|
const failure = asRecord(value);
|
|
230
202
|
if (
|
|
@@ -237,23 +209,9 @@ function safeDesignFailure(value: unknown): {
|
|
|
237
209
|
) {
|
|
238
210
|
return null;
|
|
239
211
|
}
|
|
240
|
-
const diagnostics = failure.diagnostics.flatMap((candidate) => {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
!record ||
|
|
244
|
-
typeof record.code !== "string" ||
|
|
245
|
-
!SAFE_DESIGN_DETAIL_VALUE.test(record.code)
|
|
246
|
-
) {
|
|
247
|
-
return [];
|
|
248
|
-
}
|
|
249
|
-
return [
|
|
250
|
-
Object.fromEntries(
|
|
251
|
-
Object.entries(record).filter(
|
|
252
|
-
([, entry]) =>
|
|
253
|
-
typeof entry === "string" && SAFE_DESIGN_DETAIL_VALUE.test(entry),
|
|
254
|
-
),
|
|
255
|
-
) as Record<string, string>,
|
|
256
|
-
];
|
|
212
|
+
const diagnostics = failure.diagnostics.slice(0, 64).flatMap((candidate) => {
|
|
213
|
+
const diagnostic = projectDesignDiagnostic(candidate);
|
|
214
|
+
return diagnostic ? [diagnostic] : [];
|
|
257
215
|
});
|
|
258
216
|
return {
|
|
259
217
|
operation: failure.operation,
|
|
@@ -286,7 +244,10 @@ function renderDesignFailure(
|
|
|
286
244
|
const lines = failure.diagnostics.map((diagnostic) => {
|
|
287
245
|
const details = Object.entries(diagnostic)
|
|
288
246
|
.filter(([key]) => key !== "code")
|
|
289
|
-
.map(
|
|
247
|
+
.map(
|
|
248
|
+
([key, value]) =>
|
|
249
|
+
`${key}=${Array.isArray(value) ? JSON.stringify(value) : value}`,
|
|
250
|
+
)
|
|
290
251
|
.join(" · ");
|
|
291
252
|
return ` - ${diagnostic.code}${details ? ` · ${details}` : ""}`;
|
|
292
253
|
});
|
|
@@ -315,6 +276,7 @@ function stateGlyph(state: ActivityState, spinnerFrame = 0): string {
|
|
|
315
276
|
switch (state) {
|
|
316
277
|
case "queued":
|
|
317
278
|
return "…";
|
|
279
|
+
case "preparing":
|
|
318
280
|
case "connecting":
|
|
319
281
|
case "waiting-first-response":
|
|
320
282
|
case "running":
|
|
@@ -532,6 +494,17 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
532
494
|
: undefined;
|
|
533
495
|
}
|
|
534
496
|
|
|
497
|
+
function hasAutomaticContinuation(result: Record<string, unknown>): boolean {
|
|
498
|
+
const continuation = asRecord(result.continuation);
|
|
499
|
+
return (
|
|
500
|
+
["paused", "approval-needed"].includes(String(result.state)) &&
|
|
501
|
+
asRecord(result.pause)?.code !== "operation-cancelled" &&
|
|
502
|
+
continuation?.owner === "parent" &&
|
|
503
|
+
continuation.automatic === true &&
|
|
504
|
+
(continuation.action === "amend" || continuation.command === "resume")
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
535
508
|
function workflowStateOf(
|
|
536
509
|
result: Record<string, unknown>,
|
|
537
510
|
): WorkflowActivityState {
|
|
@@ -544,6 +517,7 @@ function workflowStateOf(
|
|
|
544
517
|
) {
|
|
545
518
|
return "operation-cancelled";
|
|
546
519
|
}
|
|
520
|
+
if (hasAutomaticContinuation(result)) return "recovering";
|
|
547
521
|
const state = result.state;
|
|
548
522
|
switch (state) {
|
|
549
523
|
case "not-started":
|
|
@@ -551,6 +525,8 @@ function workflowStateOf(
|
|
|
551
525
|
case "ready":
|
|
552
526
|
case "queued":
|
|
553
527
|
return "queued";
|
|
528
|
+
case "preparing":
|
|
529
|
+
return "preparing";
|
|
554
530
|
case "connecting":
|
|
555
531
|
return "connecting";
|
|
556
532
|
case "waiting-first-response":
|
|
@@ -669,6 +645,7 @@ function workflowWait(
|
|
|
669
645
|
) {
|
|
670
646
|
return approval.gate;
|
|
671
647
|
}
|
|
648
|
+
if (state === "preparing") return "worker-setup";
|
|
672
649
|
if (state === "connecting") return "connection";
|
|
673
650
|
if (state === "waiting-first-response") return "first-response";
|
|
674
651
|
if (state === "retrying") return "bounded-policy";
|
|
@@ -755,7 +732,8 @@ export function projectWorkflowActivity(
|
|
|
755
732
|
...(workflowWait(state, payload)
|
|
756
733
|
? { wait: workflowWait(state, payload) }
|
|
757
734
|
: {}),
|
|
758
|
-
...(
|
|
735
|
+
...(!hasAutomaticContinuation(payload) &&
|
|
736
|
+
nextWorkflowAction(state, legalCommands)
|
|
759
737
|
? { nextAction: nextWorkflowAction(state, legalCommands) }
|
|
760
738
|
: {}),
|
|
761
739
|
};
|
|
@@ -854,6 +832,7 @@ function terminalState(
|
|
|
854
832
|
} {
|
|
855
833
|
return ![
|
|
856
834
|
"queued",
|
|
835
|
+
"preparing",
|
|
857
836
|
"connecting",
|
|
858
837
|
"waiting-first-response",
|
|
859
838
|
"running",
|
package/src/submit-tool.ts
CHANGED
|
@@ -17,65 +17,16 @@ import {
|
|
|
17
17
|
validateDiffResult,
|
|
18
18
|
validateEvidenceResult,
|
|
19
19
|
} from "./contracts.ts";
|
|
20
|
+
import {
|
|
21
|
+
completeEvidenceDraft,
|
|
22
|
+
evidenceDraftSchema,
|
|
23
|
+
} from "./evidence-draft.ts";
|
|
20
24
|
import {
|
|
21
25
|
type BeginCandidateInput,
|
|
22
26
|
TASK_LEDGER_LIMITS,
|
|
23
27
|
type TaskLedger,
|
|
24
28
|
} from "./task-ledger.ts";
|
|
25
29
|
|
|
26
|
-
const compactEvidenceSchema = Type.Object({
|
|
27
|
-
id: Type.String(),
|
|
28
|
-
role: Type.String(),
|
|
29
|
-
kind: Type.Literal("evidence"),
|
|
30
|
-
conclusions: Type.Array(Type.String()),
|
|
31
|
-
citations: Type.Array(
|
|
32
|
-
Type.Object({ path: Type.String(), lines: Type.String() }),
|
|
33
|
-
),
|
|
34
|
-
constraints: Type.Array(Type.String()),
|
|
35
|
-
dependencies: Type.Array(Type.String()),
|
|
36
|
-
risks: Type.Array(Type.String()),
|
|
37
|
-
blockingQuestions: Type.Array(Type.String()),
|
|
38
|
-
hints: Type.Object({
|
|
39
|
-
writeSet: Type.Array(Type.String()),
|
|
40
|
-
verification: Type.String(),
|
|
41
|
-
agentsImpact: Type.String(),
|
|
42
|
-
}),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const designEvidenceSchema = Type.Object(
|
|
46
|
-
{
|
|
47
|
-
id: Type.String(),
|
|
48
|
-
role: Type.Literal("design-explorer"),
|
|
49
|
-
kind: Type.Literal("evidence"),
|
|
50
|
-
packet_id: Type.String(),
|
|
51
|
-
module_name: Type.String(),
|
|
52
|
-
scope: Type.Array(Type.String()),
|
|
53
|
-
files_read: Type.Array(Type.String()),
|
|
54
|
-
evidence: Type.Array(
|
|
55
|
-
Type.Object(
|
|
56
|
-
{
|
|
57
|
-
claim: Type.String(),
|
|
58
|
-
path: Type.String(),
|
|
59
|
-
line_start: Type.Integer({ minimum: 1 }),
|
|
60
|
-
line_end: Type.Integer({ minimum: 1 }),
|
|
61
|
-
},
|
|
62
|
-
{ additionalProperties: false },
|
|
63
|
-
),
|
|
64
|
-
),
|
|
65
|
-
existing_structures: Type.Array(Type.String()),
|
|
66
|
-
existing_conventions: Type.Array(Type.String()),
|
|
67
|
-
constraints_discovered: Type.Array(Type.String()),
|
|
68
|
-
open_questions: Type.Array(Type.String()),
|
|
69
|
-
dependencies: Type.Array(Type.String()),
|
|
70
|
-
write_set_hints: Type.Array(Type.String()),
|
|
71
|
-
validation_hints: Type.Array(Type.String()),
|
|
72
|
-
agents_impact_hints: Type.Array(Type.String()),
|
|
73
|
-
risks: Type.Array(Type.String()),
|
|
74
|
-
success_criteria_hints: Type.Array(Type.String()),
|
|
75
|
-
},
|
|
76
|
-
{ additionalProperties: false },
|
|
77
|
-
);
|
|
78
|
-
|
|
79
30
|
const diffSchema = Type.Object({
|
|
80
31
|
id: Type.String(),
|
|
81
32
|
role: Type.String(),
|
|
@@ -225,6 +176,7 @@ export interface CandidateContextBoundary {
|
|
|
225
176
|
writePaths: readonly string[];
|
|
226
177
|
deletePaths?: readonly string[];
|
|
227
178
|
taskPaths: readonly string[];
|
|
179
|
+
contextReadRoots?: readonly string[];
|
|
228
180
|
redWritePaths: readonly string[];
|
|
229
181
|
agents: {
|
|
230
182
|
impact: "none" | "update-existing" | "create-index" | "remove-index";
|
|
@@ -354,6 +306,19 @@ function pathWithinAny(relative: string, approved: readonly string[]): boolean {
|
|
|
354
306
|
return approved.some((path) => contextRefWithinBoundary(relative, path));
|
|
355
307
|
}
|
|
356
308
|
|
|
309
|
+
export function permitsContextRead(
|
|
310
|
+
relative: string,
|
|
311
|
+
roots: readonly string[],
|
|
312
|
+
): boolean {
|
|
313
|
+
// Dynamic discovery never grants hidden credentials or a directory tree.
|
|
314
|
+
return (
|
|
315
|
+
isValidRelativePath(relative) &&
|
|
316
|
+
relative.split("/").every((part) => !part.startsWith(".")) &&
|
|
317
|
+
!/\.(?:pem|key|p12|pfx)$/iu.test(relative) &&
|
|
318
|
+
pathWithinAny(relative, roots)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
357
322
|
export function classifyCandidateContextRequest(
|
|
358
323
|
request: {
|
|
359
324
|
kind: "context-request";
|
|
@@ -377,11 +342,15 @@ export function classifyCandidateContextRequest(
|
|
|
377
342
|
const ordinaryRequested = requested.filter((ref) => !isAgentsPath(ref.path));
|
|
378
343
|
const outsideOrdinary = ordinaryRequested.filter(
|
|
379
344
|
(ref) =>
|
|
345
|
+
!(
|
|
346
|
+
ref.access === "read" &&
|
|
347
|
+
permitsContextRead(ref.path, boundary.contextReadRoots ?? [])
|
|
348
|
+
) &&
|
|
380
349
|
!pathWithinAny(
|
|
381
350
|
ref.path,
|
|
382
351
|
ref.access === "write"
|
|
383
352
|
? [...boundary.writePaths, ...(boundary.deletePaths ?? [])]
|
|
384
|
-
: phasePaths,
|
|
353
|
+
: [...phasePaths, ...boundary.taskPaths],
|
|
385
354
|
),
|
|
386
355
|
);
|
|
387
356
|
const agentsRequested = requested.filter((ref) => isAgentsPath(ref.path));
|
|
@@ -428,7 +397,7 @@ export function classifyCandidateContextRequest(
|
|
|
428
397
|
classification = { kind: "paused", code: "agents-context-needed" };
|
|
429
398
|
} else {
|
|
430
399
|
classification = {
|
|
431
|
-
kind:
|
|
400
|
+
kind: "retryable",
|
|
432
401
|
code:
|
|
433
402
|
request.code === "task-split-needed"
|
|
434
403
|
? "task-split-needed"
|
|
@@ -863,13 +832,11 @@ export function createSubmitTool(input: {
|
|
|
863
832
|
name: "abel_submit_result",
|
|
864
833
|
label: "Submit Abel Result",
|
|
865
834
|
description:
|
|
866
|
-
"Submit
|
|
835
|
+
"Submit one accepted final result. Evidence identity and omitted advisory fields are completed by this tool; keep citations, constraints, risks, and open questions explicit. After a rejection, correct once in this same session. Brief accompanying text is harmless.",
|
|
867
836
|
executionMode: "sequential",
|
|
868
837
|
parameters:
|
|
869
838
|
input.output === "evidence"
|
|
870
|
-
? input.role
|
|
871
|
-
? designEvidenceSchema
|
|
872
|
-
: compactEvidenceSchema
|
|
839
|
+
? evidenceDraftSchema(input.requestId, input.role)
|
|
873
840
|
: diffSchema,
|
|
874
841
|
async execute(_toolCallId, params) {
|
|
875
842
|
attempts++;
|
|
@@ -898,7 +865,11 @@ export function createSubmitTool(input: {
|
|
|
898
865
|
phase: true,
|
|
899
866
|
};
|
|
900
867
|
failure = undefined;
|
|
901
|
-
const value =
|
|
868
|
+
const value = (
|
|
869
|
+
input.output === "evidence"
|
|
870
|
+
? completeEvidenceDraft(params, input.requestId, input.role)
|
|
871
|
+
: params
|
|
872
|
+
) as Record<string, unknown>;
|
|
902
873
|
if (value === null || typeof value !== "object") {
|
|
903
874
|
identity.request = false;
|
|
904
875
|
identity.role = false;
|
|
@@ -945,7 +916,15 @@ export function createSubmitTool(input: {
|
|
|
945
916
|
stage: "structural-submit",
|
|
946
917
|
});
|
|
947
918
|
throw new Error(
|
|
948
|
-
|
|
919
|
+
JSON.stringify({
|
|
920
|
+
code: "code" in failure ? failure.code : failure.kind,
|
|
921
|
+
reason:
|
|
922
|
+
validation.reason ?? "submitted result identity does not match",
|
|
923
|
+
identityMismatch: Object.entries(identity)
|
|
924
|
+
.filter(([, matches]) => !matches)
|
|
925
|
+
.map(([field]) => field),
|
|
926
|
+
remainingCorrections: Math.max(0, 2 - attempts),
|
|
927
|
+
}),
|
|
949
928
|
);
|
|
950
929
|
}
|
|
951
930
|
submitted = structuredClone(value) as unknown as
|
package/src/task-ledger.ts
CHANGED
|
@@ -2,14 +2,16 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { chmodSync, lstatSync, mkdirSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
|
-
|
|
6
5
|
import type { ArtifactStore } from "./artifact-store.ts";
|
|
6
|
+
import { compareCanonicalStrings } from "./canonical.ts";
|
|
7
7
|
import {
|
|
8
8
|
diffWritePaths,
|
|
9
9
|
IMPLEMENTATION_PHASES,
|
|
10
10
|
type ImplementationPhase,
|
|
11
11
|
isValidRelativePath,
|
|
12
12
|
} from "./contracts.ts";
|
|
13
|
+
import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
|
|
14
|
+
import { TASK_SCHEMA } from "./storage-schema.ts";
|
|
13
15
|
|
|
14
16
|
export const TASK_LEDGER_LIMITS = {
|
|
15
17
|
maxSegmentBytes: 128 * 1024,
|
|
@@ -413,7 +415,9 @@ function normalizeContextRefs(value: unknown): TaskContextReference[] {
|
|
|
413
415
|
if (new Set(identities).size !== identities.length) {
|
|
414
416
|
throw new Error("ledger-context-refs-invalid");
|
|
415
417
|
}
|
|
416
|
-
return refs.sort((left, right) =>
|
|
418
|
+
return refs.sort((left, right) =>
|
|
419
|
+
compareCanonicalStrings(left.path, right.path),
|
|
420
|
+
);
|
|
417
421
|
}
|
|
418
422
|
|
|
419
423
|
function safeText(value: unknown, code: string, maximum = 256): string {
|
|
@@ -694,57 +698,8 @@ export class TaskLedger {
|
|
|
694
698
|
throw new Error("task-ledger-database-invalid");
|
|
695
699
|
}
|
|
696
700
|
this.#database = new DatabaseSync(databasePath);
|
|
697
|
-
this.#database
|
|
698
|
-
|
|
699
|
-
);
|
|
700
|
-
this.#database.exec(`
|
|
701
|
-
CREATE TABLE IF NOT EXISTS candidates (
|
|
702
|
-
candidate_id TEXT PRIMARY KEY,
|
|
703
|
-
identity_json TEXT NOT NULL,
|
|
704
|
-
state TEXT NOT NULL,
|
|
705
|
-
next_sequence INTEGER NOT NULL,
|
|
706
|
-
total_bytes INTEGER NOT NULL,
|
|
707
|
-
artifact_hash TEXT,
|
|
708
|
-
candidate_hash TEXT,
|
|
709
|
-
segment_count INTEGER,
|
|
710
|
-
paths_json TEXT,
|
|
711
|
-
pause_code TEXT
|
|
712
|
-
) STRICT;
|
|
713
|
-
CREATE TABLE IF NOT EXISTS candidate_segments (
|
|
714
|
-
candidate_id TEXT NOT NULL REFERENCES candidates(candidate_id) ON DELETE CASCADE,
|
|
715
|
-
sequence INTEGER NOT NULL,
|
|
716
|
-
artifact_hash TEXT NOT NULL,
|
|
717
|
-
size_bytes INTEGER NOT NULL,
|
|
718
|
-
segment_hash TEXT NOT NULL,
|
|
719
|
-
PRIMARY KEY (candidate_id, sequence)
|
|
720
|
-
) STRICT;
|
|
721
|
-
CREATE TABLE IF NOT EXISTS task_ledger (
|
|
722
|
-
run_id TEXT NOT NULL,
|
|
723
|
-
task_id TEXT NOT NULL,
|
|
724
|
-
delivery_revision INTEGER NOT NULL,
|
|
725
|
-
boundary_hash TEXT NOT NULL,
|
|
726
|
-
objective TEXT NOT NULL,
|
|
727
|
-
context_refs_json TEXT NOT NULL,
|
|
728
|
-
initial_phase TEXT NOT NULL,
|
|
729
|
-
PRIMARY KEY (run_id, task_id)
|
|
730
|
-
) STRICT;
|
|
731
|
-
CREATE TABLE IF NOT EXISTS task_events (
|
|
732
|
-
run_id TEXT NOT NULL,
|
|
733
|
-
task_id TEXT NOT NULL,
|
|
734
|
-
ordinal INTEGER NOT NULL,
|
|
735
|
-
event_id TEXT NOT NULL,
|
|
736
|
-
event_json TEXT NOT NULL,
|
|
737
|
-
prior_hash TEXT NOT NULL,
|
|
738
|
-
event_hash TEXT NOT NULL,
|
|
739
|
-
PRIMARY KEY (run_id, task_id, ordinal),
|
|
740
|
-
UNIQUE (run_id, task_id, event_id),
|
|
741
|
-
FOREIGN KEY (run_id, task_id) REFERENCES task_ledger(run_id, task_id) ON DELETE CASCADE
|
|
742
|
-
) STRICT;
|
|
743
|
-
CREATE TABLE IF NOT EXISTS durable_facts (
|
|
744
|
-
fact_key TEXT PRIMARY KEY,
|
|
745
|
-
fact_json TEXT NOT NULL
|
|
746
|
-
) STRICT;
|
|
747
|
-
`);
|
|
701
|
+
configureSqlite(this.#database);
|
|
702
|
+
ensureSqliteSchema(this.#database, TASK_SCHEMA);
|
|
748
703
|
chmodSync(databasePath, 0o600);
|
|
749
704
|
}
|
|
750
705
|
|
|
@@ -1173,7 +1128,7 @@ export class TaskLedger {
|
|
|
1173
1128
|
commitVerifiedEvent(input: VerifiedTaskEvent) {
|
|
1174
1129
|
this.#assertOpen();
|
|
1175
1130
|
const normalized = normalizeVerifiedEvent(input);
|
|
1176
|
-
this.#task(normalized.runId, normalized.taskId);
|
|
1131
|
+
const task = this.#task(normalized.runId, normalized.taskId);
|
|
1177
1132
|
const rows = this.#events(normalized.runId, normalized.taskId);
|
|
1178
1133
|
const existing = rows.find((row) => row.event_id === normalized.eventId);
|
|
1179
1134
|
const json = JSON.stringify(normalized);
|
|
@@ -1186,6 +1141,8 @@ export class TaskLedger {
|
|
|
1186
1141
|
throw new Error("ledger-event-capacity-exceeded");
|
|
1187
1142
|
}
|
|
1188
1143
|
if (normalized.kind === "phase-verified") {
|
|
1144
|
+
if (task.initial_phase === "green" && normalized.phase === "red")
|
|
1145
|
+
throw new Error("ledger-phase-order-invalid");
|
|
1189
1146
|
const completed = rows
|
|
1190
1147
|
.map((row) => JSON.parse(row.event_json) as VerifiedTaskEvent)
|
|
1191
1148
|
.filter(
|
|
@@ -1195,7 +1152,9 @@ export class TaskLedger {
|
|
|
1195
1152
|
.map((event) => event.phase);
|
|
1196
1153
|
const required =
|
|
1197
1154
|
normalized.phase === "green"
|
|
1198
|
-
? "
|
|
1155
|
+
? task.initial_phase === "green"
|
|
1156
|
+
? undefined
|
|
1157
|
+
: "red"
|
|
1199
1158
|
: normalized.phase === "refactor"
|
|
1200
1159
|
? "green"
|
|
1201
1160
|
: undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Request budgets describe observable model progress, never TCP/TLS events. */
|
|
2
|
+
export const REQUEST_BOUNDS = Object.freeze({
|
|
3
|
+
firstProgressMs: 90_000,
|
|
4
|
+
streamIdleMs: 180_000,
|
|
5
|
+
cancellationDrainMs: 250,
|
|
6
|
+
});
|
|
7
|
+
export type TransportTimeoutCode =
|
|
8
|
+
| "first-progress-timeout"
|
|
9
|
+
| "stream-idle-timeout"
|
|
10
|
+
| "attempt-timeout";
|
|
11
|
+
export class TransportTimeout extends Error {
|
|
12
|
+
readonly scope: "request" | "attempt";
|
|
13
|
+
constructor(readonly code: TransportTimeoutCode) {
|
|
14
|
+
super(code);
|
|
15
|
+
this.name = "TransportTimeout";
|
|
16
|
+
this.scope = code === "attempt-timeout" ? "attempt" : "request";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function isTransportTimeoutCode(
|
|
20
|
+
code: string,
|
|
21
|
+
): code is TransportTimeoutCode {
|
|
22
|
+
return [
|
|
23
|
+
"first-progress-timeout",
|
|
24
|
+
"stream-idle-timeout",
|
|
25
|
+
"attempt-timeout",
|
|
26
|
+
].includes(code);
|
|
27
|
+
}
|
|
28
|
+
export function transportFailureError(code: string): Error {
|
|
29
|
+
return isTransportTimeoutCode(code)
|
|
30
|
+
? new TransportTimeout(code)
|
|
31
|
+
: new Error("transport-failure");
|
|
32
|
+
}
|