clinkx 0.2.0 → 0.2.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.
Files changed (149) hide show
  1. package/clinkx-workflows/dist/artifacts.d.ts +65 -0
  2. package/clinkx-workflows/dist/artifacts.js +268 -0
  3. package/clinkx-workflows/dist/artifacts.js.map +1 -0
  4. package/clinkx-workflows/dist/backend.d.ts +33 -0
  5. package/clinkx-workflows/dist/backend.js +9 -0
  6. package/clinkx-workflows/dist/backend.js.map +1 -0
  7. package/clinkx-workflows/dist/child-env.d.ts +23 -0
  8. package/clinkx-workflows/dist/child-env.js +53 -0
  9. package/clinkx-workflows/dist/child-env.js.map +1 -0
  10. package/clinkx-workflows/dist/clink-client.d.ts +51 -0
  11. package/clinkx-workflows/dist/clink-client.js +216 -0
  12. package/clinkx-workflows/dist/clink-client.js.map +1 -0
  13. package/clinkx-workflows/dist/config.d.ts +126 -0
  14. package/clinkx-workflows/dist/config.js +226 -0
  15. package/clinkx-workflows/dist/config.js.map +1 -0
  16. package/clinkx-workflows/dist/definition-normalizer.d.ts +59 -0
  17. package/clinkx-workflows/dist/definition-normalizer.js +75 -0
  18. package/clinkx-workflows/dist/definition-normalizer.js.map +1 -0
  19. package/clinkx-workflows/dist/engine.d.ts +235 -0
  20. package/clinkx-workflows/dist/engine.js +1044 -0
  21. package/clinkx-workflows/dist/engine.js.map +1 -0
  22. package/clinkx-workflows/dist/errors.d.ts +74 -0
  23. package/clinkx-workflows/dist/errors.js +84 -0
  24. package/clinkx-workflows/dist/errors.js.map +1 -0
  25. package/clinkx-workflows/dist/fidelity.d.ts +112 -0
  26. package/clinkx-workflows/dist/fidelity.js +140 -0
  27. package/clinkx-workflows/dist/fidelity.js.map +1 -0
  28. package/clinkx-workflows/dist/fingerprint.d.ts +69 -0
  29. package/clinkx-workflows/dist/fingerprint.js +143 -0
  30. package/clinkx-workflows/dist/fingerprint.js.map +1 -0
  31. package/clinkx-workflows/dist/index.d.ts +16 -0
  32. package/clinkx-workflows/dist/index.js +42 -0
  33. package/clinkx-workflows/dist/index.js.map +1 -0
  34. package/clinkx-workflows/dist/loader.d.ts +64 -0
  35. package/clinkx-workflows/dist/loader.js +371 -0
  36. package/clinkx-workflows/dist/loader.js.map +1 -0
  37. package/clinkx-workflows/dist/logger.d.ts +16 -0
  38. package/clinkx-workflows/dist/logger.js +31 -0
  39. package/clinkx-workflows/dist/logger.js.map +1 -0
  40. package/clinkx-workflows/dist/path-validation.d.ts +23 -0
  41. package/clinkx-workflows/dist/path-validation.js +73 -0
  42. package/clinkx-workflows/dist/path-validation.js.map +1 -0
  43. package/clinkx-workflows/dist/prompt-budget.d.ts +31 -0
  44. package/clinkx-workflows/dist/prompt-budget.js +78 -0
  45. package/clinkx-workflows/dist/prompt-budget.js.map +1 -0
  46. package/clinkx-workflows/dist/queue.d.ts +16 -0
  47. package/clinkx-workflows/dist/queue.js +46 -0
  48. package/clinkx-workflows/dist/queue.js.map +1 -0
  49. package/clinkx-workflows/dist/ranking-reducer.d.ts +11 -0
  50. package/clinkx-workflows/dist/ranking-reducer.js +245 -0
  51. package/clinkx-workflows/dist/ranking-reducer.js.map +1 -0
  52. package/clinkx-workflows/dist/reducers/index.d.ts +8 -0
  53. package/clinkx-workflows/dist/reducers/index.js +12 -0
  54. package/clinkx-workflows/dist/reducers/index.js.map +1 -0
  55. package/clinkx-workflows/dist/run-id.d.ts +17 -0
  56. package/clinkx-workflows/dist/run-id.js +26 -0
  57. package/clinkx-workflows/dist/run-id.js.map +1 -0
  58. package/clinkx-workflows/dist/run-summary/cards/council-answer.d.ts +8 -0
  59. package/clinkx-workflows/dist/run-summary/cards/council-answer.js +75 -0
  60. package/clinkx-workflows/dist/run-summary/cards/council-answer.js.map +1 -0
  61. package/clinkx-workflows/dist/run-summary/cards/council-code-review.d.ts +13 -0
  62. package/clinkx-workflows/dist/run-summary/cards/council-code-review.js +90 -0
  63. package/clinkx-workflows/dist/run-summary/cards/council-code-review.js.map +1 -0
  64. package/clinkx-workflows/dist/run-summary/cards/council-debug.d.ts +9 -0
  65. package/clinkx-workflows/dist/run-summary/cards/council-debug.js +79 -0
  66. package/clinkx-workflows/dist/run-summary/cards/council-debug.js.map +1 -0
  67. package/clinkx-workflows/dist/run-summary/cards/council-default.d.ts +11 -0
  68. package/clinkx-workflows/dist/run-summary/cards/council-default.js +57 -0
  69. package/clinkx-workflows/dist/run-summary/cards/council-default.js.map +1 -0
  70. package/clinkx-workflows/dist/run-summary/cards/council-discover.d.ts +10 -0
  71. package/clinkx-workflows/dist/run-summary/cards/council-discover.js +79 -0
  72. package/clinkx-workflows/dist/run-summary/cards/council-discover.js.map +1 -0
  73. package/clinkx-workflows/dist/run-summary/cards/generic.d.ts +2 -0
  74. package/clinkx-workflows/dist/run-summary/cards/generic.js +4 -0
  75. package/clinkx-workflows/dist/run-summary/cards/generic.js.map +1 -0
  76. package/clinkx-workflows/dist/run-summary/cards/index.d.ts +6 -0
  77. package/clinkx-workflows/dist/run-summary/cards/index.js +17 -0
  78. package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -0
  79. package/clinkx-workflows/dist/run-summary/utils.d.ts +6 -0
  80. package/clinkx-workflows/dist/run-summary/utils.js +30 -0
  81. package/clinkx-workflows/dist/run-summary/utils.js.map +1 -0
  82. package/clinkx-workflows/dist/run-summary-derived.d.ts +19 -0
  83. package/clinkx-workflows/dist/run-summary-derived.js +100 -0
  84. package/clinkx-workflows/dist/run-summary-derived.js.map +1 -0
  85. package/clinkx-workflows/dist/run-summary.d.ts +70 -0
  86. package/clinkx-workflows/dist/run-summary.js +125 -0
  87. package/clinkx-workflows/dist/run-summary.js.map +1 -0
  88. package/clinkx-workflows/dist/schema.d.ts +609 -0
  89. package/clinkx-workflows/dist/schema.js +123 -0
  90. package/clinkx-workflows/dist/schema.js.map +1 -0
  91. package/clinkx-workflows/dist/server.d.ts +16 -0
  92. package/clinkx-workflows/dist/server.js +33 -0
  93. package/clinkx-workflows/dist/server.js.map +1 -0
  94. package/clinkx-workflows/dist/shutdown.d.ts +54 -0
  95. package/clinkx-workflows/dist/shutdown.js +120 -0
  96. package/clinkx-workflows/dist/shutdown.js.map +1 -0
  97. package/clinkx-workflows/dist/state-schema.d.ts +141 -0
  98. package/clinkx-workflows/dist/state-schema.js +21 -0
  99. package/clinkx-workflows/dist/state-schema.js.map +1 -0
  100. package/clinkx-workflows/dist/state.d.ts +37 -0
  101. package/clinkx-workflows/dist/state.js +838 -0
  102. package/clinkx-workflows/dist/state.js.map +1 -0
  103. package/clinkx-workflows/dist/template-loader.d.ts +30 -0
  104. package/clinkx-workflows/dist/template-loader.js +77 -0
  105. package/clinkx-workflows/dist/template-loader.js.map +1 -0
  106. package/clinkx-workflows/dist/template.d.ts +54 -0
  107. package/clinkx-workflows/dist/template.js +128 -0
  108. package/clinkx-workflows/dist/template.js.map +1 -0
  109. package/clinkx-workflows/dist/transport.d.ts +91 -0
  110. package/clinkx-workflows/dist/transport.js +249 -0
  111. package/clinkx-workflows/dist/transport.js.map +1 -0
  112. package/clinkx-workflows/dist/types.d.ts +137 -0
  113. package/clinkx-workflows/dist/types.js +11 -0
  114. package/clinkx-workflows/dist/types.js.map +1 -0
  115. package/clinkx-workflows/dist/validators/council.d.ts +1488 -0
  116. package/clinkx-workflows/dist/validators/council.js +509 -0
  117. package/clinkx-workflows/dist/validators/council.js.map +1 -0
  118. package/clinkx-workflows/dist/validators/index.d.ts +40 -0
  119. package/clinkx-workflows/dist/validators/index.js +43 -0
  120. package/clinkx-workflows/dist/validators/index.js.map +1 -0
  121. package/clinkx-workflows/dist/workflow-receipt.d.ts +4 -0
  122. package/clinkx-workflows/dist/workflow-receipt.js +177 -0
  123. package/clinkx-workflows/dist/workflow-receipt.js.map +1 -0
  124. package/clinkx-workflows/dist/workflow-tools.d.ts +77 -0
  125. package/clinkx-workflows/dist/workflow-tools.js +1131 -0
  126. package/clinkx-workflows/dist/workflow-tools.js.map +1 -0
  127. package/clinkx-workflows/dist/workflows/council-default.d.ts +123 -0
  128. package/clinkx-workflows/dist/workflows/council-default.js +141 -0
  129. package/clinkx-workflows/dist/workflows/council-default.js.map +1 -0
  130. package/clinkx-workflows/dist/workflows/index.d.ts +12 -0
  131. package/clinkx-workflows/dist/workflows/index.js +15 -0
  132. package/clinkx-workflows/dist/workflows/index.js.map +1 -0
  133. package/conf/adapters/codex.json +2 -2
  134. package/conf/adapters/hapi/codex.json +2 -2
  135. package/dist/config.d.ts +5 -0
  136. package/dist/config.js +17 -0
  137. package/dist/config.js.map +1 -1
  138. package/dist/parsers/extract.d.ts +2 -0
  139. package/dist/parsers/extract.js +29 -20
  140. package/dist/parsers/extract.js.map +1 -1
  141. package/dist/pipeline.d.ts +2 -4
  142. package/dist/pipeline.js +93 -8
  143. package/dist/pipeline.js.map +1 -1
  144. package/dist/result-contract.d.ts +6 -1
  145. package/dist/result-contract.js +10 -22
  146. package/dist/result-contract.js.map +1 -1
  147. package/dist/runner.js +43 -1
  148. package/dist/runner.js.map +1 -1
  149. package/package.json +11 -5
