@cueai/omni-reader-mcp 1.5.4 → 1.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.
@@ -4,8 +4,8 @@ import { OmniBridgeError } from "./errors.js";
4
4
  import { LEGACY_RECOVERY_FAILURE_CODE } from "./operation-journal.js";
5
5
  import { openAllowedFile, } from "./path-security.js";
6
6
  import { NOOP_PROGRESS } from "./progress.js";
7
- import { normalizeRepresentation } from "./protocol.js";
8
- import { localResultToResultField } from "./result-contract.js";
7
+ import { normalizeRepresentation, } from "./protocol.js";
8
+ import { LOCAL_ARTIFACT_RETENTION_WARNING, createFlatLocalResultCache, localResultToResultField, presentRetainedResult, } from "./result-contract.js";
9
9
  const TERMINAL_STATES = new Set([
10
10
  "COMPLETED",
11
11
  "FAILED",
@@ -217,30 +217,52 @@ function resultFromRecord(record) {
217
217
  export class OperationManager {
218
218
  #journal;
219
219
  #driver;
220
+ #presentResult;
220
221
  #now;
221
222
  #sleep;
222
223
  #submissions = new Map();
224
+ #submissionDelivery = new Map();
223
225
  #executions = new Map();
224
226
  #results = new Map();
225
227
  constructor(options) {
226
228
  this.#journal = options.journal;
227
229
  this.#driver = options.driver;
230
+ this.#presentResult = options.presentResult ?? (async (_record, result) => result);
228
231
  this.#now = options.now ?? Date.now;
229
232
  this.#sleep = options.sleep ?? defaultSleep;
230
233
  }
234
+ async #strengthenResultDelivery(record, requested) {
235
+ if (requested === "auto" || record.resultDeliveryEffective === "artifact") {
236
+ return record;
237
+ }
238
+ if (record.requestIdentityHmac === null
239
+ && record.errorCode === LEGACY_RECOVERY_FAILURE_CODE) {
240
+ return record;
241
+ }
242
+ return this.#journal.strengthenResultDelivery(record.clientRequestId, requested);
243
+ }
231
244
  async submit(input) {
232
245
  const requestHash = operationRequestHash(input);
246
+ const requestedDelivery = input.resultDelivery ?? "auto";
247
+ if (requestedDelivery === "artifact"
248
+ || this.#submissionDelivery.get(requestHash) === undefined) {
249
+ this.#submissionDelivery.set(requestHash, requestedDelivery);
250
+ }
233
251
  const active = this.#submissions.get(requestHash);
234
- if (active !== undefined)
235
- return active;
252
+ if (active !== undefined) {
253
+ const record = await active;
254
+ return this.#strengthenResultDelivery(record, requestedDelivery);
255
+ }
236
256
  const pending = this.#submit(input);
237
257
  this.#submissions.set(requestHash, pending);
238
258
  try {
239
- return await pending;
259
+ const record = await pending;
260
+ return await this.#strengthenResultDelivery(record, this.#submissionDelivery.get(requestHash) ?? requestedDelivery);
240
261
  }
241
262
  finally {
242
263
  if (this.#submissions.get(requestHash) === pending) {
243
264
  this.#submissions.delete(requestHash);
265
+ this.#submissionDelivery.delete(requestHash);
244
266
  }
245
267
  }
246
268
  }
@@ -282,7 +304,7 @@ export class OperationManager {
282
304
  effectiveInput = { ...input, clientRequestId: matching.clientRequestId };
283
305
  }
284
306
  else {
285
- record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation);
307
+ record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation, input.resultDelivery ?? "auto");
286
308
  }
287
309
  }
288
310
  else if (record.requestIdentityHmac === null) {
@@ -291,18 +313,26 @@ export class OperationManager {
291
313
  // recovery failure. It can never be resumed as grounded/layout.
292
314
  const migrated = await this.#journal.migrateLegacyRecord(input.clientRequestId, identityJson, sourceLocator);
293
315
  record = migrated === null
294
- ? await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation)
316
+ ? await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation, input.resultDelivery ?? "auto")
295
317
  : migrated;
