@agwab/pi-workflow 0.2.1 → 0.3.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.
Files changed (70) hide show
  1. package/dist/compiler.js +6 -8
  2. package/dist/dynamic-decision.d.ts +0 -1
  3. package/dist/dynamic-decision.js +0 -7
  4. package/dist/dynamic-profiles.d.ts +0 -1
  5. package/dist/dynamic-profiles.js +0 -3
  6. package/dist/engine-run-graph.d.ts +1 -0
  7. package/dist/engine-run-graph.js +142 -2
  8. package/dist/engine.d.ts +5 -0
  9. package/dist/engine.js +112 -27
  10. package/dist/extension.d.ts +2 -1
  11. package/dist/extension.js +27 -6
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +2 -1
  14. package/dist/store.js +55 -11
  15. package/dist/subagent-backend.js +155 -29
  16. package/dist/types.d.ts +6 -0
  17. package/dist/workflow-runtime.js +10 -1
  18. package/dist/workflow-view.js +3 -1
  19. package/dist/workflow-web-source-extension.js +167 -48
  20. package/dist/workflow-web-source.d.ts +2 -1
  21. package/dist/workflow-web-source.js +84 -19
  22. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  23. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  24. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  25. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  26. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  27. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  28. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  29. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  30. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  31. package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
  32. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  33. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  34. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  35. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  36. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  37. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  38. package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
  39. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  40. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  41. package/package.json +2 -2
  42. package/src/compiler.ts +14 -9
  43. package/src/dynamic-decision.ts +0 -11
  44. package/src/dynamic-profiles.ts +0 -4
  45. package/src/engine-run-graph.ts +185 -2
  46. package/src/engine.ts +145 -24
  47. package/src/extension.ts +33 -4
  48. package/src/index.ts +3 -1
  49. package/src/store.ts +74 -11
  50. package/src/subagent-backend.ts +201 -28
  51. package/src/types.ts +6 -0
  52. package/src/workflow-runtime.ts +18 -2
  53. package/src/workflow-view.ts +2 -1
  54. package/src/workflow-web-source-extension.ts +621 -228
  55. package/src/workflow-web-source.ts +118 -28
  56. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
  57. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  58. package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
  59. package/workflows/deep-research/helpers/render-executive.mjs +8 -21
  60. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  61. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
  62. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
  63. package/workflows/impact-review/spec.json +3 -3
  64. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  65. package/dist/dynamic-loader.d.ts +0 -25
  66. package/dist/dynamic-loader.js +0 -13
  67. package/src/dynamic-loader.ts +0 -49
  68. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  69. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  70. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -1,111 +1,187 @@
1
- import { appendRunEvent, readRunRecord, recordInterruptRequest, type RunAttemptRecord, type RunRecord } from "../artifacts/index.ts";
1
+ import {
2
+ appendRunEvent,
3
+ readRunRecord,
4
+ recordInterruptRequest,
5
+ type RunAttemptRecord,
6
+ type RunRecord,
7
+ } from "../artifacts/index.ts";
8
+ import { resolveRunRef } from "./run-ref.ts";
2
9
  import { isTerminalStatus } from "./status.ts";
3
10
 
4
11
  export interface InterruptRunOptions {
5
- cwd?: string;
6
- runId: string;
7
- runsDir?: string;
8
- attemptId?: string;
9
- /** @deprecated v1 compatibility alias. */
10
- taskId?: string;
11
- reason?: string;
12
- signal?: NodeJS.Signals;
13
- escalateAfterMs?: number;
14
- killAfterMs?: number;
12
+ cwd?: string;
13
+ runId: string;
14
+ runsDir?: string;
15
+ attemptId?: string;
16
+ /** @deprecated v1 compatibility alias. */
17
+ taskId?: string;
18
+ reason?: string;
19
+ signal?: NodeJS.Signals;
20
+ escalateAfterMs?: number;
21
+ killAfterMs?: number;
15
22
  }
