harnery 0.44.0 → 0.45.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 (87) hide show
  1. package/dist/commander.js +1 -0
  2. package/dist/commands/files.d.ts +2 -2
  3. package/dist/commands/files.d.ts.map +1 -1
  4. package/dist/commands/files.js +7 -4
  5. package/dist/commands/servers.d.ts.map +1 -1
  6. package/dist/commands/servers.js +11 -17
  7. package/dist/commands/tunnel.d.ts +15 -0
  8. package/dist/commands/tunnel.d.ts.map +1 -1
  9. package/dist/commands/tunnel.js +453 -153
  10. package/dist/commands/turn-rollback.d.ts +5 -0
  11. package/dist/commands/turn-rollback.d.ts.map +1 -0
  12. package/dist/commands/turn-rollback.js +76 -0
  13. package/dist/commands/workflow.d.ts.map +1 -1
  14. package/dist/commands/workflow.js +44 -0
  15. package/dist/core/agents/coord-client.d.ts.map +1 -1
  16. package/dist/core/agents/coord-client.js +18 -0
  17. package/dist/core/agents/state/proc-start.d.ts +5 -6
  18. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  19. package/dist/core/agents/state/proc-start.js +13 -9
  20. package/dist/core/agents/state/process-handle.d.ts +26 -0
  21. package/dist/core/agents/state/process-handle.d.ts.map +1 -0
  22. package/dist/core/agents/state/process-handle.js +121 -0
  23. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  24. package/dist/core/artifacts/delivery-card.js +2 -1
  25. package/dist/core/resources/service.d.ts +2 -16
  26. package/dist/core/resources/service.d.ts.map +1 -1
  27. package/dist/core/resources/service.js +1 -78
  28. package/dist/core/semantic/service.d.ts.map +1 -1
  29. package/dist/core/semantic/service.js +2 -9
  30. package/dist/core/servers/index.d.ts +12 -11
  31. package/dist/core/servers/index.d.ts.map +1 -1
  32. package/dist/core/servers/index.js +194 -81
  33. package/dist/core/servers/tunnels.d.ts +5 -3
  34. package/dist/core/servers/tunnels.d.ts.map +1 -1
  35. package/dist/core/servers/tunnels.js +45 -2
  36. package/dist/core/supervisor/service.d.ts +2 -4
  37. package/dist/core/supervisor/service.d.ts.map +1 -1
  38. package/dist/core/turn-rollback/content.d.ts +53 -0
  39. package/dist/core/turn-rollback/content.d.ts.map +1 -0
  40. package/dist/core/turn-rollback/content.js +396 -0
  41. package/dist/core/turn-rollback/index.d.ts +72 -0
  42. package/dist/core/turn-rollback/index.d.ts.map +1 -0
  43. package/dist/core/turn-rollback/index.js +443 -0
  44. package/dist/core/workflow/index.d.ts +4 -2
  45. package/dist/core/workflow/index.d.ts.map +1 -1
  46. package/dist/core/workflow/index.js +2 -1
  47. package/dist/core/workflow/recovery-audit.d.ts +30 -0
  48. package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
  49. package/dist/core/workflow/recovery-audit.js +116 -0
  50. package/dist/core/workflow/run-state.d.ts +19 -2
  51. package/dist/core/workflow/run-state.d.ts.map +1 -1
  52. package/dist/core/workflow/run-state.js +113 -8
  53. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  54. package/dist/core/workflow/workspaces/local-git.js +70 -14
  55. package/dist/core/workflow/workspaces/reclaim.d.ts +6 -1
  56. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  57. package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
  58. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  59. package/dist/core/workflow/workspaces/recovery.js +24 -0
  60. package/dist/lib/tunnel/state.d.ts +13 -6
  61. package/dist/lib/tunnel/state.d.ts.map +1 -1
  62. package/dist/lib/tunnel/state.js +58 -12
  63. package/package.json +6 -1
  64. package/src/commander.ts +10 -0
  65. package/src/commands/files.ts +13 -5
  66. package/src/commands/servers.ts +12 -23
  67. package/src/commands/tunnel.ts +533 -166
  68. package/src/commands/turn-rollback.ts +111 -0
  69. package/src/commands/workflow.ts +59 -0
  70. package/src/core/agents/coord-client.ts +20 -0
  71. package/src/core/agents/state/proc-start.ts +14 -11
  72. package/src/core/agents/state/process-handle.ts +165 -0
  73. package/src/core/artifacts/delivery-card.ts +7 -1
  74. package/src/core/resources/service.ts +3 -88
  75. package/src/core/semantic/service.ts +2 -8
  76. package/src/core/servers/index.ts +234 -85
  77. package/src/core/servers/tunnels.ts +55 -3
  78. package/src/core/supervisor/service.ts +2 -3
  79. package/src/core/turn-rollback/content.ts +499 -0
  80. package/src/core/turn-rollback/index.ts +580 -0
  81. package/src/core/workflow/index.ts +12 -1
  82. package/src/core/workflow/recovery-audit.ts +181 -0
  83. package/src/core/workflow/run-state.ts +137 -8
  84. package/src/core/workflow/workspaces/local-git.ts +86 -15
  85. package/src/core/workflow/workspaces/reclaim.ts +4 -0
  86. package/src/core/workflow/workspaces/recovery.ts +23 -0
  87. package/src/lib/tunnel/state.ts +70 -15