296
318
  if (TERMINAL_STATES.has(record.state))
297
319
  return record;
298
320
  }
299
321
  else {
300
- record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation);
322
+ record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation, input.resultDelivery ?? "auto");
301
323
  }
324
+ record = await this.#strengthenResultDelivery(record, input.resultDelivery ?? "auto");
302
325
  const signal = effectiveInput.signal ?? new AbortController().signal;
303
326
  if (TERMINAL_STATES.has(record.state))
304
327
  return record;
305
328
  if (record.state !== "CREATED") {
329
+ if (record.sourceKind === "url"
330
+ && record.operationId !== null) {
331
+ // The remote server already owns this operation. A repeated parse call
332
+ // would create or attach to a second remote submission; callers must
333
+ // reuse the journaled operation and continue through status instead.
334
+ return record;
335
+ }
306
336
  if (record.operationId !== null && this.#executions.has(record.operationId)) {
307
337
  return record;
308
338
  }
@@ -334,6 +364,14 @@ export class OperationManager {
334
364
  throw structured;
335
365
  }
336
366
  }
367
+ async #present(record, canonicalResult) {
368
+ if (canonicalResult.status === "processing")
369
+ return canonicalResult;
370
+ const latest = record.operationId === null
371
+ ? await this.#journal.loadByRequestId(record.clientRequestId)
372
+ : await this.#journal.loadByOperationId(record.operationId);
373
+ return this.#presentResult(latest ?? record, canonicalResult);
374
+ }
337
375
  async submitResult(input, foregroundBudgetMs = FOREGROUND_BUDGET_MS) {
338
376
  const startedAt = this.#now();
339
377
  let record = await this.submit(input);
@@ -341,19 +379,20 @@ export class OperationManager {
341
379
  ? undefined
342
380
  : this.#results.get(record.operationId);
343
381
  if (existingResult !== undefined)
344
- return existingResult;
382
+ return this.#present(record, existingResult);
345
383
  const execution = record.operationId === null
346
384
  ? undefined
347
385
  : this.#executions.get(record.operationId);
348
386
  if (execution === undefined) {
349
- return TERMINAL_STATES.has(record.state) && record.operationId !== null
350
- ? this.statusResult(record.operationId, 0, input.signal)
351
- : resultFromRecord(record);
387
+ if (TERMINAL_STATES.has(record.state) && record.operationId !== null) {
388
+ return this.statusResult(record.operationId, 0, input.signal);
389
+ }
390
+ return this.#present(record, resultFromRecord(record));
352
391
  }
353
392
  const remaining = Math.max(0, foregroundBudgetMs - (this.#now() - startedAt));
354
393
  if (remaining === 0) {
355
394
  record = await this.#applyUpdate(record, execution.handle.snapshot());
356
- return resultFromRecord(record);
395
+ return this.#present(record, resultFromRecord(record));
357
396
  }
358
397
  const winner = await Promise.race([
359
398
  execution.settled.then((result) => ({ kind: "completed", result })),
@@ -361,76 +400,80 @@ export class OperationManager {
361
400
  ]);
362
401
  if (winner.kind === "completed") {
363
402
  return this.#now() - startedAt < foregroundBudgetMs
364
- ? winner.result
365
- : resultFromRecord(record);
403
+ ? this.#present(record, winner.result)
404
+ : this.#present(record, resultFromRecord(record));
366
405
  }
367
406
  const current = await this.#journal.loadByOperationId(record.operationId);
368
407
  if (current !== null && !TERMINAL_STATES.has(current.state)) {
369
408
  record = await this.#applyUpdate(current, execution.handle.snapshot());
370
409
  }
371
- else if (current !== null) {
372
- record = current;
373
- }
374
- return resultFromRecord(record);
410
+ return this.#present(record, resultFromRecord(record));
375
411
  }
376
412
  async statusResult(operationId, waitMs = 0, signal = new AbortController().signal) {
377
413
  const boundedWait = Math.min(STATUS_LONG_POLL_MAX_MS, Math.max(0, waitMs));
378
414
  let record = await this.#existingOperation(operationId);
379
415
  const cached = this.#results.get(operationId);
380
- if (cached !== undefined && TERMINAL_STATES.has(record.state))
381
- return cached;
416
+ if (cached !== undefined && TERMINAL_STATES.has(record.state)) {
417
+ return this.#present(record, cached);
418
+ }
382
419
  if (TERMINAL_STATES.has(record.state)) {
383
420
  const recovered = await this.#driver.result?.(record, signal);
384
421
  if (recovered !== undefined) {
385
422
  this.#results.set(operationId, recovered);
386
- return recovered;
423
+ return this.#present(record, recovered);
387
424
  }
388
- return resultFromRecord(record);
425
+ return this.#present(record, resultFromRecord(record));
389
426
  }
390
427
  if (record.resultExpiresAt !== null &&
391
428
  Date.parse(record.resultExpiresAt) <= this.#now()) {
392
429
  record = await this.#journal.transition(record.clientRequestId, record.state, "EXPIRED", { errorCode: "RESULT_EXPIRED" });
393
- return resultFromRecord(record);
430
+ return this.#present(record, resultFromRecord(record));
394
431
  }
395
432
  let execution = this.#executions.get(operationId);
396
433
  if (execution === undefined) {
397
434
  record = await this.status(operationId, boundedWait, signal);
398
435
  const statusResult = this.#results.get(operationId);
399
436
  if (statusResult !== undefined && TERMINAL_STATES.has(record.state)) {
400
- return statusResult;
437
+ return this.#present(record, statusResult);
401
438
  }
402
439
  execution = this.#executions.get(operationId);
403
- if (execution === undefined)
404
- return statusResult ?? resultFromRecord(record);
440
+ if (execution === undefined) {
441
+ return this.#present(record, statusResult ?? resultFromRecord(record));
442
+ }
405
443
  }
406
444
  if (!TERMINAL_STATES.has(record.state)) {
407
445
  record = await this.#applyUpdate(record, execution.handle.snapshot());
408
446
  }
409
- if (boundedWait === 0)
410
- return this.#results.get(operationId) ?? resultFromRecord(record);
447
+ if (boundedWait === 0) {
448
+ return this.#present(record, this.#results.get(operationId) ?? resultFromRecord(record));
449
+ }
411
450
  const winner = await Promise.race([
412
451
  execution.settled.then((value) => ({ kind: "completed", value })),
413
452
  this.#sleep(boundedWait).then(() => ({ kind: "timeout" })),
414
453
  ]);
415
454
  if (winner.kind === "completed")
416
- return winner.value;
455
+ return this.#present(record, winner.value);
417
456
  const current = await this.#existingOperation(operationId);
418
457
  record = TERMINAL_STATES.has(current.state)
419
458
  ? current
420
459
  : await this.#applyUpdate(current, execution.handle.snapshot());
421
- return this.#results.get(operationId) ?? resultFromRecord(record);
460
+ return this.#present(record, this.#results.get(operationId) ?? resultFromRecord(record));
422
461
  }
423
462
  async cancelResult(operationId, signal = new AbortController().signal) {
463
+ let record = await this.#existingOperation(operationId);
424
464
  const existing = this.#results.get(operationId);
425
- if (existing !== undefined && existing.status !== "processing")
426
- return existing;
465
+ if (existing !== undefined && existing.status !== "processing") {
466
+ return this.#present(record, existing);
467
+ }
427
468
  const execution = this.#executions.get(operationId);
428
469
  if (execution !== undefined) {
429
470
  await execution.handle.cancel();
430
- return execution.settled;
471
+ const settled = await execution.settled;
472
+ record = await this.#existingOperation(operationId);
473
+ return this.#present(record, settled);
431
474
  }
432
- const record = await this.cancel(operationId, signal);
433
- return this.#results.get(operationId) ?? resultFromRecord(record);
475
+ record = await this.cancel(operationId, signal);
476
+ return this.#present(record, this.#results.get(operationId) ?? resultFromRecord(record));
434
477
  }
435
478
  async status(operationId, waitMs, signal = new AbortController().signal) {
436
479
  const record = await this.#existingOperation(operationId);
@@ -537,18 +580,10 @@ export class OperationManager {
537
580
  }
538
581
  #shouldReuseByRequestHash(record) {
539
582
  if (!TERMINAL_STATES.has(record.state)) {
540
- // Local uploads may resume mid-flight (restart recovery). URL recovery
541
- // is only valid BEFORE the remote operation exists (GRANT_PENDING — the
542
- // retry reuses the same request identity and the server's idempotency
543
- // key dedupes). Once a URL op is PROCESSING (operationId set), the
544
- // server owns it and a fresh parse creates a NEW operation id — reusing
545
- // the stale record pairs old op_id with the server's new one →
546
- // "changed the operation identifier" → REMOTE_PROTOCOL_ERROR
547
- // (dogfood 2026-08-06). Reuse URL non-terminal records only when no
548
- // operation id has been issued yet.
549
- if (record.sourceKind === "url") {
550
- return record.operationId === null;
551
- }
583
+ // Local uploads may resume mid-flight. URL request identities must also
584
+ // reuse the existing record: before an operation id exists, the retry
585
+ // keeps the same server idempotency key; after it exists, #submit returns
586
+ // the record without calling parse again and callers continue by status.
552
587
  return true;
553
588
  }
554
589
  if (record.state !== "COMPLETED" || record.operationId === null)
@@ -663,18 +698,34 @@ export async function hydrateInlineUrlResult(result, artifactStore) {
663
698
  ...start,
664
699
  resultDigest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
665
700
  });
666
- return { ...result, result: localResultToResultField(retention.result()) };
701
+ const local = retention.result();
702
+ return {
703
+ ...result,
704
+ result: localResultToResultField(local),
705
+ local_result_cache: createFlatLocalResultCache({
706
+ resultId: local.resultId,
707
+ resultBytes: local.resultBytes,
708
+ expiresAt: local.expiresAt,
709
+ }),
710
+ };
667
711
  }
668
712
  // hydrateInlineUrlResult is a pure local-storage optimization on top of content the caller
669
713
  // already has in full -- a local-disk failure here (e.g. disk full, permission error) must never
670
- // turn an otherwise-successful URL parse into a failure. This wrapper swallows any hydration
671
- // error and falls back to the original, un-hydrated result: the caller still gets their content;
672
- // they just lose local read_result/read_outline re-read capability for that particular result.
714
+ // turn an otherwise-successful URL parse into a failure. Preserve the delivered inline content
715
+ // and expose one exact warning instead of silently advertising unavailable local actions.
673
716
  export async function hydrateInlineUrlResultSafely(result, artifactStore) {
674
717
  try {
675
718
  return await hydrateInlineUrlResult(result, artifactStore);
676
719
  }
677
720
  catch {
721
+ if ((result.status === "completed" || result.status === "cleanup_pending")
722
+ && result.result?.kind === "inline") {
723
+ const { local_result_cache: _unavailableCache, ...delivered } = result;
724
+ return {
725
+ ...delivered,
726
+ delivery_warning: LOCAL_ARTIFACT_RETENTION_WARNING,
727
+ };
728
+ }
678
729
  return result;
679
730
  }
680
731
  }
@@ -723,6 +774,13 @@ function remoteFailure(result) {
723
774
  ...(error.constraints === undefined ? {} : { constraints: error.constraints }),
724
775
  });
725
776
  }
777
+ function retainedFlatResultId(result) {
778
+ if (result.status !== "completed" && result.status !== "cleanup_pending") {
779
+ return undefined;
780
+ }
781
+ const cache = result.local_result_cache;
782
+ return cache !== undefined && "result_id" in cache ? cache.result_id : undefined;
783
+ }
726
784
  function remoteResultUpdate(result, expectedOperationId) {
727
785
  if (result.status === "failed")
728
786
  throw remoteFailure(result);
@@ -770,6 +828,9 @@ function remoteResultUpdate(result, expectedOperationId) {
770
828
  processingCopy: "deleted",
771
829
  temporaryData: "deleted",
772
830
  deliveryResult: "deleted_after_ack",
831
+ ...(retainedFlatResultId(result) === undefined
832
+ ? {}
833
+ : { resultId: retainedFlatResultId(result) }),
773
834
  },
774
835
  result,
775
836
  };
@@ -793,6 +854,9 @@ function remoteResultUpdate(result, expectedOperationId) {
793
854
  temporaryData: result.data_handling.temporary_data,
794
855
  deliveryResult: "pending",
795
856
  resultExpiresAt: result.cleanup_deadline,
857
+ ...(retainedFlatResultId(result) === undefined
858
+ ? {}
859
+ : { resultId: retainedFlatResultId(result) }),
796
860
  },
797
861
  result,
798
862
  };