@@ -0,0 +1,838 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
4
+ import { dirname, basename, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { hostname } from "node:os";
6
+ import { fileURLToPath } from "node:url";
7
+ import { WorkflowError } from "./errors.js";
8
+ import { buildFidelityMetadata, } from "./fidelity.js";
9
+ import { fingerprintsMatch, generateFingerprint, generateWorkflowFingerprint, } from "./fingerprint.js";
10
+ import { deriveAllowedRoots, getArtifactsDir, getChildConfigPath, getChildMaxConcurrent, getChildMaxOutputBytes, getChildMaxPromptBytes, getChildMaxResponseChars, getLogLevel, getRunStateDir, getStateDir, resolveWorkspaceRoot, } from "./config.js";
11
+ import { logger } from "./logger.js";
12
+ const STATE_FILENAME = "state.json";
13
+ const STATE_TMP_FILENAME = "state.json.tmp";
14
+ const WORKFLOW_SNAPSHOT_FILENAME = "workflow.snapshot.json";
15
+ const INPUTS_SNAPSHOT_FILENAME = "inputs.snapshot.json";
16
+ const CHILD_CONFIG_DIRNAME = "child-config";
17
+ const CHILD_CONFIG_SNAPSHOT_FILENAME = "snapshot.json";
18
+ const RUN_LOCK_FILENAME = "run.lock";
19
+ const OUTPUT_INLINE_THRESHOLD_BYTES = 10 * 1024;
20
+ export class WorkflowStateStore {
21
+ runDir;
22
+ definition;
23
+ variables;
24
+ hooks;
25
+ resume;
26
+ state;
27
+ lockPath;
28
+ debugKeepArtifacts;
29
+ writeChain = Promise.resolve();
30
+ lockHeld = true;
31
+ constructor(options) {
32
+ this.runDir = options.runDir;
33
+ this.definition = options.definition;
34
+ this.variables = options.variables;
35
+ this.state = options.state;
36
+ this.resume = options.resume;
37
+ this.debugKeepArtifacts = options.debugKeepArtifacts;
38
+ this.lockPath = join(options.runDir, RUN_LOCK_FILENAME);
39
+ this.hooks = {
40
+ onRunStateChange: async (event) => {
41
+ await this.enqueueWrite(async () => {
42
+ const nextState = structuredClone(this.state);
43
+ nextState.status = event.state;
44
+ nextState.startedAt = event.startedAt ?? nextState.startedAt;
45
+ nextState.completedAt = event.completedAt;
46
+ nextState.error = event.error;
47
+ nextState.failureClass = event.failureClass;
48
+ this.state = nextState;
49
+ await this.writeState();
50
+ });
51
+ },
52
+ onCallPrepared: async (event) => {
53
+ await this.enqueueWrite(async () => {
54
+ const inputPath = `${event.stageId}.${event.callId}.input.json`;
55
+ await writeJsonAtomic(join(this.runDir, inputPath), event.input);
56
+ const nextState = structuredClone(this.state);
57
+ const callEntry = getCallEntry(nextState, event.stageId, event.callId);
58
+ callEntry.materializedInputPath = inputPath;
59
+ callEntry.contextProducing = event.contextProducing;
60
+ callEntry.retryCount = event.retryCount;
61
+ this.state = nextState;
62
+ await this.writeState();
63
+ });
64
+ },
65
+ onCallStateChange: async (event) => {
66
+ await this.enqueueWrite(async () => {
67
+ const nextState = structuredClone(this.state);
68
+ const callEntry = getCallEntry(nextState, event.stageId, event.callId);
69
+ callEntry.state = event.state;
70
+ callEntry.retryCount = event.retryCount;
71
+ callEntry.startedAt = event.startedAt;
72
+ callEntry.completedAt = event.completedAt;
73
+ callEntry.durationMs = event.durationMs;
74
+ callEntry.errorMessage = event.error;
75
+ callEntry.failureClass = event.failureClass;
76
+ callEntry.childDebugMetadata = event.childDebugMetadata;
77
+ callEntry.contextProducing = callEntry.contextProducing ?? false;
78
+ if (event.outputText != null) {
79
+ const { outputInline, outputPath } = await persistOutput(this.runDir, event.stageId, event.callId, event.outputText);
80
+ callEntry.outputInline = outputInline;
81
+ callEntry.outputPath = outputPath;
82
+ }
83
+ if (event.artifact != null) {
84
+ callEntry.artifactPath = relative(this.runDir, event.artifact.filePath);
85
+ }
86
+ if (event.fidelity != null) {
87
+ callEntry.fidelity = buildPersistedFidelity(event.fidelity, callEntry.contextProducing ?? false);
88
+ }
89
+ for (const stageEntry of nextState.stages) {
90
+ stageEntry.state = deriveStageState(stageEntry.calls);
91
+ stageEntry.startedAt = stageEntry.calls.find((call) => call.startedAt != null)?.startedAt;
92
+ stageEntry.completedAt = deriveStageCompletedAt(stageEntry.calls);
93
+ }
94
+ this.state = nextState;
95
+ await this.writeState();
96
+ });
97
+ },
98
+ onRetryScheduled: async (event) => {
99
+ await this.applyRetryUpdate(event);
100
+ },
101
+ };
102
+ }
103
+ static async create(options) {
104
+ const runDir = options.runDir ?? getRunStateDir(options.runId);
105
+ const debugKeepArtifacts = options.debugKeepArtifacts ?? false;
106
+ const recycled = await prepareRunDirForCreate(runDir, options.runId);
107
+ let lockHeld = false;
108
+ try {
109
+ await acquireRunLock(runDir);
110
+ lockHeld = true;
111
+ const workflowSnapshotHash = await writeJsonSnapshot(join(runDir, WORKFLOW_SNAPSHOT_FILENAME), options.definition);
112
+ const inputsSnapshotHash = await writeJsonSnapshot(join(runDir, INPUTS_SNAPSHOT_FILENAME), options.variables);
113
+ const childConfigPath = getChildConfigPath();
114
+ const resumable = childConfigPath != null;
115
+ const fingerprintData = resumable
116
+ ? await buildChildConfigFingerprint(options.definition)
117
+ : undefined;
118
+ if (fingerprintData != null) {
119
+ const childConfigDir = join(runDir, CHILD_CONFIG_DIRNAME);
120
+ await mkdir(childConfigDir, { recursive: true });
121
+ await writeJsonAtomic(join(childConfigDir, CHILD_CONFIG_SNAPSHOT_FILENAME), fingerprintData.snapshot);
122
+ }
123
+ const state = {
124
+ state_version: 1,
125
+ runId: options.runId,
126
+ workflowName: options.definition.name,
127
+ workflowVersion: options.definition.version,
128
+ status: "pending",
129
+ resumable,
130
+ fingerprint: fingerprintData?.fingerprint,
131
+ workflowSnapshotPath: WORKFLOW_SNAPSHOT_FILENAME,
132
+ workflowSnapshotHash,
133
+ inputsSnapshotPath: INPUTS_SNAPSHOT_FILENAME,
134
+ inputsSnapshotHash,
135
+ childConfigSnapshotPath: fingerprintData != null
136
+ ? join(CHILD_CONFIG_DIRNAME, CHILD_CONFIG_SNAPSHOT_FILENAME)
137
+ : undefined,
138
+ stages: options.definition.stages.map((stage) => ({
139
+ stageId: stage.id,
140
+ state: "pending",
141
+ calls: stage.calls.map((call) => ({
142
+ stageId: stage.id,
143
+ callId: call.id,
144
+ state: "pending",
145
+ retryCount: 0,
146
+ contextProducing: isContextProducing(options.definition, stage.id, call.id),
147
+ })),
148
+ })),
149
+ variables: options.variables,
150
+ debugKeepArtifacts,
151
+ };
152
+ await writeJsonAtomic(join(runDir, STATE_FILENAME), state);
153
+ if (recycled) {
154
+ await rm(`${runDir}.recycled`, { recursive: true, force: true }).catch(() => { });
155
+ }
156
+ return new WorkflowStateStore({
157
+ runDir,
158
+ definition: options.definition,
159
+ variables: options.variables,
160
+ state,
161
+ resume: {},
162
+ debugKeepArtifacts,
163
+ });
164
+ }
165
+ catch (error) {
166
+ if (lockHeld) {
167
+ await safeUnlink(join(runDir, RUN_LOCK_FILENAME));
168
+ }
169
+ if (recycled) {
170
+ try {
171
+ await rm(runDir, { recursive: true, force: true });
172
+ await rename(`${runDir}.recycled`, runDir);
173
+ }
174
+ catch {
175
+ // Best effort — if both are gone, nothing we can do
176
+ }
177
+ }
178
+ else {
179
+ await rm(runDir, { recursive: true, force: true }).catch(() => { });
180
+ }
181
+ throw error;
182
+ }
183
+ }
184
+ static async resume(options) {
185
+ const runDir = options.runDir ?? getRunStateDir(options.runId);
186
+ const debugKeepArtifacts = options.debugKeepArtifacts ?? false;
187
+ await acquireRunLock(runDir);
188
+ try {
189
+ const state = await readRunState(runDir);
190
+ if (!state.resumable) {
191
+ throw new Error(`Run ${options.runId} is non-resumable because it was started from ambient child config`);
192
+ }
193
+ const workflowSnapshotPath = join(runDir, state.workflowSnapshotPath ?? WORKFLOW_SNAPSHOT_FILENAME);
194
+ const inputsSnapshotPath = join(runDir, state.inputsSnapshotPath ?? INPUTS_SNAPSHOT_FILENAME);
195
+ const definition = await readJsonFile(workflowSnapshotPath);
196
+ const variables = await readJsonFile(inputsSnapshotPath);
197
+ await verifySnapshotHash(workflowSnapshotPath, state.workflowSnapshotHash, "workflow snapshot");
198
+ await verifySnapshotHash(inputsSnapshotPath, state.inputsSnapshotHash, "inputs snapshot");
199
+ if (state.fingerprint == null) {
200
+ throw new Error(`Run ${options.runId} is missing a persisted fingerprint and cannot be resumed`);
201
+ }
202
+ // Verify fingerprint from persisted snapshot (self-contained, no live config dependency)
203
+ if (state.childConfigSnapshotPath == null) {
204
+ throw new Error(`Run ${options.runId} is missing a persisted child-config snapshot and cannot be resumed`);
205
+ }
206
+ const snapshotPath = join(runDir, state.childConfigSnapshotPath);
207
+ let persistedSnapshot;
208
+ try {
209
+ persistedSnapshot = await readJsonFile(snapshotPath);
210
+ }
211
+ catch (err) {
212
+ throw new Error(`Run ${options.runId} has a corrupted or missing child-config snapshot at ${snapshotPath}: ${err instanceof Error ? err.message : String(err)}`);
213
+ }
214
+ const snapshotFingerprint = generateFingerprint(persistedSnapshot);
215
+ if (!fingerprintsMatch(state.fingerprint, snapshotFingerprint)) {
216
+ throw new Error(`Run ${options.runId} fingerprint does not match persisted snapshot. The snapshot may be corrupted; start a new run instead.`);
217
+ }
218
+ // Also verify live config hasn't drifted from original run
219
+ const childConfigPath = getChildConfigPath();
220
+ if (childConfigPath != null) {
221
+ const currentFingerprint = await buildChildConfigFingerprint(definition);
222
+ if (!fingerprintsMatch(state.fingerprint, currentFingerprint.fingerprint)) {
223
+ throw new Error(`Run ${options.runId} cannot be resumed: live child config at ${childConfigPath} has changed since the original run. ` +
224
+ `Restore the original config or start a new run.`);
225
+ }
226
+ }
227
+ else {
228
+ // Live config path is missing but run was created with one
229
+ throw new Error(`Run ${options.runId} cannot be resumed: CLINKX_WORKFLOWS_CHILD_CONFIG_PATH is not set but the original run required it for child spawning.`);
230
+ }
231
+ const recovered = structuredClone(state);
232
+ const recoveredAt = new Date().toISOString();
233
+ let mutated = false;
234
+ if (recovered.status === "running") {
235
+ recovered.status = "failed";
236
+ recovered.completedAt = recoveredAt;
237
+ recovered.error = "Recovered stale running run during resume";
238
+ recovered.failureClass = "transient";
239
+ mutated = true;
240
+ }
241
+ for (const stage of recovered.stages) {
242
+ for (const call of stage.calls) {
243
+ if (call.state === "running") {
244
+ call.state = "failed";
245
+ call.completedAt = recoveredAt;
246
+ call.errorMessage = "Recovered stale running call during resume";
247
+ call.failureClass = "transient";
248
+ mutated = true;
249
+ }
250
+ }
251
+ stage.state = deriveStageState(stage.calls);
252
+ stage.completedAt = deriveStageCompletedAt(stage.calls);
253
+ }
254
+ if (mutated) {
255
+ await writeJsonAtomic(join(runDir, STATE_FILENAME), recovered);
256
+ }
257
+ const resume = await buildResumeState(definition, recovered, runDir);
258
+ return new WorkflowStateStore({
259
+ runDir,
260
+ definition,
261
+ variables,
262
+ state: recovered,
263
+ resume,
264
+ debugKeepArtifacts,
265
+ });
266
+ }
267
+ catch (error) {
268
+ await safeUnlink(join(runDir, RUN_LOCK_FILENAME));
269
+ throw error;
270
+ }
271
+ }
272
+ get runState() {
273
+ return structuredClone(this.state);
274
+ }
275
+ async markCancelled(reason = "Workflow cancelled") {
276
+ await this.enqueueWrite(async () => {
277
+ const nextState = structuredClone(this.state);
278
+ nextState.status = "cancelled";
279
+ nextState.completedAt = new Date().toISOString();
280
+ nextState.error = reason;
281
+ this.state = nextState;
282
+ await this.writeState();
283
+ });
284
+ }
285
+ async flush() {
286
+ await this.writeChain;
287
+ }
288
+ async close() {
289
+ await this.flush();
290
+ if (this.state.status === "succeeded" && !this.debugKeepArtifacts) {
291
+ this.lockHeld = false;
292
+ await rm(this.runDir, { recursive: true, force: true });
293
+ return;
294
+ }
295
+ if (this.lockHeld) {
296
+ await safeUnlink(this.lockPath);
297
+ this.lockHeld = false;
298
+ }
299
+ }
300
+ async applyRetryUpdate(event) {
301
+ await this.enqueueWrite(async () => {
302
+ const nextState = structuredClone(this.state);
303
+ const callEntry = getCallEntry(nextState, event.stageId, event.callId);
304
+ callEntry.retryCount = event.nextRetryCount;
305
+ callEntry.errorMessage = event.error;
306
+ callEntry.failureClass = event.failureClass;
307
+ this.state = nextState;
308
+ await this.writeState();
309
+ });
310
+ }
311
+ async enqueueWrite(task) {
312
+ const next = this.writeChain.then(task, task);
313
+ this.writeChain = next.catch(() => { });
314
+ await next;
315
+ }
316
+ async writeState() {
317
+ await writeJsonAtomic(join(this.runDir, STATE_FILENAME), this.state);
318
+ }
319
+ }
320
+ async function prepareRunDirForCreate(runDir, runId) {
321
+ await mkdir(dirname(runDir), { recursive: true });
322
+ try {
323
+ await mkdir(runDir);
324
+ return false;
325
+ }
326
+ catch (error) {
327
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
328
+ throw error;
329
+ }
330
+ }
331
+ const recycled = await recycleSucceededRunDir(runDir);
332
+ if (recycled) {
333
+ const recycledPath = `${runDir}.recycled`;
334
+ try {
335
+ await mkdir(runDir);
336
+ }
337
+ catch (error) {
338
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
339
+ throw duplicateRunDirError(runId);
340
+ }
341
+ try {
342
+ await rename(recycledPath, runDir);
343
+ }
344
+ catch {
345
+ logger.warn({ recycledPath }, "failed to restore recycled directory after mkdir failure");
346
+ }
347
+ throw error;
348
+ }
349
+ return true;
350
+ }
351
+ throw duplicateRunDirError(runId);
352
+ }
353
+ function duplicateRunDirError(runId) {
354
+ return new WorkflowError(`Run directory for run_id "${runId}" already exists. Use a unique run_id for new runs. If this is a failed/cancelled resumable run, try resume_workflow.`);
355
+ }
356
+ async function recycleSucceededRunDir(runDir) {
357
+ const lockPath = join(runDir, RUN_LOCK_FILENAME);
358
+ let lockHeld = false;
359
+ try {
360
+ await acquireRunLock(runDir);
361
+ lockHeld = true;
362
+ }
363
+ catch {
364
+ return false;
365
+ }
366
+ try {
367
+ const state = await readRunState(runDir);
368
+ if (state.status !== "succeeded") {
369
+ return false;
370
+ }
371
+ await rename(runDir, `${runDir}.recycled`);
372
+ lockHeld = false;
373
+ return true;
374
+ }
375
+ catch {
376
+ return false;
377
+ }
378
+ finally {
379
+ if (lockHeld) {
380
+ await safeUnlink(lockPath);
381
+ }
382
+ }
383
+ }
384
+ function getCallEntry(state, stageId, callId) {
385
+ const stage = state.stages.find((entry) => entry.stageId === stageId);
386
+ if (stage == null) {
387
+ throw new Error(`Unknown stage "${stageId}" in persisted state`);
388
+ }
389
+ const call = stage.calls.find((entry) => entry.callId === callId);
390
+ if (call == null) {
391
+ throw new Error(`Unknown call "${stageId}.${callId}" in persisted state`);
392
+ }
393
+ return call;
394
+ }
395
+ function deriveStageState(calls) {
396
+ if (calls.every((call) => call.state === "succeeded")) {
397
+ return "succeeded";
398
+ }
399
+ if (calls.some((call) => call.state === "running")) {
400
+ return "running";
401
+ }
402
+ if (calls.some((call) => call.state === "cancelled")) {
403
+ return "cancelled";
404
+ }
405
+ if (calls.some((call) => call.state === "failed")) {
406
+ return "failed";
407
+ }
408
+ return "pending";
409
+ }
410
+ function deriveStageCompletedAt(calls) {
411
+ const completedValues = calls
412
+ .map((call) => call.completedAt)
413
+ .filter((value) => value != null)
414
+ .sort();
415
+ return completedValues.length > 0 ? completedValues[completedValues.length - 1] : undefined;
416
+ }
417
+ async function persistOutput(runDir, stageId, callId, outputText) {
418
+ const sizeBytes = Buffer.byteLength(outputText, "utf-8");
419
+ if (sizeBytes <= OUTPUT_INLINE_THRESHOLD_BYTES) {
420
+ return {
421
+ outputInline: outputText,
422
+ outputPath: undefined,
423
+ };
424
+ }
425
+ const relativePath = `${stageId}.${callId}.output.txt`;
426
+ await writeTextAtomic(join(runDir, relativePath), outputText);
427
+ return {
428
+ outputInline: undefined,
429
+ outputPath: relativePath,
430
+ };
431
+ }
432
+ async function buildResumeState(definition, state, runDir) {
433
+ const completedCalls = new Map();
434
+ const preparedInputs = new Map();
435
+ const failedCalls = new Map();
436
+ const firstIncompleteStageIndex = definition.stages.findIndex((stage) => {
437
+ const persistedStage = state.stages.find((entry) => entry.stageId === stage.id);
438
+ return !persistedStage?.calls.every((call) => call.state === "succeeded");
439
+ });
440
+ const resumeStageIndex = firstIncompleteStageIndex === -1
441
+ ? definition.stages.length
442
+ : firstIncompleteStageIndex;
443
+ for (let stageIndex = 0; stageIndex < definition.stages.length; stageIndex += 1) {
444
+ const stage = definition.stages[stageIndex];
445
+ const persistedStage = state.stages.find((entry) => entry.stageId === stage.id);
446
+ if (persistedStage == null) {
447
+ continue;
448
+ }
449
+ if (stageIndex < resumeStageIndex) {
450
+ for (const call of stage.calls) {
451
+ const entry = persistedStage.calls.find((candidate) => candidate.callId === call.id);
452
+ if (entry?.state === "succeeded") {
453
+ completedCalls.set(`${stage.id}.${call.id}`, await rehydrateExecutedCall(runDir, entry));
454
+ }
455
+ else if (entry?.state === "failed" && entry.errorMessage != null) {
456
+ failedCalls.set(`${stage.id}.${call.id}`, {
457
+ stageId: stage.id,
458
+ callId: call.id,
459
+ error: entry.errorMessage,
460
+ });
461
+ }
462
+ }
463
+ continue;
464
+ }
465
+ if (stageIndex === resumeStageIndex) {
466
+ for (const call of stage.calls) {
467
+ const entry = persistedStage.calls.find((candidate) => candidate.callId === call.id);
468
+ if (entry?.state === "succeeded") {
469
+ completedCalls.set(`${stage.id}.${call.id}`, await rehydrateExecutedCall(runDir, entry));
470
+ continue;
471
+ }
472
+ if (entry?.state === "failed" && entry.errorMessage != null) {
473
+ failedCalls.set(`${stage.id}.${call.id}`, {
474
+ stageId: stage.id,
475
+ callId: call.id,
476
+ error: entry.errorMessage,
477
+ });
478
+ }
479
+ if (entry?.materializedInputPath != null) {
480
+ preparedInputs.set(`${stage.id}.${call.id}`, await readJsonFile(join(runDir, entry.materializedInputPath)));
481
+ }
482
+ }
483
+ }
484
+ }
485
+ return {
486
+ completedCalls,
487
+ preparedInputs,
488
+ failedCalls,
489
+ };
490
+ }
491
+ async function rehydrateExecutedCall(runDir, entry) {
492
+ const outputText = await readPersistedOutput(runDir, entry);
493
+ const artifact = entry.artifactPath != null
494
+ ? await rehydrateArtifact(runDir, entry.stageId, entry.callId, entry.artifactPath)
495
+ : undefined;
496
+ const fidelity = entry.fidelity != null ? rehydrateFidelity(entry.fidelity) : undefined;
497
+ return {
498
+ stageId: entry.stageId,
499
+ callId: entry.callId,
500
+ ok: true,
501
+ outputText,
502
+ artifact,
503
+ fidelity,
504
+ };
505
+ }
506
+ async function readPersistedOutput(runDir, entry) {
507
+ if (entry.outputInline != null) {
508
+ return entry.outputInline;
509
+ }
510
+ if (entry.outputPath != null) {
511
+ return await readFile(join(runDir, entry.outputPath), "utf-8");
512
+ }
513
+ throw new Error(`Succeeded call ${entry.stageId}.${entry.callId} is missing persisted output`);
514
+ }
515
+ async function rehydrateArtifact(runDir, stageId, callId, artifactPath) {
516
+ const filePath = join(runDir, artifactPath);
517
+ const serialized = await readFile(filePath, "utf-8");
518
+ const parsed = JSON.parse(serialized);
519
+ const stats = await stat(filePath);
520
+ return {
521
+ stageId,
522
+ callId,
523
+ reference: `${stageId}.${callId}`,
524
+ filePath,
525
+ filename: basename(filePath),
526
+ sizeBytes: stats.size,
527
+ parsed,
528
+ };
529
+ }
530
+ function rehydrateFidelity(metadata) {
531
+ return {
532
+ responseTruncated: metadata.responseTruncated,
533
+ responseTrailer: metadata.responseTrailer,
534
+ captureTruncated: metadata.captureTruncated,
535
+ stdoutTruncated: metadata.stdoutTruncated,
536
+ stderrTruncated: metadata.stderrTruncated,
537
+ parseSource: metadata.extractionPath === "result_contract" ? "result_contract" : "parser",
538
+ };
539
+ }
540
+ function buildPersistedFidelity(fidelity, contextProducing) {
541
+ return buildFidelityMetadata(fidelity, {
542
+ contextProducing,
543
+ resultContractEnabled: fidelity.parseSource === "result_contract",
544
+ });
545
+ }
546
+ async function buildChildConfigFingerprint(definition) {
547
+ const configPath = getChildConfigPath();
548
+ if (configPath == null) {
549
+ throw new Error("Persistent resume requires CLINKX_WORKFLOWS_CHILD_CONFIG_PATH");
550
+ }
551
+ const adapterConfigs = await loadNormalizedAdapterConfigs(configPath);
552
+ const resolvedPrompts = await loadResolvedPrompts(configPath, adapterConfigs);
553
+ const workspaceRoot = resolveWorkspaceRoot();
554
+ const childEnv = {
555
+ CLINKX_MAX_CONCURRENT: String(getChildMaxConcurrent()),
556
+ CLINKX_MAX_RESPONSE_CHARS: String(getChildMaxResponseChars()),
557
+ CLINKX_MAX_OUTPUT_BYTES: String(getChildMaxOutputBytes()),
558
+ CLINKX_MAX_PROMPT_BYTES: String(getChildMaxPromptBytes()),
559
+ CLINKX_CONFIG_PATH: configPath,
560
+ CLINKX_ALLOWED_ROOTS: deriveAllowedRoots(workspaceRoot, getStateDir(), getArtifactsDir()),
561
+ CLINKX_LOG_LEVEL: getLogLevel(),
562
+ };
563
+ return generateWorkflowFingerprint(definition, adapterConfigs, resolvedPrompts, childEnv);
564
+ }
565
+ async function loadNormalizedAdapterConfigs(configPath) {
566
+ const paths = await listConfigFiles(configPath);
567
+ const configs = new Map();
568
+ for (const filePath of paths) {
569
+ const raw = await readJsonFile(filePath);
570
+ const config = normalizeAdapterConfig(raw);
571
+ if (configs.has(config.name)) {
572
+ continue;
573
+ }
574
+ configs.set(config.name, {
575
+ ...config,
576
+ command: resolveCommandPath(config.command),
577
+ });
578
+ }
579
+ return Object.fromEntries([...configs.entries()].sort(([a], [b]) => a.localeCompare(b)));
580
+ }
581
+ async function loadResolvedPrompts(configPath, adapterConfigs) {
582
+ const configFiles = await listConfigFiles(configPath);
583
+ const filesByName = new Map();
584
+ for (const filePath of configFiles) {
585
+ const raw = await readJsonFile(filePath);
586
+ const config = normalizeAdapterConfig(raw);
587
+ if (!filesByName.has(config.name)) {
588
+ filesByName.set(config.name, filePath);
589
+ }
590
+ }
591
+ const prompts = new Map();
592
+ for (const [name, value] of Object.entries(adapterConfigs)) {
593
+ const config = value;
594
+ const filePath = filesByName.get(name);
595
+ if (filePath == null) {
596
+ continue;
597
+ }
598
+ for (const [roleName, role] of Object.entries(config.roles)) {
599
+ const prompt = await resolveRolePrompt(filePath, role);
600
+ if (prompt != null) {
601
+ prompts.set(`${name}.${roleName}`, prompt);
602
+ }
603
+ }
604
+ }
605
+ return Object.fromEntries([...prompts.entries()].sort(([a], [b]) => a.localeCompare(b)));
606
+ }
607
+ async function resolveRolePrompt(configFilePath, role) {
608
+ if (role.inline_prompt != null) {
609
+ return role.inline_prompt;
610
+ }
611
+ if (role.prompt_file == null) {
612
+ return undefined;
613
+ }
614
+ const resolved = resolve(dirname(configFilePath), role.prompt_file);
615
+ const builtinFallback = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "conf", "adapters", role.prompt_file);
616
+ if (await fileExists(resolved)) {
617
+ return (await readFile(resolved, "utf-8")).trimEnd();
618
+ }
619
+ if (await fileExists(builtinFallback)) {
620
+ return (await readFile(builtinFallback, "utf-8")).trimEnd();
621
+ }
622
+ throw new Error(`Prompt file not found for ${configFilePath}: ${role.prompt_file}`);
623
+ }
624
+ function normalizeAdapterConfig(raw) {
625
+ const rolesRecord = asRecord(raw["roles"]);
626
+ const roles = Object.fromEntries(Object.entries(rolesRecord).map(([name, value]) => [name, normalizeRoleConfig(asRecord(value))]));
627
+ return {
628
+ name: expectString(raw["name"], "adapter.name"),
629
+ command: expectString(raw["command"], "adapter.command"),
630
+ runner: asOptionalString(raw["runner"]),
631
+ args: asStringArray(raw["args"]),
632
+ unsafe_args: asStringArray(raw["unsafe_args"]),
633
+ env: asStringRecord(raw["env"]),
634
+ env_allowlist: asStringArray(raw["env_allowlist"]),
635
+ timeout_seconds: asOptionalNumber(raw["timeout_seconds"]) ?? 600,
636
+ idle_timeout_seconds: asOptionalNumber(raw["idle_timeout_seconds"]),
637
+ idle_timeout_startup_seconds: asOptionalNumber(raw["idle_timeout_startup_seconds"]),
638
+ parser: asOptionalString(raw["parser"]) ?? "text",
639
+ tolerate_nonzero_exit_for_parse: asOptionalBoolean(raw["tolerate_nonzero_exit_for_parse"]) ?? false,
640
+ capabilities_hint: asOptionalString(raw["capabilities_hint"]),
641
+ roles,
642
+ supports_images: asOptionalBoolean(raw["supports_images"]) ?? false,
643
+ requires_tty: asOptionalBoolean(raw["requires_tty"]) ?? false,
644
+ result_contract: normalizeResultContract(asRecord(raw["result_contract"])),
645
+ prompt_mode: normalizePromptMode(raw["prompt_mode"]),
646
+ stdin_placeholder_arg: asOptionalString(raw["stdin_placeholder_arg"]),
647
+ native_output_file_flag: asOptionalString(raw["native_output_file_flag"]),
648
+ suppress_stdout: asOptionalBoolean(raw["suppress_stdout"]) ?? false,
649
+ };
650
+ }
651
+ function normalizeRoleConfig(raw) {
652
+ return {
653
+ inline_prompt: asOptionalString(raw["inline_prompt"]),
654
+ prompt_file: asOptionalString(raw["prompt_file"]),
655
+ parser: asOptionalString(raw["parser"]),
656
+ extra_args: asStringArray(raw["extra_args"]),
657
+ };
658
+ }
659
+ function normalizeResultContract(raw) {
660
+ return {
661
+ enabled: asOptionalBoolean(raw["enabled"]) ?? false,
662
+ filename: raw["filename"] === "RESULT.json" ? "RESULT.json" : "RESULT.md",
663
+ };
664
+ }
665
+ function normalizePromptMode(raw) {
666
+ return raw === "arg" || raw === "file" ? raw : "stdin";
667
+ }
668
+ async function listConfigFiles(configPath) {
669
+ const configStats = await stat(configPath);
670
+ if (!configStats.isDirectory()) {
671
+ return [configPath];
672
+ }
673
+ const entries = await (await import("node:fs/promises")).readdir(configPath, { withFileTypes: true });
674
+ return entries
675
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
676
+ .map((entry) => join(configPath, entry.name))
677
+ .sort();
678
+ }
679
+ async function readRunState(runDir) {
680
+ const statePath = join(runDir, STATE_FILENAME);
681
+ const state = await readJsonFile(statePath);
682
+ if (state.state_version !== 1) {
683
+ throw new Error(`Unsupported state_version ${String(state.state_version)} in ${statePath}`);
684
+ }
685
+ return state;
686
+ }
687
+ async function verifySnapshotHash(filePath, expectedHash, label) {
688
+ if (expectedHash == null) {
689
+ throw new Error(`Missing ${label} hash for ${filePath}`);
690
+ }
691
+ const content = await readFile(filePath, "utf-8");
692
+ const actualHash = hashContent(content);
693
+ if (actualHash !== expectedHash) {
694
+ throw new Error(`Corrupted ${label}: hash mismatch for ${filePath}`);
695
+ }
696
+ }
697
+ async function writeJsonSnapshot(filePath, value) {
698
+ const serialized = serializeJson(value);
699
+ await writeTextAtomic(filePath, serialized);
700
+ return hashContent(serialized);
701
+ }
702
+ async function writeJsonAtomic(filePath, value) {
703
+ await writeTextAtomic(filePath, serializeJson(value));
704
+ }
705
+ async function writeTextAtomic(filePath, content) {
706
+ await mkdir(dirname(filePath), { recursive: true });
707
+ const tmpPath = `${filePath}.tmp`;
708
+ await writeFile(tmpPath, content, "utf-8");
709
+ await rename(tmpPath, filePath);
710
+ }
711
+ function serializeJson(value) {
712
+ return `${JSON.stringify(value, null, 2)}\n`;
713
+ }
714
+ function hashContent(content) {
715
+ return createHash("sha256").update(content, "utf-8").digest("hex");
716
+ }
717
+ async function readJsonFile(filePath) {
718
+ return JSON.parse(await readFile(filePath, "utf-8"));
719
+ }
720
+ async function acquireRunLock(runDir) {
721
+ const lockPath = join(runDir, RUN_LOCK_FILENAME);
722
+ const record = {
723
+ pid: process.pid,
724
+ hostname: hostname(),
725
+ startedAt: new Date().toISOString(),
726
+ };
727
+ for (let attempt = 0; attempt < 2; attempt += 1) {
728
+ try {
729
+ const handle = await open(lockPath, "wx");
730
+ try {
731
+ await handle.writeFile(serializeJson(record), "utf-8");
732
+ }
733
+ finally {
734
+ await handle.close();
735
+ }
736
+ return;
737
+ }
738
+ catch (error) {
739
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
740
+ throw error;
741
+ }
742
+ const existing = await readJsonFile(lockPath);
743
+ if (existing.hostname === hostname() && !isPidLive(existing.pid)) {
744
+ await unlink(lockPath);
745
+ continue;
746
+ }
747
+ throw new Error(`Run lock is held for ${runDir} by pid ${String(existing.pid)} on ${existing.hostname} since ${existing.startedAt}`);
748
+ }
749
+ }
750
+ }
751
+ function isPidLive(pid) {
752
+ try {
753
+ process.kill(pid, 0);
754
+ return true;
755
+ }
756
+ catch (error) {
757
+ return error.code !== "ESRCH";
758
+ }
759
+ }
760
+ function resolveCommandPath(command) {
761
+ if (command.includes("/")) {
762
+ return isAbsolute(command) ? command : resolve(command);
763
+ }
764
+ try {
765
+ return execFileSync("which", [command], { encoding: "utf-8" }).trim();
766
+ }
767
+ catch {
768
+ return command;
769
+ }
770
+ }
771
+ function isContextProducing(definition, stageId, callId) {
772
+ const target = `${stageId}.${callId}`;
773
+ for (const stage of definition.stages) {
774
+ for (const call of stage.calls) {
775
+ for (const ref of call.context_from ?? []) {
776
+ if (ref === target) {
777
+ return true;
778
+ }
779
+ if (ref === stageId) {
780
+ return true;
781
+ }
782
+ }
783
+ }
784
+ }
785
+ return false;
786
+ }
787
+ async function safeUnlink(filePath) {
788
+ try {
789
+ await unlink(filePath);
790
+ }
791
+ catch (error) {
792
+ const code = error.code;
793
+ if (code !== "ENOENT") {
794
+ logger.warn({ path: filePath, err: error instanceof Error ? error.message : String(error) }, "failed to remove file");
795
+ }
796
+ }
797
+ }
798
+ async function fileExists(filePath) {
799
+ try {
800
+ await stat(filePath);
801
+ return true;
802
+ }
803
+ catch {
804
+ return false;
805
+ }
806
+ }
807
+ function asRecord(value) {
808
+ return value != null && typeof value === "object" ? value : {};
809
+ }
810
+ function asOptionalString(value) {
811
+ return typeof value === "string" && value.length > 0 ? value : undefined;
812
+ }
813
+ function expectString(value, label) {
814
+ const parsed = asOptionalString(value);
815
+ if (parsed == null) {
816
+ throw new Error(`Expected ${label} to be a non-empty string`);
817
+ }
818
+ return parsed;
819
+ }
820
+ function asOptionalNumber(value) {
821
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
822
+ }
823
+ function asOptionalBoolean(value) {
824
+ return typeof value === "boolean" ? value : undefined;
825
+ }
826
+ function asStringArray(value) {
827
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
828
+ }
829
+ function asStringRecord(value) {
830
+ const result = {};
831
+ for (const [key, entry] of Object.entries(asRecord(value))) {
832
+ if (typeof entry === "string") {
833
+ result[key] = entry;
834
+ }
835
+ }
836
+ return result;
837
+ }
838
+ //# sourceMappingURL=state.js.map