@@ -1,5 +1,6 @@
1
1
  import type { RepoSnapshot } from "../context/index.js";
2
2
  import { type NormalizedPolicy, type PolicyIsolation, type PolicyNetworkAccess } from "../policy/index.js";
3
+ import { type WorkflowRecoveryAuthorization } from "./recovery-audit.js";
3
4
  import type { WorkflowAttemptContext, WorkflowDiagnosticAdmissionConfig, WorkflowSpecialistProfile, WorkflowWorkContext } from "./types.js";
4
5
  import type { WorkspaceBinding, WorkspaceCompatibilityExecutionEvidence } from "./workspaces/index.js";
5
6
  export declare const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION: 2;
@@ -48,7 +49,23 @@ export declare function assertWorkflowRunResumable(coordRoot: string, runId: str
48
49
  export declare function assertWorkflowScriptUnchanged(manifest: WorkflowRunManifest): void;
49
50
  export declare function workflowRunManifestPath(coordRoot: string, runId: string): string;
50
51
  /** Acquire an exclusive lease shared by resume and reclaim. Existing locks
51
- * always refuse: a stale observation cannot authorize unlinking a new owner.
52
- * Crash recovery requires an audited ownership check outside this operation. */
52
+ * always refuse; explicit audited recovery is a separate operation. */
53
53
  export declare function acquireWorkflowResumeLease(coordRoot: string, runId: string): () => void;
54
+ export interface WorkflowResumeRecoveryOptions extends Partial<WorkflowRecoveryAuthorization> {
55
+ yes?: boolean;
56
+ lockSha256?: string;
57
+ }
58
+ export interface WorkflowResumeRecoveryResult {
59
+ action: "preview" | "unlocked";
60
+ run_id: string;
61
+ lock_sha256: string | null;
62
+ owner_state: "dead" | "live" | "unknown" | "absent";
63
+ owner: {
64
+ pid: number;
65
+ host: string;
66
+ created_at: string;
67
+ } | null;
68
+ recovery_id?: string;
69
+ }
70
+ export declare function recoverWorkflowResumeLease(coordRoot: string, runId: string, options: WorkflowResumeRecoveryOptions): WorkflowResumeRecoveryResult;
54
71
  //# sourceMappingURL=run-state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/run-state.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,KAAK,EACV,sBAAsB,EACtB,iCAAiC,EACjC,yBAAyB,EACzB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAEV,gBAAgB,EAChB,uCAAuC,EACxC,MAAM,uBAAuB,CAAC;AAO/B,eAAO,MAAM,oCAAoC,EAAG,CAAU,CAAC;AAI/D,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,OAAO,oCAAoC,CAAC;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,eAAe,CAAC,EAAE,sBAAsB,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,iBAAiB,EAAE,YAAY,CAAC;IAChC,SAAS,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,eAAe,EAAE,MAAM,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,oBAAoB,CAAC,EAAE,iCAAiC,CAAC;QACzD,iBAAiB,EAAE,OAAO,CAAC;QAC3B,iBAAiB,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;QAC/B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,SAAS,EAAE,eAAe,CAAC;QAC3B,cAAc,EAAE,mBAAmB,CAAC;QACpC,MAAM,CAAC,EAAE,gBAAgB,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;QACxD,iBAAiB,CAAC,EAAE,gBAAgB,CAAC;QACrC,kBAAkB,CAAC,EAAE,uCAAuC,CAAC;KAC9D,CAAC;CACH;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG,MAAM,CAgBrF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,CAgD7F;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ;IAAE,QAAQ,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA4BvD;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CASjF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED;;gFAEgF;AAChF,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,IAAI,CA2CvF"}
1
+ {"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/run-state.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,17 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { hostname } from "node:os";
4
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { normalizePolicy, policyDigest, } from "../policy/index.js";
6
6
  import { stateFileMode } from "../storage/modes.js";
7
7
  import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.js";
8
8
  import { isCanonicalWorkflowAttemptContext } from "./attempt-context.js";
9
- import { readJsonRecord, writeImmutableJson } from "./durable-record.js";
9
+ import { fsyncParentDirectory, readJsonRecord, stableDigest, writeImmutableJson, } from "./durable-record.js";
10
+ import { beginWorkflowRecovery } from "./recovery-audit.js";
10
11
  import { normalizeWorkflowSpecialists } from "./specialists.js";
11
12
  import { WORKFLOW_DIAGNOSTIC_ADMISSION_SCHEMA_VERSION } from "./types.js";
12
13
  import { isCanonicalWorkflowWorkContext } from "./work-context.js";
14
+ import { acquireNoClobberLease } from "./workspaces/leases.js";
13
15
  import { assertWorkspaceManifestAuthority, isWorkspaceBinding, isWorkspaceCompatibilityExecutionEvidence, } from "./workspaces/validate.js";
14
16
  export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 2;
15
17
  const MANIFEST_LIMIT_BYTES = 256 * 1024;
@@ -128,8 +130,7 @@ export function workflowRunManifestPath(coordRoot, runId) {
128
130
  return join(coordRoot, ".harnery", "workflows", runId, "run.json");
129
131
  }
130
132
  /** Acquire an exclusive lease shared by resume and reclaim. Existing locks
131
- * always refuse: a stale observation cannot authorize unlinking a new owner.
132
- * Crash recovery requires an audited ownership check outside this operation. */
133
+ * always refuse; explicit audited recovery is a separate operation. */
133
134
  export function acquireWorkflowResumeLease(coordRoot, runId) {
134
135
  assertWorkflowRunId(runId);
135
136
  const path = join(coordRoot, ".harnery", "workflows", runId, "resume.lock");
@@ -146,12 +147,15 @@ export function acquireWorkflowResumeLease(coordRoot, runId) {
146
147
  const fd = openSync(path, "wx", stateFileMode());
147
148
  try {
148
149
  writeFileSync(fd, ownerBytes, "utf8");
150
+ fsyncSync(fd);
149
151
  }
150
152
  finally {
151
153
  closeSync(fd);
152
154
  }
153
155
  chmodSync(path, stateFileMode());
154
- return true;
156
+ fsyncParentDirectory(path);
157
+ const identity = lstatSync(path);
158
+ return { device: identity.dev, inode: identity.ino };
155
159
  }
156
160
  catch (error) {
157
161
  if (error.code !== "EEXIST")
@@ -159,7 +163,8 @@ export function acquireWorkflowResumeLease(coordRoot, runId) {
159
163
  return false;
160
164
  }
161
165
  };
162
- if (!acquire()) {
166
+ const acquired = withResumeLeaseMutation(coordRoot, runId, acquire);
167
+ if (!acquired) {
163
168
  const existing = readResumeLease(path);
164
169
  const holder = existing
165
170
  ? ` by pid ${existing.pid} on ${existing.host}; its liveness has not been verified`
@@ -168,14 +173,113 @@ export function acquireWorkflowResumeLease(coordRoot, runId) {
168
173
  }
169
174
  return () => {
170
175
  try {
171
- if (readFileSync(path, "utf8") === ownerBytes)
176
+ // Only this live process can release this nonce. Acquisition never
177
+ // replaces a present lock, and unlock requires ESRCH for its owner, so
178
+ // neither supported writer can replace this lock during synchronous
179
+ // release. Taking their mutation gate would strand a live owner's lock
180
+ // on ordinary contention. Pin both identity and bytes for stale closures.
181
+ const identity = lstatSync(path);
182
+ if (identity.isFile() &&
183
+ identity.dev === acquired.device &&
184
+ identity.ino === acquired.inode &&
185
+ readFileSync(path, "utf8") === ownerBytes) {
172
186
  unlinkSync(path);
187
+ fsyncParentDirectory(path);
188
+ }
173
189
  }
174
190
  catch {
175
191
  // Leave an unreadable or replaced lock untouched.
176
192
  }
177
193
  };
178
194
  }
195
+ export function recoverWorkflowResumeLease(coordRoot, runId, options) {
196
+ assertWorkflowRunId(runId);
197
+ const path = join(coordRoot, ".harnery", "workflows", runId, "resume.lock");
198
+ const inspect = () => {
199
+ let lockSha256;
200
+ try {
201
+ const stat = lstatSync(path);
202
+ if (!stat.isFile() || stat.size > 8 * 1024)
203
+ throw new Error("resume lock is not a bounded regular file");
204
+ lockSha256 = stableDigest({
205
+ bytes: readFileSync(path, "utf8"),
206
+ device: stat.dev,
207
+ inode: stat.ino,
208
+ });
209
+ }
210
+ catch (error) {
211
+ if (error.code !== "ENOENT")
212
+ throw error;
213
+ return {
214
+ action: "preview",
215
+ run_id: runId,
216
+ lock_sha256: null,
217
+ owner_state: "absent",
218
+ owner: null,
219
+ };
220
+ }
221
+ const owner = readResumeLease(path);
222
+ let ownerState = "unknown";
223
+ if (owner?.host === hostname()) {
224
+ try {
225
+ process.kill(owner.pid, 0);
226
+ ownerState = "live";
227
+ }
228
+ catch (error) {
229
+ if (error.code === "ESRCH")
230
+ ownerState = "dead";
231
+ }
232
+ }
233
+ return {
234
+ action: "preview",
235
+ run_id: runId,
236
+ lock_sha256: lockSha256,
237
+ owner_state: ownerState,
238
+ owner,
239
+ };
240
+ };
241
+ if (!options.yes)
242
+ return inspect();
243
+ return withResumeLeaseMutation(coordRoot, runId, () => {
244
+ const observed = inspect();
245
+ if (observed.owner_state !== "dead")
246
+ throw new Error(`workflow resume unlock refused: owner is ${observed.owner_state}`);
247
+ if (!options.lockSha256 || options.lockSha256 !== observed.lock_sha256)
248
+ throw new Error("workflow resume unlock requires the exact current preview digest");
249
+ const audit = beginWorkflowRecovery(coordRoot, {
250
+ run_id: runId,
251
+ action: "unlock_resume",
252
+ target_sha256: options.lockSha256,
253
+ target: { lock: "resume.lock", owner: observed.owner },
254
+ actor: options.actor,
255
+ reason: options.reason,
256
+ });
257
+ const current = inspect();
258
+ if (current.lock_sha256 !== observed.lock_sha256 || current.owner_state !== "dead")
259
+ throw new Error("workflow resume lock authority changed before unlock");
260
+ audit.assertCurrent();
261
+ unlinkSync(path);
262
+ fsyncParentDirectory(path);
263
+ audit.complete();
264
+ return { ...observed, action: "unlocked", recovery_id: audit.intent.operation_id };
265
+ });
266
+ }
267
+ function withResumeLeaseMutation(coordRoot, runId, action) {
268
+ const authority = stableDigest({ run_id: runId, lock: "resume.lock" });
269
+ const lease = acquireNoClobberLease({
270
+ path: join(coordRoot, ".harnery", "workflows", runId, "resume-mutation.lease"),
271
+ scope: "workflow-resume-mutation",
272
+ authoritySha256: authority,
273
+ staleAfterMs: 300_000,
274
+ validateStaleOwner: (owner) => owner.authority_sha256 === authority,
275
+ });
276
+ try {
277
+ return action();
278
+ }
279
+ finally {
280
+ lease.release();
281
+ }
282
+ }
179
283
  function readResumeLease(path) {
180
284
  try {
181
285
  const value = JSON.parse(readFileSync(path, "utf8"));
@@ -183,7 +287,8 @@ function readResumeLease(path) {
183
287
  Number.isSafeInteger(value.pid) &&
184
288
  value.pid > 0 &&
185
289
  typeof value.host === "string" &&
186
- typeof value.created_at === "string"
290
+ value.host.length > 0 &&
291
+ validTimestamp(value.created_at)
187
292
  ? { pid: value.pid, host: value.host, created_at: value.created_at }
188
293
  : null;
189
294
  }
@@ -1 +1 @@
1
- {"version":3,"file":"local-git.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/local-git.ts"],"names":[],"mappings":"AA6CA,OAAO,EAML,KAAK,uBAAuB,EAE5B,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAiBtB,OAAO,KAAK,EAYV,gBAAgB,EAIhB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB,CAAC,EAAE,MAAM,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACpC;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,+BAA+B,GACvC,wBAAwB,CAkB1B;AAED,wBAAsB,KAAK,CACzB,KAAK,EAAE,mBAAmB,EAC1B,wBAAwB,GAAE,MAAM,OAAwC,GACvE,OAAO,CAAC,oBAAoB,CAAC,CA4E/B"}
1
+ {"version":3,"file":"local-git.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/local-git.ts"],"names":[],"mappings":"AA8CA,OAAO,EAML,KAAK,uBAAuB,EAE5B,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAC;AAiBtB,OAAO,KAAK,EAYV,gBAAgB,EAIhB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB,CAAC,EAAE,MAAM,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,OAAO,CACL,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACpC;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,+BAA+B,GACvC,wBAAwB,CAkB1B;AAED,wBAAsB,KAAK,CACzB,KAAK,EAAE,mBAAmB,EAC1B,wBAAwB,GAAE,MAAM,OAAwC,GACvE,OAAO,CAAC,oBAAoB,CAAC,CA4E/B"}
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { stateDirMode, stateFileMode } from "../../storage/modes.js";
5
+ import { beginWorkflowRecovery, validateWorkflowRecoveryAuthorization } from "../recovery-audit.js";
5
6
  import { acquireWorkflowResumeLease } from "../run-state.js";
6
7
  import { WorkspaceAttestationError } from "./attestation-error.js";
7
8
  import { cleanupWorkspace } from "./cleanup.js";
@@ -10,7 +11,7 @@ import { acquireNoClobberLease } from "./leases.js";
10
11
  import { claimOverlapsWorkspace, reserveWorkspaceReclaim, } from "./ownership.js";
11
12
  import { assertContainedExisting, assertPathIdentity, candidateUnderRoot, containsPath, createContainedDirectories, descriptorBackedPathsSupported, descriptorPath, filesystemIdentity, openContainedDirectory, sameFilesystemIdentity, validateConfiguredRoot, } from "./paths.js";
12
13
  import { prepareReclaim, WorkflowReclaimError, } from "./reclaim.js";
13
- import { readWorkflowLiveness } from "./recovery.js";
14
+ import { hasActiveWorkflowEvidence, readWorkflowLiveness } from "./recovery.js";
14
15
  import { appendWorkspaceEvent, fileSha256, readWorkflowSupplement, readWorkspaceBinding, readWorkspaceClaim, readWorkspaceClaims, readWorkspaceEvents, stableDigest, workspaceClaimPath, workspaceLockPath, writeWorkspaceBinding, writeWorkspaceClaim, } from "./state.js";
15
16
  import { isWorkspaceAttestation } from "./validate.js";
16
17
  const PROVIDER_ID = "local-git-worktree";
@@ -597,6 +598,7 @@ function reclaimPreview(coordRoot, binding, mode) {
597
598
  run_id: binding.run_id,
598
599
  mode,
599
600
  liveness: readWorkflowLiveness(coordRoot, binding.run_id),
601
+ evidence_sha256: reclaimEvidenceDigest(coordRoot, binding),
600
602
  ownership: "unproven",
601
603
  dirty_paths: null,
602
604
  lease_holder: reclaimLeaseHolders(coordRoot, binding),
@@ -617,13 +619,36 @@ function reclaimPreview(coordRoot, binding, mode) {
617
619
  }
618
620
  return preview;
619
621
  }
620
- function assertReclaimLiveness(coordRoot, binding) {
622
+ function reclaimEvidenceDigest(coordRoot, binding) {
623
+ const evidence = (name) => {
624
+ try {
625
+ return readFileSync(join(coordRoot, ".harnery", "workflows", binding.run_id, name), "utf8");
626
+ }
627
+ catch (error) {
628
+ if (error.code === "ENOENT")
629
+ return null;
630
+ throw error;
631
+ }
632
+ };
633
+ return stableDigest({
634
+ binding,
635
+ proof: evidence("proof.json"),
636
+ transcript: evidence("transcript.jsonl"),
637
+ });
638
+ }
639
+ function assertReclaimLiveness(coordRoot, binding, recovery) {
621
640
  const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
641
+ if (liveness === "active" || (recovery && hasActiveWorkflowEvidence(coordRoot, binding.run_id))) {
642
+ throw new WorkflowReclaimError("workflow_reclaim_run_live", `run ${binding.run_id} has active evidence; its workspace was left untouched`);
643
+ }
644
+ if (recovery && reclaimEvidenceDigest(coordRoot, binding) !== recovery.evidenceSha256) {
645
+ throw new WorkflowReclaimError("workflow_reclaim_recovery_mismatch", "workflow reclaim requires the exact current preview evidence digest");
646
+ }
622
647
  if (liveness === "inactive")
623
648
  return;
624
- throw new WorkflowReclaimError(liveness === "active" ? "workflow_reclaim_run_live" : "workflow_reclaim_liveness_unknown", liveness === "active"
625
- ? `run ${binding.run_id} is active; its workspace was left untouched`
626
- : `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`);
649
+ if (liveness === "unknown" && recovery)
650
+ return;
651
+ throw new WorkflowReclaimError("workflow_reclaim_liveness_unknown", `run ${binding.run_id} has missing, corrupt, or ambiguous liveness evidence; its workspace was left untouched`);
627
652
  }
628
653
  function assertReclaimOwnership(coordRoot, binding) {
629
654
  try {
@@ -666,10 +691,10 @@ function assertPeerWorkspaceAuthority(coordRoot, claim) {
666
691
  throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", error.message);
667
692
  }
668
693
  }
669
- function reclaimAuthorityGuard(coordRoot, binding, claim, reservation) {
694
+ function reclaimAuthorityGuard(coordRoot, binding, claim, reservation, recovery) {
670
695
  try {
671
696
  reservation.assertCurrent();
672
- assertReclaimLiveness(coordRoot, binding);
697
+ assertReclaimLiveness(coordRoot, binding, recovery);
673
698
  assertReclaimOwnership(coordRoot, binding);
674
699
  assertPeerWorkspaceAuthority(coordRoot, claim);
675
700
  }
@@ -685,9 +710,11 @@ async function reclaim(coordRoot, provider, binding, options) {
685
710
  }
686
711
  if (!options.yes)
687
712
  return reclaimPreview(coordRoot, binding, options.mode);
713
+ if (options.unknownLiveness)
714
+ validateWorkflowRecoveryAuthorization(options.unknownLiveness);
688
715
  // Liveness comes before terminal-proof cleanup helpers. A live run may not
689
716
  // have a proof yet, and a missing proof must never be discovered after reset.
690
- assertReclaimLiveness(coordRoot, binding);
717
+ assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
691
718
  const claim = assertReclaimOwnership(coordRoot, binding);
692
719
  let reservation;
693
720
  try {
@@ -697,7 +724,7 @@ async function reclaim(coordRoot, provider, binding, options) {
697
724
  throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
698
725
  }
699
726
  try {
700
- reclaimAuthorityGuard(coordRoot, binding, claim, reservation);
727
+ reclaimAuthorityGuard(coordRoot, binding, claim, reservation, options.unknownLiveness);
701
728
  let releaseResume;
702
729
  try {
703
730
  releaseResume = acquireWorkflowResumeLease(coordRoot, binding.run_id);
@@ -722,9 +749,24 @@ async function reclaim(coordRoot, provider, binding, options) {
722
749
  throw new WorkflowReclaimError("workflow_reclaim_lease_held", error.message);
723
750
  }
724
751
  try {
725
- assertReclaimLiveness(coordRoot, binding);
752
+ assertReclaimLiveness(coordRoot, binding, options.unknownLiveness);
726
753
  assertReclaimOwnership(coordRoot, binding);
727
754
  const preview = reclaimPreview(coordRoot, binding, options.mode);
755
+ const audit = options.unknownLiveness
756
+ ? beginWorkflowRecovery(coordRoot, {
757
+ run_id: binding.run_id,
758
+ action: "reclaim_unknown_liveness",
759
+ actor: options.unknownLiveness.actor,
760
+ reason: options.unknownLiveness.reason,
761
+ target_sha256: options.unknownLiveness.evidenceSha256,
762
+ target: {
763
+ binding_id: binding.binding_id,
764
+ workspace_root: binding.workspace_root,
765
+ mode: options.mode,
766
+ liveness: preview.liveness,
767
+ },
768
+ })
769
+ : undefined;
728
770
  let preparation;
729
771
  // Keep ordinary cleanup's terminal-evidence checks, intent and receipts.
730
772
  // Its provider callback uses the leases already held here instead of
@@ -736,11 +778,22 @@ async function reclaim(coordRoot, provider, binding, options) {
736
778
  provider: {
737
779
  ...provider,
738
780
  cleanup: async (candidate, intent) => {
739
- assertReclaimLiveness(coordRoot, candidate);
781
+ assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
740
782
  assertReclaimOwnership(coordRoot, candidate);
741
783
  return cleanupUnderLease(coordRoot, candidate, claim, intent, {
742
784
  mode: options.mode,
743
- beforeEffect: () => reclaimAuthorityGuard(coordRoot, candidate, claim, reservation),
785
+ recovery: options.unknownLiveness,
786
+ beforeEffect: () => {
787
+ audit?.assertCurrent();
788
+ reclaimAuthorityGuard(coordRoot, candidate, claim, reservation, options.unknownLiveness);
789
+ },
790
+ beforeRefEffect: () => {
791
+ audit?.assertCurrent();
792
+ reservation.assertCurrent();
793
+ assertReclaimLiveness(coordRoot, candidate, options.unknownLiveness);
794
+ requiredClaim(coordRoot, candidate);
795
+ assertPeerWorkspaceAuthority(coordRoot, claim);
796
+ },
744
797
  prepared: (value) => {
745
798
  preparation = value;
746
799
  },
@@ -754,6 +807,8 @@ async function reclaim(coordRoot, provider, binding, options) {
754
807
  throw new WorkflowReclaimError("workflow_reclaim_ownership_unproven", ("reason" in cleanupResult ? cleanupResult.reason : undefined) ??
755
808
  "workspace cleanup preconditions could not be proved");
756
809
  }
810
+ if (cleanupResult.status === "released" || cleanupResult.status === "already_released")
811
+ audit?.complete();
757
812
  return {
758
813
  ...preview,
759
814
  action: "reclaimed",
@@ -891,7 +946,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
891
946
  };
892
947
  }
893
948
  const liveness = readWorkflowLiveness(coordRoot, binding.run_id);
894
- if (liveness !== "inactive") {
949
+ if (liveness !== "inactive" && !(liveness === "unknown" && reclaimRequest?.recovery)) {
895
950
  return {
896
951
  ...base,
897
952
  status: "blocked",
@@ -962,7 +1017,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
962
1017
  };
963
1018
  }
964
1019
  if (reclaimRequest) {
965
- assertReclaimLiveness(coordRoot, binding);
1020
+ assertReclaimLiveness(coordRoot, binding, reclaimRequest.recovery);
966
1021
  assertReclaimOwnership(coordRoot, binding);
967
1022
  revalidateClaimResources(claim, root);
968
1023
  reclaimRequest.prepared(prepareReclaim({
@@ -1002,6 +1057,7 @@ function cleanupUnderLease(coordRoot, binding, claim, intent, reclaimRequest, be
1002
1057
  return { ...base, status: "blocked", reason: "workspace ref changed during cleanup" };
1003
1058
  }
1004
1059
  revalidateClaimResources(claim, root);
1060
+ reclaimRequest?.beforeRefEffect();
1005
1061
  const deleted = gitMaybe(repository.source_root.realpath, [
1006
1062
  "update-ref",
1007
1063
  "-d",
@@ -14,10 +14,11 @@
14
14
  * always has. So reclaim adds no second removal path that could diverge from the
15
15
  * audited one, and a force-delete of live work exists nowhere in the codebase.
16
16
  */
17
+ import type { WorkflowRecoveryAuthorization } from "../recovery-audit.js";
17
18
  import type { WorkflowLiveness } from "./recovery.js";
18
19
  import type { WorkspaceCleanupAttempt, WorkspaceCleanupReceipt } from "./types.js";
19
20
  export type ReclaimMode = "salvage" | "discard";
20
- export type WorkflowReclaimRefusalCode = "workflow_reclaim_run_live" | "workflow_reclaim_liveness_unknown" | "workflow_reclaim_ownership_unproven" | "workflow_reclaim_lease_held";
21
+ export type WorkflowReclaimRefusalCode = "workflow_reclaim_run_live" | "workflow_reclaim_liveness_unknown" | "workflow_reclaim_recovery_mismatch" | "workflow_reclaim_ownership_unproven" | "workflow_reclaim_lease_held";
21
22
  export declare class WorkflowReclaimError extends Error {
22
23
  readonly code: WorkflowReclaimRefusalCode;
23
24
  constructor(code: WorkflowReclaimRefusalCode, message: string);
@@ -25,6 +26,9 @@ export declare class WorkflowReclaimError extends Error {
25
26
  export interface WorkspaceReclaimOptions {
26
27
  mode: ReclaimMode;
27
28
  yes?: boolean;
29
+ unknownLiveness?: WorkflowRecoveryAuthorization & {
30
+ evidenceSha256: string;
31
+ };
28
32
  }
29
33
  export interface ReclaimLeaseHolder {
30
34
  scope: "binding" | "repository" | "resume";
@@ -38,6 +42,7 @@ export interface WorkspaceReclaimPreview {
38
42
  run_id: string;
39
43
  mode: ReclaimMode;
40
44
  liveness: WorkflowLiveness;
45
+ evidence_sha256: string;
41
46
  ownership: "ok" | "unproven";
42
47
  ownership_detail?: string;
43
48
  dirty_paths: string[] | null;
@@ -1 +1 @@
1
- {"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhD,MAAM,MAAM,0BAA0B,GAClC,2BAA2B,GAC3B,mCAAmC,GACnC,qCAAqC,GACrC,6BAA6B,CAAC;AAElC,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,IAAI,EAAE,0BAA0B;gBAAhC,IAAI,EAAE,0BAA0B,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,IAAI,GAAG,UAAU,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,kBAAkB,EAAE,CAAC;CACpC;AAED,MAAM,MAAM,sBAAsB,GAC9B,uBAAuB,GACvB,CAAC,IAAI,CAAC,uBAAuB,EAAE,QAAQ,CAAC,GAAG;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,kBAAkB,CAAC;IAChC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAAC;CAC5D,CAAC,CAAC;AAEP,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,kBAAkB,CAwD7E;AAYD;qEACqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
1
+ {"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhD,MAAM,MAAM,0BAA0B,GAClC,2BAA2B,GAC3B,mCAAmC,GACnC,oCAAoC,GACpC,qCAAqC,GACrC,6BAA6B,CAAC;AAElC,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,IAAI,EAAE,0BAA0B;gBAAhC,IAAI,EAAE,0BAA0B,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,6BAA6B,GAAG;QAAE,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,IAAI,GAAG,UAAU,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,kBAAkB,EAAE,CAAC;CACpC;AAED,MAAM,MAAM,sBAAsB,GAC9B,uBAAuB,GACvB,CAAC,IAAI,CAAC,uBAAuB,EAAE,QAAQ,CAAC,GAAG;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,EAAE,kBAAkB,CAAC;IAChC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAAC;CAC5D,CAAC,CAAC;AAEP,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,kBAAkB,CAwD7E;AAYD;qEACqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD"}
@@ -1,3 +1,5 @@
1
1
  export type WorkflowLiveness = "active" | "inactive" | "unknown";
2
+ /** Corruption must not hide a readable active marker from an audited override. */
3
+ export declare function hasActiveWorkflowEvidence(coordRoot: string, runId: string): boolean;
2
4
  export declare function readWorkflowLiveness(coordRoot: string, runId: string): WorkflowLiveness;
3
5
  //# sourceMappingURL=recovery.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/recovery.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CA6DvF"}
1
+ {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/recovery.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEjE,kFAAkF;AAClF,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBnF;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CA6DvF"}
@@ -1,6 +1,30 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { readWorkflowProof } from "../proof.js";
4
+ /** Corruption must not hide a readable active marker from an audited override. */
5
+ export function hasActiveWorkflowEvidence(coordRoot, runId) {
6
+ const path = join(resolve(coordRoot), ".harnery", "workflows", runId, "transcript.jsonl");
7
+ if (!existsSync(path))
8
+ return false;
9
+ let active = false;
10
+ for (const line of readFileSync(path, "utf8").split("\n")) {
11
+ try {
12
+ const event = JSON.parse(line);
13
+ if (event?.run_id !== runId)
14
+ continue;
15
+ if (["run.start", "run.resume", "agent.start"].includes(event.event))
16
+ active = true;
17
+ if (event.event === "run.parked" ||
18
+ (event.event === "run.end" && typeof event.ok === "boolean") ||
19
+ (event.event === "workspace.reattach.failed" && ["blocked", "lost"].includes(event.status)))
20
+ active = false;
21
+ }
22
+ catch {
23
+ /* Unreadable evidence cannot establish inactivity. */
24
+ }
25
+ }
26
+ return active;
27
+ }
4
28
  export function readWorkflowLiveness(coordRoot, runId) {
5
29
  const root = resolve(coordRoot);
6
30
  const transcriptPath = join(root, ".harnery", "workflows", runId, "transcript.jsonl");
@@ -32,10 +32,12 @@ export interface TunnelState {
32
32
  provider: TunnelProvider;
33
33
  url: string;
34
34
  gate_pid: number;
35
+ gate_start_token?: string;
36
+ cloudflared_start_token?: string;
37
+ registration_id?: string;
38
+ provider_state?: "pending" | "ready";
35
39
  /** Present for Cloudflare quick tunnels; absent for Tailscale Serve/Funnel. */
36
40
  cloudflared_pid?: number;
37
- /** Optional provider-side process when a provider owns one. */
38
- provider_pid?: number;
39
41
  started_at: string;
40
42
  target: string;
41
43
  vhost: string;
@@ -57,9 +59,14 @@ export declare function clearState(name?: string, root?: string): void;
57
59
  */
58
60
  export declare function listStates(root?: string): TunnelState[];
59
61
  export declare function isProcessAlive(pid: number): boolean;
60
- export type ProcessAliveCheck = (pid: number) => boolean;
61
- /** True when both the local gate and its provider-side process are live. */
62
- export declare function isTunnelStateLive(state: TunnelState, processAlive?: ProcessAliveCheck): boolean;
62
+ export type TunnelReadiness = "ready" | "dead" | "unknown";
63
+ export type TunnelProcessObservation = "same" | "absent" | "different" | "unknown";
64
+ export interface TunnelReadinessVerifier {
65
+ observeProcess(pid: number, originalToken: string): TunnelProcessObservation;
66
+ withRegistration(state: TunnelState, inspect: () => TunnelReadiness): TunnelReadiness;
67
+ }
68
+ /** Classify saved original witnesses under the verifier's registration custody. */
69
+ export declare function tunnelReadiness(state: TunnelState, verifier: TunnelReadinessVerifier): TunnelReadiness;
63
70
  export declare function tunnelTargetPort(state: TunnelState): number | null;
64
71
  /**
65
72
  * Newest live tunnel that serves `vhost` on the requested local web port.
@@ -72,7 +79,7 @@ export declare function tunnelTargetPort(state: TunnelState): number | null;
72
79
  * link 400s. Matching the vhost too keeps the returned tunnel the one that
73
80
  * actually serves the caller's origin.
74
81
  */
75
- export declare function findLiveTunnelForOrigin(port: number, vhost: string, root?: string, processAlive?: ProcessAliveCheck): TunnelState | null;
82
+ export declare function findLiveTunnelForOrigin(port: number, vhost: string, root: string, verifier: TunnelReadinessVerifier): TunnelState | null;
76
83
  /** True when the tunnel's path scope forwards every one of `paths`. */
77
84
  export declare function tunnelServesPaths(state: TunnelState, paths: readonly string[]): boolean;
78
85
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/state.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/state.ts"],"names":[],"mappings":"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"}