16
23
 
17
24
  export interface InterruptRunResult {
18
- status: "interrupt-requested" | "not-found" | "already-terminal" | "unsupported";
19
- runId: string;
20
- signal: NodeJS.Signals;
21
- interruptedAttempts: string[];
22
- unsupportedAttempts: string[];
23
- /** @deprecated v1 compatibility alias. */
24
- interruptedTasks: string[];
25
- /** @deprecated v1 compatibility alias. */
26
- unsupportedTasks: string[];
27
- record: RunRecord | null;
25
+ status:
26
+ | "interrupt-requested"
27
+ | "not-found"
28
+ | "already-terminal"
29
+ | "unsupported";
30
+ runId: string;
31
+ signal: NodeJS.Signals;
32
+ interruptedAttempts: string[];
33
+ unsupportedAttempts: string[];
34
+ /** @deprecated v1 compatibility alias. */
35
+ interruptedTasks: string[];
36
+ /** @deprecated v1 compatibility alias. */
37
+ unsupportedTasks: string[];
38
+ record: RunRecord | null;
28
39
  }
29
40
 
30
- function sendProcessSignal(attempt: RunAttemptRecord, signal: NodeJS.Signals): boolean {
31
- const pid = attempt.process?.pid;
32
- if (pid === undefined) return false;
33
- try {
34
- const target = process.platform === "win32" ? pid : -(attempt.process?.processGroupId ?? pid);
35
- process.kill(target, signal);
36
- return true;
37
- } catch {
38
- try {
39
- process.kill(pid, signal);
40
- return true;
41
- } catch {
42
- return false;
43
- }
44
- }
41
+ function sendProcessSignal(
42
+ attempt: RunAttemptRecord,
43
+ signal: NodeJS.Signals,
44
+ ): boolean {
45
+ const pid = attempt.process?.pid;
46
+ if (pid === undefined) return false;
47
+ try {
48
+ const target =
49
+ process.platform === "win32"
50
+ ? pid
51
+ : -(attempt.process?.processGroupId ?? pid);
52
+ process.kill(target, signal);
53
+ return true;
54
+ } catch {
55
+ try {
56
+ process.kill(pid, signal);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
45
62
  }
46
63
 
47
- function runningAttempts(record: RunRecord, targetAttemptId?: string): RunAttemptRecord[] {
48
- return record.attempts.filter((attempt) => {
49
- if (targetAttemptId !== undefined && attempt.attemptId !== targetAttemptId) return false;
50
- return attempt.status === "running" || attempt.status === "pending";
51
- });
64
+ function runningAttempts(
65
+ record: RunRecord,
66
+ targetAttemptId?: string,
67
+ ): RunAttemptRecord[] {
68
+ return record.attempts.filter((attempt) => {
69
+ if (targetAttemptId !== undefined && attempt.attemptId !== targetAttemptId)
70
+ return false;
71
+ return attempt.status === "running" || attempt.status === "pending";
72
+ });
52
73
  }
53
74
 
54
- async function escalate(options: InterruptRunOptions, signal: NodeJS.Signals): Promise<void> {
55
- const record = await readRunRecord(options).catch(() => null);
56
- if (record === null || isTerminalStatus(record.status)) return;
57
- for (const attempt of runningAttempts(record, options.attemptId ?? options.taskId)) sendProcessSignal(attempt, signal);
58
- await appendRunEvent(options, { type: "run.interrupt_requested", status: record.status, message: `interrupt escalation ${signal}`, data: { signal } }).catch(() => undefined);
75
+ async function escalate(
76
+ options: InterruptRunOptions,
77
+ signal: NodeJS.Signals,
78
+ ): Promise<void> {
79
+ const ref = await resolveRunRef(options);
80
+ const record = await readRunRecord(ref).catch(() => null);
81
+ if (record === null || isTerminalStatus(record.status)) return;
82
+ for (const attempt of runningAttempts(
83
+ record,
84
+ options.attemptId ?? options.taskId,
85
+ ))
86
+ sendProcessSignal(attempt, signal);
87
+ await appendRunEvent(ref, {
88
+ type: "run.interrupt_requested",
89
+ status: record.status,
90
+ message: `interrupt escalation ${signal}`,
91
+ data: { signal },
92
+ }).catch(() => undefined);
59
93
  }
60
94
 
61
- function result(status: InterruptRunResult["status"], runId: string, signal: NodeJS.Signals, interruptedAttempts: string[], unsupportedAttempts: string[], record: RunRecord | null): InterruptRunResult {
62
- return {
63
- status,
64
- runId,
65
- signal,
66
- interruptedAttempts,
67
- unsupportedAttempts,
68
- interruptedTasks: interruptedAttempts,
69
- unsupportedTasks: unsupportedAttempts,
70
- record,
71
- };
95
+ function result(
96
+ status: InterruptRunResult["status"],
97
+ runId: string,
98
+ signal: NodeJS.Signals,
99
+ interruptedAttempts: string[],
100
+ unsupportedAttempts: string[],
101
+ record: RunRecord | null,
102
+ ): InterruptRunResult {
103
+ return {
104
+ status,
105
+ runId,
106
+ signal,
107
+ interruptedAttempts,
108
+ unsupportedAttempts,
109
+ interruptedTasks: interruptedAttempts,
110
+ unsupportedTasks: unsupportedAttempts,
111
+ record,
112
+ };
72
113
  }
73
114
 
74
- export async function interruptRun(options: InterruptRunOptions): Promise<InterruptRunResult> {
75
- const signal = options.signal ?? "SIGINT";
76
- const record = await readRunRecord(options);
77
- if (record === null) {
78
- return result("not-found", options.runId, signal, [], [], null);
79
- }
80
- if (isTerminalStatus(record.status)) {
81
- return result("already-terminal", options.runId, signal, [], [], record);
82
- }
115
+ export async function interruptRun(
116
+ options: InterruptRunOptions,
117
+ ): Promise<InterruptRunResult> {
118
+ const signal = options.signal ?? "SIGINT";
119
+ const ref = await resolveRunRef(options);
120
+ const record = await readRunRecord(ref);
121
+ if (record === null) {
122
+ return result("not-found", options.runId, signal, [], [], null);
123
+ }
124
+ if (isTerminalStatus(record.status)) {
125
+ return result("already-terminal", options.runId, signal, [], [], record);
126
+ }
83
127
 
84
- const candidates = runningAttempts(record, options.attemptId ?? options.taskId);
85
- const interruptedAttempts: string[] = [];
86
- const unsupportedAttempts: string[] = [];
87
- for (const attempt of candidates) {
88
- if (sendProcessSignal(attempt, signal)) interruptedAttempts.push(attempt.attemptId);
89
- else unsupportedAttempts.push(attempt.attemptId);
90
- }
128
+ const candidates = runningAttempts(
129
+ record,
130
+ options.attemptId ?? options.taskId,
131
+ );
132
+ const interruptedAttempts: string[] = [];
133
+ const unsupportedAttempts: string[] = [];
134
+ for (const attempt of candidates) {
135
+ if (sendProcessSignal(attempt, signal))
136
+ interruptedAttempts.push(attempt.attemptId);
137
+ else unsupportedAttempts.push(attempt.attemptId);
138
+ }
91
139
 
92
- if (interruptedAttempts.length === 0) {
93
- await appendRunEvent(options, { type: "run.interrupt_requested", status: record.status, message: "interrupt unsupported: no interruptable process metadata", data: { signal, unsupportedAttempts } });
94
- return result("unsupported", options.runId, signal, interruptedAttempts, unsupportedAttempts, record);
95
- }
140
+ if (interruptedAttempts.length === 0) {
141
+ await appendRunEvent(ref, {
142
+ type: "run.interrupt_requested",
143
+ status: record.status,
144
+ message: "interrupt unsupported: no interruptable process metadata",
145
+ data: { signal, unsupportedAttempts },
146
+ });
147
+ return result(
148
+ "unsupported",
149
+ options.runId,
150
+ signal,
151
+ interruptedAttempts,
152
+ unsupportedAttempts,
153
+ record,
154
+ );
155
+ }
96
156
 
97
- const updated = await recordInterruptRequest(options, signal, options.reason ?? null);
98
- await appendRunEvent(options, {
99
- type: "run.interrupt_requested",
100
- status: updated.status,
101
- message: `interrupt requested with ${signal}`,
102
- data: { signal, interruptedAttempts, unsupportedAttempts, reason: options.reason ?? null },
103
- });
157
+ const updated = await recordInterruptRequest(
158
+ ref,
159
+ signal,
160
+ options.reason ?? null,
161
+ );
162
+ await appendRunEvent(ref, {
163
+ type: "run.interrupt_requested",
164
+ status: updated.status,
165
+ message: `interrupt requested with ${signal}`,
166
+ data: {
167
+ signal,
168
+ interruptedAttempts,
169
+ unsupportedAttempts,
170
+ reason: options.reason ?? null,
171
+ },
172
+ });
104
173
 
105
- const termDelay = options.escalateAfterMs ?? 1_000;
106
- const killDelay = options.killAfterMs ?? 3_000;
107
- setTimeout(() => void escalate(options, "SIGTERM"), termDelay).unref?.();
108
- setTimeout(() => void escalate(options, "SIGKILL"), killDelay).unref?.();
174
+ const termDelay = options.escalateAfterMs ?? 1_000;
175
+ const killDelay = options.killAfterMs ?? 3_000;
176
+ setTimeout(() => void escalate(ref, "SIGTERM"), termDelay).unref?.();
177
+ setTimeout(() => void escalate(ref, "SIGKILL"), killDelay).unref?.();
109
178
 
110
- return result("interrupt-requested", options.runId, signal, interruptedAttempts, unsupportedAttempts, updated);
179
+ return result(
180
+ "interrupt-requested",
181
+ options.runId,
182
+ signal,
183
+ interruptedAttempts,
184
+ unsupportedAttempts,
185
+ updated,
186
+ );
111
187
  }
@@ -1,98 +1,144 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { isAbsolute, resolve, sep } from "node:path";
3
3
  import {
4
- appendRunEvent,
5
- commitAttemptResultIfActive,
6
- readRunRecord,
7
- upsertRunAttempt,
8
- type ResultEnvelope,
9
- type RunAttemptRecord,
10
- type RunRef,
11
- type RunRecord,
4
+ appendRunEvent,
5
+ commitAttemptResultIfActive,
6
+ readRunRecord,
7
+ upsertRunAttempt,
8
+ type ResultEnvelope,
9
+ type RunAttemptRecord,
10
+ type RunRef,
11
+ type RunRecord,
12
12
  } from "../artifacts/index.ts";
13
+ import { resolveRunRef } from "./run-ref.ts";
13
14
  import { isTerminalStatus } from "./status.ts";
14
15
 
15
16
  export interface ReconcileSubagentRunOptions extends RunRef {
16
- staleAfterMs?: number;
17
+ staleAfterMs?: number;
17
18
  }
18
19
 
19
20
  export interface ReconcileSubagentRunResult {
20
- status: "not-found" | "already-terminal" | "running" | "committed-result" | "marked-stale";
21
- runId: string;
22
- record: RunRecord | null;
21
+ status:
22
+ | "not-found"
23
+ | "already-terminal"
24
+ | "running"
25
+ | "committed-result"
26
+ | "marked-stale";
27
+ runId: string;
28
+ record: RunRecord | null;
23
29
  }
24
30
 
25
31
  function safeArtifactPath(attempt: RunAttemptRecord): string | null {
26
- if (attempt.resultPath === undefined || attempt.artifactCwd === undefined) return null;
27
- if (isAbsolute(attempt.resultPath) || attempt.resultPath.split("/").includes("..")) return null;
28
- return resolve(attempt.artifactCwd, attempt.resultPath.split("/").join(sep));
32
+ if (attempt.resultPath === undefined || attempt.artifactCwd === undefined)
33
+ return null;
34
+ if (
35
+ isAbsolute(attempt.resultPath) ||
36
+ attempt.resultPath.split("/").includes("..")
37
+ )
38
+ return null;
39
+ return resolve(attempt.artifactCwd, attempt.resultPath.split("/").join(sep));
29
40
  }
30
41
 
31
- async function readAttemptResult(attempt: RunAttemptRecord): Promise<ResultEnvelope | null> {
32
- const path = safeArtifactPath(attempt);
33
- if (path === null) return null;
34
- try {
35
- return JSON.parse(await readFile(path, "utf8")) as ResultEnvelope;
36
- } catch {
37
- return null;
38
- }
42
+ async function readAttemptResult(
43
+ attempt: RunAttemptRecord,
44
+ ): Promise<ResultEnvelope | null> {
45
+ const path = safeArtifactPath(attempt);
46
+ if (path === null) return null;
47
+ try {
48
+ return JSON.parse(await readFile(path, "utf8")) as ResultEnvelope;
49
+ } catch {
50
+ return null;
51
+ }
39
52
  }
40
53
 
41
54
  function pidAlive(pid: number | undefined): boolean {
42
- if (pid === undefined) return false;
43
- try {
44
- process.kill(pid, 0);
45
- return true;
46
- } catch {
47
- return false;
48
- }
55
+ if (pid === undefined) return false;
56
+ try {
57
+ process.kill(pid, 0);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
49
62
  }
50
63
 
51
64
  function processAlive(attempt: RunAttemptRecord): boolean {
52
- return pidAlive(attempt.process?.pid) || pidAlive(attempt.process?.workerPid);
65
+ return pidAlive(attempt.process?.pid) || pidAlive(attempt.process?.workerPid);
53
66
  }
54
67
 
55
- function heartbeatFresh(attempt: RunAttemptRecord, staleAfterMs: number): boolean {
56
- if (attempt.heartbeatAt === undefined) return false;
57
- const time = Date.parse(attempt.heartbeatAt);
58
- return Number.isFinite(time) && Date.now() - time <= staleAfterMs;
68
+ function heartbeatFresh(
69
+ attempt: RunAttemptRecord,
70
+ staleAfterMs: number,
71
+ ): boolean {
72
+ if (attempt.heartbeatAt === undefined) return false;
73
+ const time = Date.parse(attempt.heartbeatAt);
74
+ return Number.isFinite(time) && Date.now() - time <= staleAfterMs;
59
75
  }
60
76
 
61
77
  function activeAttempt(record: RunRecord): RunAttemptRecord | undefined {
62
- const activeId = record.activeAttemptId ?? record.latestAttemptId ?? undefined;
63
- return activeId === undefined ? record.attempts.at(-1) : record.attempts.find((attempt) => attempt.attemptId === activeId) ?? record.attempts.at(-1);
78
+ const activeId =
79
+ record.activeAttemptId ?? record.latestAttemptId ?? undefined;
80
+ return activeId === undefined
81
+ ? record.attempts.at(-1)
82
+ : (record.attempts.find((attempt) => attempt.attemptId === activeId) ??
83
+ record.attempts.at(-1));
64
84
  }
65
85
 
66
- export async function reconcileSubagentRun(options: ReconcileSubagentRunOptions): Promise<ReconcileSubagentRunResult> {
67
- const staleAfterMs = options.staleAfterMs ?? 30_000;
68
- const record = await readRunRecord(options);
69
- if (record === null) return { status: "not-found", runId: options.runId, record: null };
70
- if (isTerminalStatus(record.status)) return { status: "already-terminal", runId: options.runId, record };
86
+ export async function reconcileSubagentRun(
87
+ options: ReconcileSubagentRunOptions,
88
+ ): Promise<ReconcileSubagentRunResult> {
89
+ const staleAfterMs = options.staleAfterMs ?? 30_000;
90
+ const ref = await resolveRunRef(options);
91
+ const record = await readRunRecord(ref);
92
+ if (record === null)
93
+ return { status: "not-found", runId: options.runId, record: null };
94
+ if (isTerminalStatus(record.status))
95
+ return { status: "already-terminal", runId: options.runId, record };
71
96
 
72
- const attempt = activeAttempt(record);
73
- if (attempt === undefined) return { status: "running", runId: options.runId, record };
97
+ const attempt = activeAttempt(record);
98
+ if (attempt === undefined)
99
+ return { status: "running", runId: options.runId, record };
74
100
 
75
- const result = await readAttemptResult(attempt);
76
- if (result !== null && result.attemptId === attempt.attemptId && isTerminalStatus(result.status)) {
77
- const committed = await commitAttemptResultIfActive(options, result);
78
- await appendRunEvent(options, { type: "reconcile.completed", attemptId: attempt.attemptId, status: result.status, message: committed.committed ? "committed terminal attempt result" : "terminal attempt result was stale" }).catch(() => undefined);
79
- return { status: committed.committed ? "committed-result" : "running", runId: options.runId, record: committed.record };
80
- }
101
+ const result = await readAttemptResult(attempt);
102
+ if (
103
+ result !== null &&
104
+ result.attemptId === attempt.attemptId &&
105
+ isTerminalStatus(result.status)
106
+ ) {
107
+ const committed = await commitAttemptResultIfActive(ref, result);
108
+ await appendRunEvent(ref, {
109
+ type: "reconcile.completed",
110
+ attemptId: attempt.attemptId,
111
+ status: result.status,
112
+ message: committed.committed
113
+ ? "committed terminal attempt result"
114
+ : "terminal attempt result was stale",
115
+ }).catch(() => undefined);
116
+ return {
117
+ status: committed.committed ? "committed-result" : "running",
118
+ runId: options.runId,
119
+ record: committed.record,
120
+ };
121
+ }
81
122
 
82
- if (processAlive(attempt) || heartbeatFresh(attempt, staleAfterMs)) {
83
- return { status: "running", runId: options.runId, record };
84
- }
123
+ if (processAlive(attempt) || heartbeatFresh(attempt, staleAfterMs)) {
124
+ return { status: "running", runId: options.runId, record };
125
+ }
85
126
 
86
- const updated = await upsertRunAttempt({
87
- ...options,
88
- attemptId: attempt.attemptId,
89
- status: "failed",
90
- backend: attempt.backend,
91
- failureKind: "stale",
92
- startedAt: attempt.startedAt,
93
- completedAt: new Date(),
94
- activate: true,
95
- });
96
- await appendRunEvent(options, { type: "reconcile.failed", attemptId: attempt.attemptId, status: "failed", message: "active attempt is stale/orphaned" }).catch(() => undefined);
97
- return { status: "marked-stale", runId: options.runId, record: updated };
127
+ const updated = await upsertRunAttempt({
128
+ ...ref,
129
+ attemptId: attempt.attemptId,
130
+ status: "failed",
131
+ backend: attempt.backend,
132
+ failureKind: "stale",
133
+ startedAt: attempt.startedAt,
134
+ completedAt: new Date(),
135
+ activate: true,
136
+ });
137
+ await appendRunEvent(ref, {
138
+ type: "reconcile.failed",
139
+ attemptId: attempt.attemptId,
140
+ status: "failed",
141
+ message: "active attempt is stale/orphaned",
142
+ }).catch(() => undefined);
143
+ return { status: "marked-stale", runId: options.runId, record: updated };
98
144
  }