@tea-agent/loop-agent 0.23.1 → 0.24.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 (53) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +1 -1
  3. package/bin/agent-worker.js +0 -0
  4. package/dist/executors/shell-executor.js +20 -7
  5. package/dist/shared/operator/capabilities.js +475 -2
  6. package/dist/worker/console/app-data.js +2 -0
  7. package/dist/worker/console/chat/artifact-card.js +23 -0
  8. package/dist/worker/console/chat/chat-event-store.js +495 -0
  9. package/dist/worker/console/chat/chat-ui-policy.js +25 -0
  10. package/dist/worker/console/chat/composer-draft-store.js +45 -0
  11. package/dist/worker/console/chat/context-panel.js +54 -0
  12. package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
  13. package/dist/worker/console/chat/explore-tools.js +299 -0
  14. package/dist/worker/console/chat/human-gate-card.js +37 -0
  15. package/dist/worker/console/chat/interview-adapter.js +136 -0
  16. package/dist/worker/console/chat/operation-card.js +23 -0
  17. package/dist/worker/console/chat/pi-console-config.js +158 -0
  18. package/dist/worker/console/chat/pi-runtime.js +581 -43
  19. package/dist/worker/console/chat/repo-browser.js +140 -0
  20. package/dist/worker/console/chat/repo-walk.js +116 -0
  21. package/dist/worker/console/chat/resource-loader.js +18 -17
  22. package/dist/worker/console/chat/routes.js +1354 -65
  23. package/dist/worker/console/chat/runtime-context.js +24 -0
  24. package/dist/worker/console/chat/runtime-selection.js +37 -0
  25. package/dist/worker/console/chat/session-store.js +210 -11
  26. package/dist/worker/console/chat/shortcuts.js +15 -0
  27. package/dist/worker/console/chat/tool-adapter.js +81 -194
  28. package/dist/worker/console/chat/tools.js +72 -48
  29. package/dist/worker/console/chat/usage.js +37 -0
  30. package/dist/worker/console/chat/workspace-landing.js +56 -0
  31. package/dist/worker/console/dag-confirmation.js +42 -8
  32. package/dist/worker/console/human-gate-token.js +130 -0
  33. package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
  34. package/dist/worker/console/operation-runner.js +6 -2
  35. package/dist/worker/console/operation-sse.js +26 -0
  36. package/dist/worker/console/operator-actions.js +420 -7
  37. package/dist/worker/console/server.js +14 -2
  38. package/dist/worker/console/static/assets/index-BTbrEHnO.css +1 -0
  39. package/dist/worker/console/static/assets/index-D9qLevoP.js +27 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -5
  42. package/dist/workflows/dag/backend-test-result-contract.js +229 -0
  43. package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
  44. package/dist/workflows/dag/init-hybrid.js +2 -1
  45. package/docs/README.md +1 -1
  46. package/docs/architecture/README.md +5 -5
  47. package/docs/architecture/evolution.md +4 -4
  48. package/docs/architecture/worker-and-feature.md +1 -1
  49. package/docs/templates/backend-test-dag.json +2 -2
  50. package/harness.json +1 -1
  51. package/package.json +1 -1
  52. package/dist/worker/console/static/assets/index-DVl7Jxt5.js +0 -25
  53. package/dist/worker/console/static/assets/index-lVcIr9Ju.css +0 -1
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import { readJsonIfExists, sha256Utf8, writeSecureJson, } from "./app-data.js";
4
+ import { confirmationPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "./human-gate-token.js";
4
5
  export const ALL_CONFIRMATION_CHALLENGES = [
5
6
  "source",
6
7
  "writer-scope",
@@ -88,8 +89,15 @@ function detectBindingDrift(rec, current) {
88
89
  }
89
90
  export class DagConfirmationStore {
90
91
  appData;
91
- constructor(appData) {
92
+ humanGateSecret;
93
+ constructor(appData,
94
+ /**
95
+ * M0-B: per-process HMAC secret for human-gate tokens
96
+ * (BootCapabilityToken.confirmationToken). Required for prepare/confirm.
97
+ */
98
+ humanGateSecret) {
92
99
  this.appData = appData;
100
+ this.humanGateSecret = humanGateSecret;
93
101
  }
94
102
  async get(confirmationId) {
95
103
  const rec = await readJsonIfExists(confirmationPath(this.appData, confirmationId));
@@ -114,15 +122,28 @@ export class DagConfirmationStore {
114
122
  const now = input.now ?? new Date();
115
123
  const ttl = input.ttlMs ?? DEFAULT_CONFIRMATION_TTL_MS;
116
124
  const confirmationId = newConfirmationId();
125
+ const dagSha256 = hashDagBytes(input.dagBytes);
126
+ const expiresAt = new Date(now.getTime() + ttl).toISOString();
127
+ // M0-B: issue a server-signed human-gate token bound to the confirmation's
128
+ // payloadHash. The browser must present it at confirm; dispatch verifies
129
+ // the signature so a model cannot self-confirm.
130
+ const payloadHash = confirmationPayloadHash({
131
+ dagSha256,
132
+ sourceBindingSha256: input.sourceBindingSha256,
133
+ taskContractBindingCanonical: JSON.stringify(input.taskContractBinding),
134
+ controllerFingerprint: input.controllerFingerprint,
135
+ validationResultSha256: input.validationResultSha256,
136
+ });
137
+ const humanGateToken = issueHumanGateToken({ confirmationId, payloadHash, expiresAt }, this.humanGateSecret, now);
117
138
  const rec = {
118
139
  schemaVersion: 1,
119
140
  confirmationId,
120
141
  state: "prepared",
121
142
  preparedAt: now.toISOString(),
122
- expiresAt: new Date(now.getTime() + ttl).toISOString(),
143
+ expiresAt,
123
144
  operatorSessionId: input.operatorSessionId,
124
145
  dagHandle: input.dagHandle,
125
- dagSha256: hashDagBytes(input.dagBytes),
146
+ dagSha256,
126
147
  dagBytesPath: input.dagBytesPath,
127
148
  sourceBindingSha256: input.sourceBindingSha256,
128
149
  taskContractBinding: input.taskContractBinding,
@@ -130,6 +151,7 @@ export class DagConfirmationStore {
130
151
  validationResultSha256: input.validationResultSha256,
131
152
  confirmedWriterScopes: input.confirmedWriterScopes ?? [],
132
153
  confirmedChallenges: [],
154
+ humanGateToken,
133
155
  reviewPacket: input.reviewPacket,
134
156
  };
135
157
  await writeSecureJson(confirmationPath(this.appData, confirmationId), rec);
@@ -195,14 +217,25 @@ export class DagConfirmationStore {
195
217
  confirmation: stale,
196
218
  };
197
219
  }
198
- const required = new Set(ALL_CONFIRMATION_CHALLENGES);
199
- for (const c of input.challenges)
200
- required.delete(c);
201
- if (required.size > 0) {
220
+ // M0-B / roadmap G03: verify the human-gate token's HMAC signature. The
221
+ // old fixed-string challenge set (`source`/`writer-scope`/...) was a fake
222
+ // gate — a model could fill those strings directly. Now the browser must
223
+ // present the server-signed token issued at prepare; the signature binds
224
+ // (confirmationId + payloadHash + expiresAt) and is verified with the
225
+ // per-process boot secret. A model-side call has no way to forge it.
226
+ const payloadHash = confirmationPayloadHash({
227
+ dagSha256: current.dagSha256,
228
+ sourceBindingSha256: current.sourceBindingSha256,
229
+ taskContractBindingCanonical: JSON.stringify(current.taskContractBinding),
230
+ controllerFingerprint: current.controllerFingerprint,
231
+ validationResultSha256: current.validationResultSha256,
232
+ });
233
+ const tokenCheck = verifyHumanGateToken(input.humanGateToken, { confirmationId: input.confirmationId, payloadHash }, this.humanGateSecret, input.now);
234
+ if (!tokenCheck.ok) {
202
235
  return {
203
236
  ok: false,
204
237
  code: "HUMAN_CONFIRMATION_REQUIRED",
205
- message: `missing confirmation challenges: ${[...required].join(", ")}`,
238
+ message: `human-gate token verification failed: ${tokenCheck.code} — ${tokenCheck.message}`,
206
239
  confirmation: rec,
207
240
  };
208
241
  }
@@ -212,6 +245,7 @@ export class DagConfirmationStore {
212
245
  state: "confirmed",
213
246
  confirmedAt: now.toISOString(),
214
247
  confirmedChallenges: [...ALL_CONFIRMATION_CHALLENGES],
248
+ humanGateToken: input.humanGateToken ?? rec.humanGateToken,
215
249
  };
216
250
  await writeSecureJson(confirmationPath(this.appData, rec.confirmationId), confirmed);
217
251
  return { ok: true, confirmation: confirmed };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Operator Chat — Human Gate confirmation token (roadmap M0-B / G03).
3
+ *
4
+ * Closes the "fake human gate" gap (roadmap §3.2 risk 1): the old
5
+ * confirmDagConfirmation only checked that a FIXED set of challenge strings
6
+ * (`source`/`writer-scope`/`verification`/`human-gates`) was present — a model
7
+ * could fill those in directly and self-confirm. This module issues a
8
+ * server-signed token bound to (confirmationId, payloadHash, expiresAt) and
9
+ * verifies its HMAC signature at dispatch time, so confirmation can only
10
+ * originate from the browser mutation gate that received the signed token.
11
+ *
12
+ * The HMAC key is the per-process boot confirmation secret
13
+ * (BootCapabilityToken.confirmationToken). This binds the human-gate token to
14
+ * the same Console boot session that the browser established via the HttpOnly
15
+ * cookie — a model-side tool call cannot fabricate it.
16
+ */
17
+ import { createHmac, timingSafeEqual } from "node:crypto";
18
+ /** Canonical message string covered by the signature. */
19
+ export function canonicalTokenMessage(p) {
20
+ return [
21
+ `v1`,
22
+ `confirmationId=${p.confirmationId}`,
23
+ `payloadHash=${p.payloadHash}`,
24
+ `expiresAt=${p.expiresAt}`,
25
+ ].join("\n");
26
+ }
27
+ function b64url(buf) {
28
+ return buf.toString("base64url");
29
+ }
30
+ /** Issue a signed token. `secret` = BootCapabilityToken.confirmationToken. */
31
+ export function issueHumanGateToken(payload, secret, now = new Date()) {
32
+ const sig = createHmac("sha256", secret).update(canonicalTokenMessage(payload)).digest();
33
+ return {
34
+ ...payload,
35
+ signature: b64url(sig),
36
+ issuedAt: now.toISOString(),
37
+ };
38
+ }
39
+ /**
40
+ * Verify a presented token. Checks signature, expiry, and that the token is
41
+ * bound to the given confirmationId + current payloadHash. Constant-time
42
+ * signature compare. Returns a typed failure (no thrown secrets).
43
+ */
44
+ export function verifyHumanGateToken(presented, expected, secret, now = new Date()) {
45
+ if (!presented || typeof presented !== "object") {
46
+ return { ok: false, code: "MISSING_TOKEN", message: "no human-gate token presented" };
47
+ }
48
+ if (presented.confirmationId !== expected.confirmationId) {
49
+ return {
50
+ ok: false,
51
+ code: "CONFIRMATION_MISMATCH",
52
+ message: "token is bound to a different confirmationId",
53
+ };
54
+ }
55
+ if (presented.payloadHash !== expected.payloadHash) {
56
+ return {
57
+ ok: false,
58
+ code: "PAYLOAD_MISMATCH",
59
+ message: "token payloadHash does not match the current confirmation bindings",
60
+ };
61
+ }
62
+ const expectedSig = createHmac("sha256", secret)
63
+ .update(canonicalTokenMessage(presented))
64
+ .digest();
65
+ let presentedSig;
66
+ try {
67
+ presentedSig = Buffer.from(presented.signature, "base64url");
68
+ }
69
+ catch {
70
+ return { ok: false, code: "INVALID_SIGNATURE", message: "signature is not valid base64url" };
71
+ }
72
+ if (presentedSig.length !== expectedSig.length ||
73
+ !timingSafeEqual(presentedSig, expectedSig)) {
74
+ return { ok: false, code: "INVALID_SIGNATURE", message: "signature verification failed" };
75
+ }
76
+ if (Date.parse(presented.expiresAt) <= now.getTime()) {
77
+ return { ok: false, code: "EXPIRED", message: "token has expired" };
78
+ }
79
+ return { ok: true, token: presented };
80
+ }
81
+ /**
82
+ * Compute the payloadHash for a confirmation receipt: SHA-256 over the binding
83
+ * snapshot (dagSha256 + sourceBindingSha256 + taskContractBinding canonical +
84
+ * controllerFingerprint + validationResultSha256). Changing any binding
85
+ * invalidates the token (defense against drift-then-confirm).
86
+ */
87
+ import { createHash } from "node:crypto";
88
+ import { sha256Utf8 } from "./app-data.js";
89
+ export function confirmationPayloadHash(b) {
90
+ const canonical = [
91
+ `dag=${b.dagSha256}`,
92
+ `source=${b.sourceBindingSha256}`,
93
+ `contract=${sha256Utf8(b.taskContractBindingCanonical)}`,
94
+ `controller=${b.controllerFingerprint}`,
95
+ `validation=${b.validationResultSha256}`,
96
+ ].join("\n");
97
+ return createHash("sha256").update(canonical).digest("hex");
98
+ }
99
+ /** Stable contract-apply binding; revision and observed hash prevent drift-then-confirm. */
100
+ export function contractApplyPayloadHash(b) {
101
+ const canonical = [
102
+ "v1-contract-apply",
103
+ `taskId=${b.taskId}`,
104
+ `revision=${b.revision}`,
105
+ `canonicalHash=${b.canonicalHash}`,
106
+ `draftSha256=${b.draftSha256}`,
107
+ `expectedObservedHash=${b.expectedObservedHash}`,
108
+ ].join("\n");
109
+ return createHash("sha256").update(canonical).digest("hex");
110
+ }
111
+ /** Stable mutation-gate binding; params are canonical-JSON with sorted keys. */
112
+ export function mutationGatePayloadHash(b) {
113
+ const paramsCanonical = stableJson(b.actionParams);
114
+ const canonical = [
115
+ "v1-mutation-gate",
116
+ `action=${b.action}`,
117
+ `operatorSessionId=${b.operatorSessionId}`,
118
+ `actionParams=${paramsCanonical}`,
119
+ ].join("\n");
120
+ return createHash("sha256").update(canonical).digest("hex");
121
+ }
122
+ function stableJson(value) {
123
+ if (value === null || typeof value !== "object")
124
+ return JSON.stringify(value);
125
+ if (Array.isArray(value))
126
+ return `[${value.map((entry) => stableJson(entry)).join(",")}]`;
127
+ const obj = value;
128
+ const keys = Object.keys(obj).sort();
129
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(obj[key])}`).join(",")}}`;
130
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Lightweight Human Gate receipts for mutation actions that are not DAG-bound
3
+ * (standaloneTaskRerun / workerTaskRetry). Mirrors contract-apply receipt
4
+ * semantics with an atomic prepared → dispatching → dispatched state machine
5
+ * so concurrent confirm POSTs cannot create duplicate operations.
6
+ */
7
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
8
+ import path from "node:path";
9
+ export class MutationGateReceiptStore {
10
+ dir;
11
+ constructor(appData) {
12
+ this.dir = path.join(appData.root, "mutation-gate-receipts");
13
+ }
14
+ file(id) {
15
+ return path.join(this.dir, `${id.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
16
+ }
17
+ async prepare(input) {
18
+ const receipt = {
19
+ schemaVersion: 1,
20
+ state: "prepared",
21
+ ...input,
22
+ };
23
+ await mkdir(this.dir, { recursive: true, mode: 0o700 });
24
+ await writeFile(this.file(receipt.receiptId), `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
25
+ return receipt;
26
+ }
27
+ async get(id) {
28
+ try {
29
+ return JSON.parse(await readFile(this.file(id), "utf8"));
30
+ }
31
+ catch (error) {
32
+ if (error.code === "ENOENT")
33
+ return undefined;
34
+ throw error;
35
+ }
36
+ }
37
+ /**
38
+ * Atomically claim a prepared receipt for dispatch.
39
+ * Concurrent callers: only one gets reserved=true; others see
40
+ * already-dispatching / already-dispatched with the same receipt.
41
+ */
42
+ async tryReserve(id) {
43
+ const receipt = await this.get(id);
44
+ if (!receipt) {
45
+ return {
46
+ ok: false,
47
+ code: "NOT_FOUND",
48
+ message: `mutation gate receipt not found: ${id}`,
49
+ };
50
+ }
51
+ if (Date.parse(receipt.expiresAt) <= Date.now()) {
52
+ return {
53
+ ok: false,
54
+ code: "EXPIRED",
55
+ message: "mutation gate receipt expired",
56
+ receipt,
57
+ };
58
+ }
59
+ if (receipt.state === "dispatching") {
60
+ return {
61
+ ok: true,
62
+ receipt,
63
+ reserved: false,
64
+ reason: "already-dispatching",
65
+ };
66
+ }
67
+ if (receipt.state === "dispatched") {
68
+ return {
69
+ ok: true,
70
+ receipt,
71
+ reserved: false,
72
+ reason: "already-dispatched",
73
+ };
74
+ }
75
+ if (receipt.state !== "prepared") {
76
+ return {
77
+ ok: false,
78
+ code: "INVALID_STATE",
79
+ message: `mutation gate receipt is ${receipt.state}`,
80
+ receipt,
81
+ };
82
+ }
83
+ const next = {
84
+ ...receipt,
85
+ state: "dispatching",
86
+ reservedAt: new Date().toISOString(),
87
+ };
88
+ const claimed = await this.compareAndSwap(id, "prepared", next);
89
+ if (!claimed.ok) {
90
+ const current = claimed.current;
91
+ if (current?.state === "dispatching") {
92
+ return {
93
+ ok: true,
94
+ receipt: current,
95
+ reserved: false,
96
+ reason: "already-dispatching",
97
+ };
98
+ }
99
+ if (current?.state === "dispatched") {
100
+ return {
101
+ ok: true,
102
+ receipt: current,
103
+ reserved: false,
104
+ reason: "already-dispatched",
105
+ };
106
+ }
107
+ return {
108
+ ok: false,
109
+ code: "INVALID_STATE",
110
+ message: `mutation gate receipt raced into ${current?.state ?? "missing"}`,
111
+ receipt: current,
112
+ };
113
+ }
114
+ return { ok: true, receipt: next, reserved: true };
115
+ }
116
+ async markDispatched(id, operationId) {
117
+ const receipt = await this.get(id);
118
+ if (!receipt)
119
+ throw new Error(`receipt not found: ${id}`);
120
+ const next = {
121
+ ...receipt,
122
+ state: "dispatched",
123
+ operationId,
124
+ dispatchedAt: new Date().toISOString(),
125
+ };
126
+ await writeFile(this.file(id), `${JSON.stringify(next, null, 2)}\n`, {
127
+ mode: 0o600,
128
+ });
129
+ return next;
130
+ }
131
+ /** Best-effort unlock if dispatch failed after reserve. */
132
+ async releaseToPrepared(id) {
133
+ const receipt = await this.get(id);
134
+ if (!receipt || receipt.state !== "dispatching")
135
+ return receipt;
136
+ const next = {
137
+ ...receipt,
138
+ state: "prepared",
139
+ reservedAt: undefined,
140
+ };
141
+ await writeFile(this.file(id), `${JSON.stringify(next, null, 2)}\n`, {
142
+ mode: 0o600,
143
+ });
144
+ return next;
145
+ }
146
+ async compareAndSwap(id, expectedState, next) {
147
+ await mkdir(this.dir, { recursive: true, mode: 0o700 });
148
+ const target = this.file(id);
149
+ const lockPath = `${target}.lock`;
150
+ try {
151
+ await writeFile(lockPath, `${process.pid}\n`, { flag: "wx", mode: 0o600 });
152
+ }
153
+ catch (error) {
154
+ if (error.code === "EEXIST") {
155
+ // Brief spin for the other writer; then re-read.
156
+ await new Promise((r) => setTimeout(r, 25));
157
+ return { ok: false, current: await this.get(id) };
158
+ }
159
+ throw error;
160
+ }
161
+ try {
162
+ const current = await this.get(id);
163
+ if (!current || current.state !== expectedState) {
164
+ return { ok: false, current };
165
+ }
166
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
167
+ await writeFile(tmp, `${JSON.stringify(next, null, 2)}\n`, {
168
+ mode: 0o600,
169
+ });
170
+ await rename(tmp, target);
171
+ return { ok: true };
172
+ }
173
+ finally {
174
+ try {
175
+ await writeFile(lockPath, "", { flag: "w" });
176
+ const { unlink } = await import("node:fs/promises");
177
+ await unlink(lockPath);
178
+ }
179
+ catch {
180
+ // lock cleanup is best-effort
181
+ }
182
+ }
183
+ }
184
+ }
@@ -155,15 +155,19 @@ function coerceEnvelope(result) {
155
155
  /** Fire-and-forget scheduler for accepted operations. */
156
156
  export function scheduleOperation(operationId, deps) {
157
157
  void runOperation(operationId, deps).catch((error) => {
158
+ const message = error instanceof Error ? error.message : String(error);
158
159
  void deps.store
159
160
  .update(operationId, {
160
161
  state: "needs-reconcile",
161
162
  finishedAt: new Date().toISOString(),
162
163
  errorCode: "OPERATION_NEEDS_RECONCILE",
163
- errorMessage: error instanceof Error ? error.message : String(error),
164
+ errorMessage: message,
165
+ })
166
+ .then((updated) => {
167
+ stateEvent(deps.events, updated, "needs-reconcile", message);
164
168
  })
165
169
  .catch(() => {
166
- // ignore secondary failures
170
+ // A failed canonical store update cannot be represented safely.
167
171
  });
168
172
  });
169
173
  }
@@ -13,6 +13,7 @@ export function createOperationEventStore(options) {
13
13
  if (persistenceDir)
14
14
  ensureSecureDir(persistenceDir);
15
15
  const rings = new Map();
16
+ const subscribers = new Map();
16
17
  function persistedPath(operationId) {
17
18
  return persistenceDir
18
19
  ? path.join(persistenceDir, `${safeOperationId(operationId)}.json`)
@@ -91,6 +92,17 @@ export function createOperationEventStore(options) {
91
92
  ring.minSeq = ring.events[0].seq;
92
93
  }
93
94
  persist(operationId, ring);
95
+ const listeners = subscribers.get(operationId);
96
+ if (listeners) {
97
+ for (const listener of listeners) {
98
+ try {
99
+ listener(event);
100
+ }
101
+ catch {
102
+ // Subscriber failures must never break the canonical append path.
103
+ }
104
+ }
105
+ }
94
106
  return event;
95
107
  },
96
108
  listFrom(operationId, afterSeq) {
@@ -112,8 +124,22 @@ export function createOperationEventStore(options) {
112
124
  rings.set(operationId, ring);
113
125
  return ring?.events.slice() ?? [];
114
126
  },
127
+ subscribe(operationId, listener) {
128
+ let listeners = subscribers.get(operationId);
129
+ if (!listeners) {
130
+ listeners = new Set();
131
+ subscribers.set(operationId, listeners);
132
+ }
133
+ listeners.add(listener);
134
+ return () => {
135
+ listeners?.delete(listener);
136
+ if (listeners?.size === 0)
137
+ subscribers.delete(operationId);
138
+ };
139
+ },
115
140
  clear(operationId) {
116
141
  rings.delete(operationId);
142
+ subscribers.delete(operationId);
117
143
  const file = persistedPath(operationId);
118
144
  if (file)
119
145
  rmSync(file, { force: true });