@@ -890,6 +954,19 @@ function localResultValue(local) {
890
954
  next_cursor: local.nextCursor,
891
955
  };
892
956
  }
957
+ function localResultCache(local) {
958
+ if (local.kind === "bundle") {
959
+ return {
960
+ expires_at: local.expiresAt,
961
+ discard_action: "discard_result",
962
+ };
963
+ }
964
+ return createFlatLocalResultCache({
965
+ resultId: local.resultId,
966
+ resultBytes: local.resultBytes,
967
+ expiresAt: local.expiresAt,
968
+ });
969
+ }
893
970
  function completedLocalParse(local) {
894
971
  return {
895
972
  status: "completed",
@@ -902,12 +979,7 @@ function completedLocalParse(local) {
902
979
  original_source: "unchanged",
903
980
  remote_content_retained: false,
904
981
  },
905
- ...(local.kind === "artifact" || local.kind === "bundle" ? {
906
- local_result_cache: {
907
- expires_at: local.expiresAt,
908
- discard_action: "discard_result",
909
- },
910
- } : {}),
982
+ local_result_cache: localResultCache(local),
911
983
  };
912
984
  }
913
985
  function cleanupPendingParse(local, cleanupDeadline) {
@@ -922,6 +994,39 @@ function cleanupPendingParse(local, cleanupDeadline) {
922
994
  delivery_result: "pending",
923
995
  original_source: "unchanged",
924
996
  },
997
+ local_result_cache: localResultCache(local),
998
+ };
999
+ }
1000
+ function withRecoveredRemoteResult(result, local) {
1001
+ if (result.status !== "completed" && result.status !== "cleanup_pending") {
1002
+ return result;
1003
+ }
1004
+ return {
1005
+ ...result,
1006
+ result: localResultValue(local),
1007
+ local_result_cache: localResultCache(local),
1008
+ };
1009
+ }
1010
+ function recoveredRemoteParse(record, local) {
1011
+ if (record.operationId === null) {
1012
+ throw managerError("OPERATION_ID_UNAVAILABLE", "The remote parse operation identifier is unavailable.");
1013
+ }
1014
+ if (record.state === "CLEANUP_PENDING") {
1015
+ return {
1016
+ status: "cleanup_pending",
1017
+ operation_id: record.operationId,
1018
+ result: localResultValue(local),
1019
+ cleanup_deadline: record.resultExpiresAt ?? record.expiresAt ?? new Date(0).toISOString(),
1020
+ data_handling: recordDataHandling(record),
1021
+ local_result_cache: localResultCache(local),
1022
+ };
1023
+ }
1024
+ return {
1025
+ status: "completed",
1026
+ operation_id: record.operationId,
1027
+ result: localResultValue(local),
1028
+ data_handling: recordDataHandling(record),
1029
+ local_result_cache: localResultCache(local),
925
1030
  };
926
1031
  }
