@tt-a1i/openpi 0.4.0 → 0.6.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 (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -22,6 +22,25 @@ export interface AcceptanceLedger {
22
22
  readonly status: "accepted" | "rejected" | "missing" | "malformed";
23
23
  readonly criteria: readonly AcceptanceCriterionResult[];
24
24
  readonly errors: readonly string[];
25
+ /** Child-authored judgment retained only for migration; never a runtime fact. */
26
+ readonly authority?: "model-self-attestation";
27
+ readonly deprecated?: {
28
+ readonly since: "0.5";
29
+ readonly removal: "1.0";
30
+ };
31
+ }
32
+
33
+ export const ACCEPTANCE_DEPRECATION_WARNING =
34
+ "acceptance is deprecated since OpenPI 0.5 and will be removed in 1.0; it is model self-attestation, not runtime-verified evidence, and does not determine ok";
35
+
36
+ function ledger(
37
+ value: Omit<AcceptanceLedger, "authority" | "deprecated">,
38
+ ): AcceptanceLedger {
39
+ return {
40
+ ...value,
41
+ authority: "model-self-attestation",
42
+ deprecated: { since: "0.5", removal: "1.0" },
43
+ };
25
44
  }
26
45
 
27
46
  const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -169,6 +188,12 @@ export function isAcceptanceLedger(value: unknown): value is AcceptanceLedger {
169
188
  value.status === "rejected" ||
170
189
  value.status === "missing" ||
171
190
  value.status === "malformed") &&
191
+ (value.authority === undefined ||
192
+ value.authority === "model-self-attestation") &&
193
+ (value.deprecated === undefined ||
194
+ (record(value.deprecated) &&
195
+ value.deprecated.since === "0.5" &&
196
+ value.deprecated.removal === "1.0")) &&
172
197
  value.errors.every((error) => typeof error === "string") &&
173
198
  value.criteria.every(
174
199
  (criterion) =>
@@ -187,7 +212,7 @@ export function acceptanceInstruction(contract: AcceptanceContract) {
187
212
  `- ${criterion.id}: ${criterion.description}${criterion.requiredEvidence?.length ? `; required evidence labels: ${criterion.requiredEvidence.join(", ")}` : ""}`,
188
213
  );
189
214
  return [
190
- "Acceptance is explicit evidence, not a self-awarded success claim.",
215
+ "Deprecated compatibility protocol: this acceptance ledger is your own model self-attestation, not runtime-verified evidence, and it does not determine execution success.",
191
216
  "Include an `acceptance.criteria` array in structured_output with exactly these ids. Mark rejected when the criterion is not demonstrated. Evidence entries must be concise labels or concrete references; do not invent evidence.",
192
217
  ...criteria,
193
218
  ].join("\n");
@@ -198,19 +223,19 @@ export function evaluateAcceptance(
198
223
  structured: unknown,
199
224
  ): AcceptanceLedger {
200
225
  if (!record(structured) || !record(structured.acceptance)) {
201
- return {
226
+ return ledger({
202
227
  status: "missing",
203
228
  criteria: [],
204
229
  errors: ["structured result omitted acceptance"],
205
- };
230
+ });
206
231
  }
207
232
  const rawCriteria = structured.acceptance.criteria;
208
233
  if (!Array.isArray(rawCriteria)) {
209
- return {
234
+ return ledger({
210
235
  status: "malformed",
211
236
  criteria: [],
212
237
  errors: ["acceptance.criteria is not an array"],
213
- };
238
+ });
214
239
  }
215
240
  const errors: string[] = [];
216
241
  const byId = new Map<string, AcceptanceCriterionResult>();
@@ -265,14 +290,15 @@ export function evaluateAcceptance(
265
290
  errors.push(`unexpected acceptance criterion "${id}"`);
266
291
  }
267
292
  }
268
- if (errors.length) return { status: "malformed", criteria: results, errors };
269
- return {
293
+ if (errors.length)
294
+ return ledger({ status: "malformed", criteria: results, errors });
295
+ return ledger({
270
296
  status: results.every((result) => result.status === "accepted")
271
297
  ? "accepted"
272
298
  : "rejected",
273
299
  criteria: results,
274
300
  errors: [],
275
- };
301
+ });
276
302
  }
277
303
 
278
304
  export function applyAcceptance(options: {
@@ -284,15 +310,13 @@ export function applyAcceptance(options: {
284
310
  const ledger = options.contract
285
311
  ? evaluateAcceptance(options.contract, options.structured)
286
312
  : undefined;
287
- const acceptanceError =
288
- ledger && ledger.status !== "accepted"
289
- ? `Acceptance ${ledger.status}${ledger.errors.length ? `: ${ledger.errors.join("; ")}` : ": one or more criteria were rejected"}`
290
- : undefined;
291
- const ok = options.agentOk && !acceptanceError;
292
- const error = ok
293
- ? undefined
294
- : options.agentError
295
- ? `${options.agentError}${acceptanceError ? `; ${acceptanceError}` : ""}`
296
- : (acceptanceError ?? "Agent failed");
297
- return { ok, ...(ledger ? { ledger } : {}), ...(error ? { error } : {}) };
313
+ const ok = options.agentOk;
314
+ const error = ok ? undefined : (options.agentError ?? "Agent failed");
315
+ return {
316
+ ok,
317
+ ...(ledger
318
+ ? { ledger, acceptanceWarning: ACCEPTANCE_DEPRECATION_WARNING }
319
+ : {}),
320
+ ...(error ? { error } : {}),
321
+ };
298
322
  }
@@ -1,32 +1,40 @@
1
- import {
2
- refreshWorkflowGraph,
3
- type TranscriptEntry,
4
- type WorkflowDetails,
5
- } from "./model.ts";
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
6
3
  import {
7
4
  boundedJournal,
8
- parseJournal,
5
+ JOURNAL_MAX_BYTES,
9
6
  type JournalEntry,
7
+ parseJournal,
10
8
  type WorkflowJournal,
9
+ type WorkflowJournalAccumulator,
11
10
  } from "./journal.ts";
12
11
  import {
12
+ refreshWorkflowGraph,
13
+ type TranscriptEntry,
14
+ type WorkflowDelivery,
15
+ type WorkflowDetails,
16
+ } from "./model.ts";
17
+ import {
18
+ encodeCompleteJson,
13
19
  safeStringify,
14
- toSerializable,
15
20
  truncateUtf8,
16
21
  writeFileAtomic,
17
22
  } from "./serialization.ts";
18
- import * as fs from "node:fs";
19
- import * as path from "node:path";
20
23
 
21
24
  export const JOURNAL_FILE = "journal.json";
22
25
 
23
26
  const ARTIFACT_TRANSCRIPT_MAX_BYTES = 32 * 1024;
24
27
  const ARTIFACT_TRANSCRIPT_ENTRY_MAX_BYTES = 8 * 1024;
28
+ const AGENT_RESULT_ARTIFACT_MAX_BYTES = 2 * 1024 * 1024;
25
29
  export const WORKFLOW_CHECKPOINT_INTERVAL_MS = 500;
26
30
  const ENTRY_TRUNCATION_MARKER = "\n[entry truncated]";
27
31
  const TRANSCRIPT_TRUNCATION_MARKER =
28
32
  "[artifact transcript truncated: older entries omitted]";
29
33
 
34
+ type WorkflowJournalSource =
35
+ | readonly JournalEntry[]
36
+ | WorkflowJournalAccumulator;
37
+
30
38
  function textBytes(text: string) {
31
39
  return Buffer.byteLength(text, "utf8");
32
40
  }
@@ -101,44 +109,61 @@ function writeRunFile(runDir: string, name: string, content: string) {
101
109
  writeFileAtomic(path.join(runDir, name), content);
102
110
  }
103
111
 
112
+ export function persistWorkflowTerminalState(
113
+ runDir: string,
114
+ details: WorkflowDetails,
115
+ ) {
116
+ const terminalManifest: WorkflowDetails = {
117
+ ...details,
118
+ agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
119
+ };
120
+ delete terminalManifest.result;
121
+ delete terminalManifest.resultArtifact;
122
+ delete terminalManifest.transcriptArtifact;
123
+ writeRunFile(
124
+ runDir,
125
+ "workflow.json",
126
+ safeStringify(terminalManifest, { maxBytes: 1024 * 1024 }),
127
+ );
128
+ }
129
+
104
130
  /** Persist one successful child result before any handoff/context projection. */
105
131
  export function persistWorkflowAgentResult(
106
132
  runDir: string,
107
133
  index: number,
108
134
  result: { output: string; structured?: unknown },
109
135
  ) {
110
- const artifact = path.join(
111
- "agent-results",
112
- `agent-${String(index).padStart(4, "0")}.json`,
113
- );
114
- writeRunFile(
115
- runDir,
116
- artifact,
117
- JSON.stringify(
118
- toSerializable(
119
- {
120
- output: result.output,
121
- ...(result.structured !== undefined
122
- ? { structured: result.structured }
123
- : {}),
124
- },
125
- {
126
- maxDepth: 32,
127
- maxNodes: 100_000,
128
- maxStringBytes: 2 * 1024 * 1024,
129
- },
130
- ),
131
- null,
132
- 2,
133
- ),
136
+ // Artifact references are persisted as portable workflow paths. The host
137
+ // filesystem join happens in writeRunFile; handoff validation and replay
138
+ // consumers intentionally use `/` regardless of the platform.
139
+ const artifact = `agent-results/agent-${String(index).padStart(4, "0")}.json`;
140
+ const encoded = encodeCompleteJson(
141
+ {
142
+ output: result.output,
143
+ ...(result.structured !== undefined
144
+ ? { structured: result.structured }
145
+ : {}),
146
+ },
147
+ {
148
+ maxBytes: AGENT_RESULT_ARTIFACT_MAX_BYTES,
149
+ maxDepth: 32,
150
+ maxNodes: 100_000,
151
+ maxStringBytes: AGENT_RESULT_ARTIFACT_MAX_BYTES,
152
+ },
134
153
  );
154
+ if (!encoded.ok) {
155
+ throw new Error(
156
+ `Agent result artifact exceeded the ${AGENT_RESULT_ARTIFACT_MAX_BYTES}-byte budget (${encoded.limit} limit at ${encoded.path})`,
157
+ );
158
+ }
159
+ writeRunFile(runDir, artifact, encoded.json);
135
160
  return artifact;
136
161
  }
137
162
 
138
163
  export function persistWorkflowJson(
139
164
  runDir: string,
140
165
  details: WorkflowDetails,
141
- journal?: readonly JournalEntry[],
166
+ journal?: WorkflowJournalSource,
142
167
  ) {
143
168
  refreshWorkflowGraph(details);
144
169
  const transcripts = Object.fromEntries(
@@ -147,6 +172,15 @@ export function persistWorkflowJson(
147
172
  boundedArtifactTranscript(agent.transcript),
148
173
  ]),
149
174
  );
175
+
176
+ // Publish terminal execution facts before dependent side artifacts. If a
177
+ // later artifact write fails, readers still see an explained terminal run
178
+ // instead of the previous `running` manifest. The final manifest below adds
179
+ // the artifact references once every dependent file has committed.
180
+ if (details.status !== "running") {
181
+ persistWorkflowTerminalState(runDir, details);
182
+ }
183
+
150
184
  writeRunFile(
151
185
  runDir,
152
186
  "transcripts.json",
@@ -154,15 +188,20 @@ export function persistWorkflowJson(
154
188
  );
155
189
  // Written alongside the rest so it inherits atomic write, 500ms coalescing,
156
190
  // and the final flush. Only present once a call has actually succeeded.
157
- // boundedJournal has already brought this under the cap, and plain
158
- // JSON.stringify is deliberate: safeStringify would swap an over-cap value
159
- // for a preview stub, silently turning the journal into something unusable.
160
- if (journal && journal.length > 0) {
161
- writeRunFile(
162
- runDir,
163
- JOURNAL_FILE,
164
- JSON.stringify(boundedJournal(journal).journal, null, 2),
165
- );
191
+ // Accumulators already enforce the cap incrementally and can assemble the
192
+ // canonical JSON from their cached entry fragments. Plain arrays retain the
193
+ // compatibility path through boundedJournal; plain JSON.stringify is
194
+ // deliberate because safeStringify would swap an over-cap value for a
195
+ // preview stub, silently turning the journal into something unusable.
196
+ if (
197
+ journal &&
198
+ (journal.length > 0 || ("toJson" in journal && journal.dropped > 0))
199
+ ) {
200
+ const content =
201
+ "toJson" in journal
202
+ ? journal.toJson()
203
+ : JSON.stringify(boundedJournal(journal).journal, null, 2);
204
+ writeRunFile(runDir, JOURNAL_FILE, content);
166
205
  }
167
206
  if (details.result !== undefined) {
168
207
  writeRunFile(
@@ -186,6 +225,25 @@ export function persistWorkflowJson(
186
225
  );
187
226
  }
188
227
 
228
+ /**
229
+ * Update only the durable delivery receipt. Completion delivery may retain a
230
+ * compact memory projection after the run has been evicted, so rewriting the
231
+ * whole details object here would risk replacing exact result artifacts with
232
+ * that projection.
233
+ */
234
+ export function persistWorkflowDeliveryState(
235
+ runDir: string,
236
+ delivery: WorkflowDelivery,
237
+ ) {
238
+ const file = path.join(runDir, "workflow.json");
239
+ const raw: unknown = JSON.parse(fs.readFileSync(file, "utf8"));
240
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
241
+ throw new Error(`Invalid persisted workflow details: ${file}`);
242
+ }
243
+ const next = { ...(raw as Record<string, unknown>), delivery };
244
+ writeFileAtomic(file, JSON.stringify(next));
245
+ }
246
+
189
247
  /** Coalesce live checkpoints while keeping final persistence synchronous. */
190
248
  export function createWorkflowPersistence(
191
249
  runDir: string,
@@ -195,10 +253,10 @@ export function createWorkflowPersistence(
195
253
  persist?: (
196
254
  runDir: string,
197
255
  details: WorkflowDetails,
198
- journal?: readonly JournalEntry[],
256
+ journal?: WorkflowJournalSource,
199
257
  ) => void;
200
- /** Read at write time so callers only have to append to their own array. */
201
- journal?: () => readonly JournalEntry[];
258
+ /** Read at write time so callers only have to append to their journal. */
259
+ journal?: () => WorkflowJournalSource;
202
260
  } = {},
203
261
  ) {
204
262
  const intervalMs = Math.max(
@@ -256,10 +314,42 @@ export function createWorkflowPersistence(
256
314
  * turn into a new way for a run to fail.
257
315
  */
258
316
  export function loadJournal(runDir: string): WorkflowJournal | undefined {
317
+ let descriptor: number | undefined;
259
318
  try {
260
- const raw = fs.readFileSync(path.join(runDir, JOURNAL_FILE), "utf8");
319
+ descriptor = fs.openSync(path.join(runDir, JOURNAL_FILE), "r");
320
+ const initialSize = fs.fstatSync(descriptor).size;
321
+ if (initialSize > JOURNAL_MAX_BYTES) return undefined;
322
+
323
+ // Read at most one byte beyond the cap. The descriptor pins the inspected
324
+ // file, while the extra byte catches growth after fstat without ever
325
+ // allocating or parsing an unbounded journal.
326
+ const bytes = Buffer.allocUnsafe(initialSize + 1);
327
+ let length = 0;
328
+ while (length < bytes.length) {
329
+ const read = fs.readSync(
330
+ descriptor,
331
+ bytes,
332
+ length,
333
+ bytes.length - length,
334
+ null,
335
+ );
336
+ if (read === 0) break;
337
+ length += read;
338
+ }
339
+ // Filling the extra byte means the file changed after inspection. Reject
340
+ // that race even when the new size would still fit the ordinary cap.
341
+ if (length === bytes.length) return undefined;
342
+ const raw = bytes.toString("utf8", 0, length);
261
343
  return parseJournal(JSON.parse(raw));
262
344
  } catch {
263
345
  return undefined;
346
+ } finally {
347
+ if (descriptor !== undefined) {
348
+ try {
349
+ fs.closeSync(descriptor);
350
+ } catch {
351
+ // Loading a replay cache is optional and always fails open to a miss.
352
+ }
353
+ }
264
354
  }
265
355
  }