927
1032
  function canceledParse(operationId, cleanupDeadline) {
@@ -1033,6 +1138,30 @@ export function createLocalParseOperationManager(options) {
1033
1138
  }
1034
1139
  throw managerError("JOURNAL_STATE_CONFLICT", "The operation changed repeatedly while saving a delivery checkpoint.", { operationCreated: true, retryable: true });
1035
1140
  }
1141
+ async function recoverArtifactAccess(record, action) {
1142
+ try {
1143
+ return await action();
1144
+ }
1145
+ catch (error) {
1146
+ const local = localFailure(error);
1147
+ throw new OmniBridgeError({
1148
+ code: local.code,
1149
+ message: local.message,
1150
+ failureScope: local.failureScope,
1151
+ sourceKind: local.sourceKind,
1152
+ userAction: local.userAction,
1153
+ requestId: local.requestId,
1154
+ operationCreated: local.operationCreated || record.operationId !== null,
1155
+ fileUploaded: local.fileUploaded || record.fileUploaded,
1156
+ parserStarted: local.parserStarted || record.parserStarted,
1157
+ billed: local.billed || record.billed,
1158
+ contentReleased: local.contentReleased || record.contentReleased,
1159
+ retryable: local.retryable,
1160
+ retryAfter: local.retryAfter,
1161
+ constraints: local.constraints,
1162
+ });
1163
+ }
1164
+ }
1036
1165
  // A retained logical bundle is reconstructed through its closed descriptor
1037
1166
  // and keeps the stored representation tuple; without the descriptor the
1038
1167
  // recovery fails safe instead of degrading the bundle to text.
@@ -1056,7 +1185,7 @@ export function createLocalParseOperationManager(options) {
1056
1185
  contentReleased: record.contentReleased,
1057
1186
  });
1058
1187
  }
1059
- const descriptor = await options.artifactStore.readBundleDescriptor(record.resultId);
1188
+ const descriptor = await recoverArtifactAccess(record, () => options.artifactStore.readBundleDescriptor(record.resultId));
1060
1189
  if (descriptor === null || descriptor.kind !== "bundle") {
1061
1190
  throw managerError("RESULT_NOT_AVAILABLE", "The retained local result bundle is unavailable.", {
1062
1191
  operationCreated: true,
@@ -1068,11 +1197,11 @@ export function createLocalParseOperationManager(options) {
1068
1197
  }
1069
1198
  return descriptor;
1070
1199
  }
1071
- const preview = await options.artifactStore.read(record.resultId, undefined, 2_048);
1200
+ const preview = await recoverArtifactAccess(record, () => options.artifactStore.read(record.resultId, undefined, 2_048));
1072
1201
  if (preview.resultBytes <= INLINE_RESULT_MAX_BYTES) {
1073
1202
  const recovered = preview.nextCursor === undefined
1074
1203
  ? preview
1075
- : await options.artifactStore.read(record.resultId, undefined, INLINE_RESULT_MAX_BYTES);
1204
+ : await recoverArtifactAccess(record, () => options.artifactStore.read(record.resultId, undefined, INLINE_RESULT_MAX_BYTES));
1076
1205
  if (recovered.nextCursor !== undefined) {
1077
1206
  throw managerError("LOCAL_RESULT_INTEGRITY_FAILED", "The retained inline result was not fully readable.", {
1078
1207
  operationCreated: true,
@@ -1101,6 +1230,16 @@ export function createLocalParseOperationManager(options) {
1101
1230
  ...(preview.nextCursor === undefined ? {} : { nextCursor: preview.nextCursor }),
1102
1231
  };
1103
1232
  }
1233
+ async function preflightArtifactDelivery(input, recovery) {
1234
+ const delivery = recovery?.resultDeliveryEffective ?? input.resultDelivery ?? "auto";
1235
+ if (delivery !== "artifact")
1236
+ return;
1237
+ if (options.artifactStore.preflightWrite === undefined
1238
+ || options.artifactStore.mintReadCursor === undefined) {
1239
+ throw managerError("ARTIFACT_PREFLIGHT_FAILED", "The local result store cannot provide artifact delivery.");
1240
+ }
1241
+ await options.artifactStore.preflightWrite();
1242
+ }
1104
1243
  async function startExecution(input, recovery) {
1105
1244
  // Recovery keeps the stored representation tuple exactly: status, download
1106
1245
  // (recoverLocalArtifact), and ACK all operate on the representation the
@@ -1136,6 +1275,7 @@ export function createLocalParseOperationManager(options) {
1136
1275
  throw managerError("SOURCE_CHANGED_DURING_SUBMISSION", "The local source changed before the upload operation was created.", { retryable: true });
1137
1276
  }
1138
1277
  try {
1278
+ await preflightArtifactDelivery(input, recovery);
1139
1279
  // D2-D Task 14: the normalized detail reaches the grant boundary so
1140
1280
  // CubeGrantClient selects the exact direct profile BEFORE constructing
1141
1281
  // the v3 request and requires the v3 response tuple before any upload.
@@ -1492,6 +1632,7 @@ export function createLocalParseOperationManager(options) {
1492
1632
  assertSameRepresentation(input, recovery);
1493
1633
  }
1494
1634
  const context = remoteContext(input.context);
1635
+ await preflightArtifactDelivery(input, recovery);
1495
1636
  const result = await options.remoteClient.parse(context.source, input.clientRequestId, input.signal ?? new AbortController().signal, representation.detail);
1496
1637
  const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
1497
1638
  return remoteResultUpdate(hydrated, recovery?.operationId ?? undefined);
@@ -1507,9 +1648,30 @@ export function createLocalParseOperationManager(options) {
1507
1648
  if (record.operationId === null)
1508
1649
  return undefined;
1509
1650
  if (record.sourceKind === "url") {
1510
- if (options.remoteClient === undefined)
1511
- return undefined;
1651
+ if (options.remoteClient === undefined) {
1652
+ if (record.resultId === null || record.state !== "CLEANUP_PENDING") {
1653
+ return undefined;
1654
+ }
1655
+ return {
1656
+ state: record.state,
1657
+ result: recoveredRemoteParse(record, await recoverLocalArtifact(record)),
1658
+ };
1659
+ }
1512
1660
  const result = await options.remoteClient.status(record.operationId, waitMs, signal);
1661
+ if (record.resultId !== null && record.state === "CLEANUP_PENDING") {
1662
+ let local;
1663
+ try {
1664
+ local = await recoverLocalArtifact(record);
1665
+ }
1666
+ catch {
1667
+ // A missing or corrupt retained copy must not block authoritative
1668
+ // remote status recovery. Rehydrate the returned body when it is
1669
+ // available, preserving the existing safe warning behavior.
1670
+ }
1671
+ if (local !== undefined) {
1672
+ return remoteResultUpdate(withRecoveredRemoteResult(result, local), record.operationId);
1673
+ }
1674
+ }
1513
1675
  const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
1514
1676
  return remoteResultUpdate(hydrated, record.operationId);
1515
1677
  }
@@ -1613,6 +1775,15 @@ export function createLocalParseOperationManager(options) {
1613
1775
  if (record.sourceKind === "local" && record.resultId !== null) {
1614
1776
  return completedLocalParse(await recoverLocalArtifact(record));
1615
1777
  }
1778
+ if (record.sourceKind === "url" && record.resultId !== null) {
1779
+ try {
1780
+ return recoveredRemoteParse(record, await recoverLocalArtifact(record));
1781
+ }
1782
+ catch (error) {
1783
+ if (options.remoteClient === undefined)
1784
+ throw error;
1785
+ }
1786
+ }
1616
1787
  if (record.sourceKind === "url" && options.remoteClient !== undefined) {
1617
1788
  const result = await options.remoteClient.status(record.operationId, 0, signal);
1618
1789
  const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
@@ -1624,6 +1795,18 @@ export function createLocalParseOperationManager(options) {
1624
1795
  return new OperationManager({
1625
1796
  journal: options.journal,
1626
1797
  driver,
1798
+ presentResult: (record, canonicalResult) => presentRetainedResult(canonicalResult, record.resultDeliveryEffective, async (resultId, byteOffset) => {
1799
+ if (options.artifactStore.mintReadCursor === undefined) {
1800
+ throw managerError("LOCAL_RESULT_INTEGRITY_FAILED", "The retained result cursor capability is unavailable.", {
1801
+ operationCreated: record.operationId !== null,
1802
+ fileUploaded: record.fileUploaded,
1803
+ parserStarted: record.parserStarted,
1804
+ billed: record.billed,
1805
+ contentReleased: record.contentReleased,
1806
+ });
1807
+ }
1808
+ return options.artifactStore.mintReadCursor(resultId, byteOffset);
1809
+ }),
1627
1810
  now: () => now().getTime(),
1628
1811
  sleep: options.sleep,
1629
1812
  });
@@ -0,0 +1,5 @@
1
+ export declare class PathNormalizationError extends Error {
2
+ readonly code = "INVALID_WINDOWS_PATH";
3
+ constructor();
4
+ }
5
+ export declare function normalizePlatformPath(value: string, platform?: NodeJS.Platform): string;
@@ -0,0 +1,25 @@
1
+ import path from "node:path";
2
+ const MSYS_DRIVE_PATH = /^\/([A-Za-z])(?:\/|$)/u;
3
+ export class PathNormalizationError extends Error {
4
+ code = "INVALID_WINDOWS_PATH";
5
+ constructor() {
6
+ super("Use an absolute Windows drive path such as C:\\Reports or exact MSYS syntax such as /c/Reports.");
7
+ this.name = "PathNormalizationError";
8
+ }
9
+ }
10
+ export function normalizePlatformPath(value, platform = process.platform) {
11
+ if (platform !== "win32")
12
+ return value;
13
+ if (value.startsWith("//") || value.startsWith("\\\\"))
14
+ return value;
15
+ const match = MSYS_DRIVE_PATH.exec(value);
16
+ if (match !== null) {
17
+ const drive = match[1].toUpperCase();
18
+ const remainder = value.slice(match[0].length).replaceAll("/", "\\");
19
+ return path.win32.normalize(`${drive}:\\${remainder}`);
20
+ }
21
+ if (value.startsWith("/") || value.startsWith("\\")) {
22
+ throw new PathNormalizationError();
23
+ }
24
+ return value;
25
+ }
@@ -9,6 +9,7 @@ export interface OpenAllowedFileOptions {
9
9
  workspace: string;
10
10
  extraRoots?: readonly string[];
11
11
  homeDirectory?: string;
12
+ platform?: NodeJS.Platform;
12
13
  fileSystem?: PathSecurityFileSystem;
13
14
  }
14
15
  export interface OpenedAllowedFile {
@@ -19,5 +20,5 @@ export interface OpenedAllowedFile {
19
20
  createReadStream(): ReadStream;
20
21
  close(): Promise<void>;
21
22
  }
22
- export declare function splitAllowedRoots(value: string | undefined): string[];
23
+ export declare function splitAllowedRoots(value: string | undefined, platform?: NodeJS.Platform): string[];
23
24
  export declare function openAllowedFile(input: string, options: OpenAllowedFileOptions): Promise<OpenedAllowedFile>;