@cueai/omni-reader-mcp 1.5.5 → 1.7.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.
- package/README.md +106 -46
- package/dist/artifact-store.d.ts +2 -0
- package/dist/artifact-store.js +45 -10
- package/dist/capabilities.d.ts +129 -2
- package/dist/capabilities.js +122 -19
- package/dist/cli/agent-config.js +2 -2
- package/dist/cli/arguments.d.ts +8 -4
- package/dist/cli/arguments.js +62 -7
- package/dist/cli/config-inspection.d.ts +59 -0
- package/dist/cli/config-inspection.js +307 -0
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +37 -2
- package/dist/cli/setup.js +13 -2
- package/dist/constants.d.ts +6 -1
- package/dist/constants.js +9 -4
- package/dist/cube-client.d.ts +4 -1
- package/dist/cube-client.js +286 -32
- package/dist/cursor.d.ts +4 -0
- package/dist/cursor.js +11 -13
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +15 -0
- package/dist/iiis-client.d.ts +33 -2
- package/dist/iiis-client.js +368 -40
- package/dist/index.js +10 -1
- package/dist/operation-journal.d.ts +17 -1
- package/dist/operation-journal.js +260 -15
- package/dist/operation-manager.d.ts +6 -2
- package/dist/operation-manager.js +448 -89
- package/dist/path-normalization.d.ts +5 -0
- package/dist/path-normalization.js +25 -0
- package/dist/path-security.d.ts +2 -1
- package/dist/path-security.js +49 -28
- package/dist/protocol.d.ts +10 -2
- package/dist/protocol.js +23 -7
- package/dist/remote-client.js +3 -13
- package/dist/result-contract.d.ts +76 -32
- package/dist/result-contract.js +121 -5
- package/dist/task-runtime.d.ts +3 -2
- package/dist/task-runtime.js +2 -2
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +41 -31
- package/package.json +1 -1
|
@@ -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",
|
|
@@ -36,6 +36,78 @@ function managerError(code, message, facts = {}) {
|
|
|
36
36
|
retryable: facts.retryable ?? false,
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
+
function journalIntegrityError(message, record) {
|
|
40
|
+
return managerError("JOURNAL_INTEGRITY_FAILED", message, record === undefined
|
|
41
|
+
? {}
|
|
42
|
+
: {
|
|
43
|
+
operationCreated: record.operationId !== null,
|
|
44
|
+
fileUploaded: record.fileUploaded,
|
|
45
|
+
parserStarted: record.parserStarted,
|
|
46
|
+
billed: record.billed,
|
|
47
|
+
contentReleased: record.contentReleased,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function isBillingProfile(profile) {
|
|
51
|
+
return profile === "omni.direct_text_billing.v1"
|
|
52
|
+
|| profile === "omni.direct_grounding_billing.v1";
|
|
53
|
+
}
|
|
54
|
+
function toJournalBilling(billing) {
|
|
55
|
+
return {
|
|
56
|
+
creditsCharged: billing.credits_charged,
|
|
57
|
+
creditsRemaining: billing.credits_remaining,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function toWireBilling(billing) {
|
|
61
|
+
return {
|
|
62
|
+
credits_charged: billing.creditsCharged,
|
|
63
|
+
credits_remaining: billing.creditsRemaining,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function resultBilling(result) {
|
|
67
|
+
if (result.status === "completed"
|
|
68
|
+
|| result.status === "cleanup_pending"
|
|
69
|
+
|| result.status === "canceled"
|
|
70
|
+
|| result.status === "expired") {
|
|
71
|
+
return result.billing ?? null;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function projectBilling(result, billing) {
|
|
76
|
+
if (billing === null
|
|
77
|
+
|| (result.status !== "completed"
|
|
78
|
+
&& result.status !== "cleanup_pending"
|
|
79
|
+
&& result.status !== "canceled"
|
|
80
|
+
&& result.status !== "expired")) {
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
return { ...result, billing };
|
|
84
|
+
}
|
|
85
|
+
function requireWireBillingForSettledProfile(profile, billed, billing) {
|
|
86
|
+
if (!isBillingProfile(profile))
|
|
87
|
+
return billing ?? undefined;
|
|
88
|
+
if (!billed && billing !== null) {
|
|
89
|
+
throw journalIntegrityError("An unbilled direct terminal carries billing facts.");
|
|
90
|
+
}
|
|
91
|
+
if (billed && billing === null) {
|
|
92
|
+
throw journalIntegrityError("New-profile settlement billing is missing.");
|
|
93
|
+
}
|
|
94
|
+
return billing ?? undefined;
|
|
95
|
+
}
|
|
96
|
+
function assertDirectTerminalSnapshot(profile, snapshot) {
|
|
97
|
+
if (!isBillingProfile(profile))
|
|
98
|
+
return;
|
|
99
|
+
if ((snapshot.directProfile ?? null) !== profile) {
|
|
100
|
+
throw journalIntegrityError("IIIS changed the selected direct profile.");
|
|
101
|
+
}
|
|
102
|
+
if (snapshot.status === "CANCELED"
|
|
103
|
+
|| snapshot.status === "FAILED"
|
|
104
|
+
|| snapshot.status === "UNSUPPORTED"
|
|
105
|
+
|| snapshot.status === "SETTLEMENT_DENIED") {
|
|
106
|
+
if (snapshot.billed || snapshot.contentReleased || snapshot.billing !== null) {
|
|
107
|
+
throw journalIntegrityError("A canceled or failed direct-v5 operation is not strictly uncharged.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
39
111
|
function canonicalValue(value) {
|
|
40
112
|
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
41
113
|
return value;
|
|
@@ -162,6 +234,7 @@ function resultFromRecord(record) {
|
|
|
162
234
|
status: "expired",
|
|
163
235
|
operation_id: record.operationId,
|
|
164
236
|
requires_user_confirmation: true,
|
|
237
|
+
...(record.billing === null ? {} : { billing: toWireBilling(record.billing) }),
|
|
165
238
|
};
|
|
166
239
|
}
|
|
167
240
|
if (record.state === "CANCELED") {
|
|
@@ -170,6 +243,7 @@ function resultFromRecord(record) {
|
|
|
170
243
|
status: "canceled",
|
|
171
244
|
operation_id: record.operationId,
|
|
172
245
|
...(cleanupDeadline === null ? {} : { cleanup_deadline: cleanupDeadline }),
|
|
246
|
+
...(record.billing === null ? {} : { billing: toWireBilling(record.billing) }),
|
|
173
247
|
data_handling: recordDataHandling(record),
|
|
174
248
|
};
|
|
175
249
|
}
|
|
@@ -188,6 +262,7 @@ function resultFromRecord(record) {
|
|
|
188
262
|
return {
|
|
189
263
|
status: "cleanup_pending",
|
|
190
264
|
operation_id: record.operationId,
|
|
265
|
+
...(record.billing === null ? {} : { billing: toWireBilling(record.billing) }),
|
|
191
266
|
cleanup_deadline: record.resultExpiresAt ?? record.expiresAt ?? new Date(0).toISOString(),
|
|
192
267
|
data_handling: recordDataHandling(record),
|
|
193
268
|
};
|
|
@@ -217,30 +292,52 @@ function resultFromRecord(record) {
|
|
|
217
292
|
export class OperationManager {
|
|
218
293
|
#journal;
|
|
219
294
|
#driver;
|
|
295
|
+
#presentResult;
|
|
220
296
|
#now;
|
|
221
297
|
#sleep;
|
|
222
298
|
#submissions = new Map();
|
|
299
|
+
#submissionDelivery = new Map();
|
|
223
300
|
#executions = new Map();
|
|
224
301
|
#results = new Map();
|
|
225
302
|
constructor(options) {
|
|
226
303
|
this.#journal = options.journal;
|
|
227
304
|
this.#driver = options.driver;
|
|
305
|
+
this.#presentResult = options.presentResult ?? (async (_record, result) => result);
|
|
228
306
|
this.#now = options.now ?? Date.now;
|
|
229
307
|
this.#sleep = options.sleep ?? defaultSleep;
|
|
230
308
|
}
|
|
309
|
+
async #strengthenResultDelivery(record, requested) {
|
|
310
|
+
if (requested === "auto" || record.resultDeliveryEffective === "artifact") {
|
|
311
|
+
return record;
|
|
312
|
+
}
|
|
313
|
+
if (record.requestIdentityHmac === null
|
|
314
|
+
&& record.errorCode === LEGACY_RECOVERY_FAILURE_CODE) {
|
|
315
|
+
return record;
|
|
316
|
+
}
|
|
317
|
+
return this.#journal.strengthenResultDelivery(record.clientRequestId, requested);
|
|
318
|
+
}
|
|
231
319
|
async submit(input) {
|
|
232
320
|
const requestHash = operationRequestHash(input);
|
|
321
|
+
const requestedDelivery = input.resultDelivery ?? "auto";
|
|
322
|
+
if (requestedDelivery === "artifact"
|
|
323
|
+
|| this.#submissionDelivery.get(requestHash) === undefined) {
|
|
324
|
+
this.#submissionDelivery.set(requestHash, requestedDelivery);
|
|
325
|
+
}
|
|
233
326
|
const active = this.#submissions.get(requestHash);
|
|
234
|
-
if (active !== undefined)
|
|
235
|
-
|
|
327
|
+
if (active !== undefined) {
|
|
328
|
+
const record = await active;
|
|
329
|
+
return this.#strengthenResultDelivery(record, requestedDelivery);
|
|
330
|
+
}
|
|
236
331
|
const pending = this.#submit(input);
|
|
237
332
|
this.#submissions.set(requestHash, pending);
|
|
238
333
|
try {
|
|
239
|
-
|
|
334
|
+
const record = await pending;
|
|
335
|
+
return await this.#strengthenResultDelivery(record, this.#submissionDelivery.get(requestHash) ?? requestedDelivery);
|
|
240
336
|
}
|
|
241
337
|
finally {
|
|
242
338
|
if (this.#submissions.get(requestHash) === pending) {
|
|
243
339
|
this.#submissions.delete(requestHash);
|
|
340
|
+
this.#submissionDelivery.delete(requestHash);
|
|
244
341
|
}
|
|
245
342
|
}
|
|
246
343
|
}
|
|
@@ -282,7 +379,7 @@ export class OperationManager {
|
|
|
282
379
|
effectiveInput = { ...input, clientRequestId: matching.clientRequestId };
|
|
283
380
|
}
|
|
284
381
|
else {
|
|
285
|
-
record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation);
|
|
382
|
+
record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation, input.resultDelivery ?? "auto");
|
|
286
383
|
}
|
|
287
384
|
}
|
|
288
385
|
else if (record.requestIdentityHmac === null) {
|
|
@@ -291,18 +388,26 @@ export class OperationManager {
|
|
|
291
388
|
// recovery failure. It can never be resumed as grounded/layout.
|
|
292
389
|
const migrated = await this.#journal.migrateLegacyRecord(input.clientRequestId, identityJson, sourceLocator);
|
|
293
390
|
record = migrated === null
|
|
294
|
-
? await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation)
|
|
391
|
+
? await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation, input.resultDelivery ?? "auto")
|
|
295
392
|
: migrated;
|
|
296
393
|
if (TERMINAL_STATES.has(record.state))
|
|
297
394
|
return record;
|
|
298
395
|
}
|
|
299
396
|
else {
|
|
300
|
-
record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation);
|
|
397
|
+
record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation, input.resultDelivery ?? "auto");
|
|
301
398
|
}
|
|
399
|
+
record = await this.#strengthenResultDelivery(record, input.resultDelivery ?? "auto");
|
|
302
400
|
const signal = effectiveInput.signal ?? new AbortController().signal;
|
|
303
401
|
if (TERMINAL_STATES.has(record.state))
|
|
304
402
|
return record;
|
|
305
403
|
if (record.state !== "CREATED") {
|
|
404
|
+
if (record.sourceKind === "url"
|
|
405
|
+
&& record.operationId !== null) {
|
|
406
|
+
// The remote server already owns this operation. A repeated parse call
|
|
407
|
+
// would create or attach to a second remote submission; callers must
|
|
408
|
+
// reuse the journaled operation and continue through status instead.
|
|
409
|
+
return record;
|
|
410
|
+
}
|
|
306
411
|
if (record.operationId !== null && this.#executions.has(record.operationId)) {
|
|
307
412
|
return record;
|
|
308
413
|
}
|
|
@@ -334,6 +439,45 @@ export class OperationManager {
|
|
|
334
439
|
throw structured;
|
|
335
440
|
}
|
|
336
441
|
}
|
|
442
|
+
async #bindResultBilling(record, result) {
|
|
443
|
+
const billing = resultBilling(result);
|
|
444
|
+
if (billing === null)
|
|
445
|
+
return record;
|
|
446
|
+
const latest = await this.#journal.loadByRequestId(record.clientRequestId) ?? record;
|
|
447
|
+
const directProfile = latest.sourceKind === "url" ? null : latest.directProfile;
|
|
448
|
+
if (latest.sourceKind === "local" && directProfile === null) {
|
|
449
|
+
throw journalIntegrityError("A local terminal carries billing without a saved direct profile.", latest);
|
|
450
|
+
}
|
|
451
|
+
return this.#journal.bindSettlementFacts(latest.clientRequestId, {
|
|
452
|
+
directProfile,
|
|
453
|
+
billing: toJournalBilling(billing),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
async #normalizeResultBilling(record, result) {
|
|
457
|
+
const bound = await this.#bindResultBilling(record, result);
|
|
458
|
+
const billing = resultBilling(result)
|
|
459
|
+
?? (bound.billing === null ? null : toWireBilling(bound.billing));
|
|
460
|
+
if (isBillingProfile(bound.directProfile)) {
|
|
461
|
+
if (result.status === "failed" || result.status === "canceled") {
|
|
462
|
+
if (bound.billed || bound.billing !== null || bound.contentReleased) {
|
|
463
|
+
throw journalIntegrityError("A failed or canceled direct-v5 terminal is not strictly uncharged.", bound);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else if (result.status === "completed"
|
|
467
|
+
|| (result.status === "cleanup_pending" && result.result !== undefined)) {
|
|
468
|
+
requireWireBillingForSettledProfile(bound.directProfile, bound.billed, billing);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return projectBilling(result, billing);
|
|
472
|
+
}
|
|
473
|
+
async #present(record, canonicalResult) {
|
|
474
|
+
if (canonicalResult.status === "processing")
|
|
475
|
+
return canonicalResult;
|
|
476
|
+
const latest = record.operationId === null
|
|
477
|
+
? await this.#journal.loadByRequestId(record.clientRequestId)
|
|
478
|
+
: await this.#journal.loadByOperationId(record.operationId);
|
|
479
|
+
return this.#presentResult(latest ?? record, canonicalResult);
|
|
480
|
+
}
|
|
337
481
|
async submitResult(input, foregroundBudgetMs = FOREGROUND_BUDGET_MS) {
|
|
338
482
|
const startedAt = this.#now();
|
|
339
483
|
let record = await this.submit(input);
|
|
@@ -341,19 +485,20 @@ export class OperationManager {
|
|
|
341
485
|
? undefined
|
|
342
486
|
: this.#results.get(record.operationId);
|
|
343
487
|
if (existingResult !== undefined)
|
|
344
|
-
return existingResult;
|
|
488
|
+
return this.#present(record, existingResult);
|
|
345
489
|
const execution = record.operationId === null
|
|
346
490
|
? undefined
|
|
347
491
|
: this.#executions.get(record.operationId);
|
|
348
492
|
if (execution === undefined) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
493
|
+
if (TERMINAL_STATES.has(record.state) && record.operationId !== null) {
|
|
494
|
+
return this.statusResult(record.operationId, 0, input.signal);
|
|
495
|
+
}
|
|
496
|
+
return this.#present(record, resultFromRecord(record));
|
|
352
497
|
}
|
|
353
498
|
const remaining = Math.max(0, foregroundBudgetMs - (this.#now() - startedAt));
|
|
354
499
|
if (remaining === 0) {
|
|
355
500
|
record = await this.#applyUpdate(record, execution.handle.snapshot());
|
|
356
|
-
return resultFromRecord(record);
|
|
501
|
+
return this.#present(record, resultFromRecord(record));
|
|
357
502
|
}
|
|
358
503
|
const winner = await Promise.race([
|
|
359
504
|
execution.settled.then((result) => ({ kind: "completed", result })),
|
|
@@ -361,76 +506,81 @@ export class OperationManager {
|
|
|
361
506
|
]);
|
|
362
507
|
if (winner.kind === "completed") {
|
|
363
508
|
return this.#now() - startedAt < foregroundBudgetMs
|
|
364
|
-
? winner.result
|
|
365
|
-
: resultFromRecord(record);
|
|
509
|
+
? this.#present(record, winner.result)
|
|
510
|
+
: this.#present(record, resultFromRecord(record));
|
|
366
511
|
}
|
|
367
512
|
const current = await this.#journal.loadByOperationId(record.operationId);
|
|
368
513
|
if (current !== null && !TERMINAL_STATES.has(current.state)) {
|
|
369
514
|
record = await this.#applyUpdate(current, execution.handle.snapshot());
|
|
370
515
|
}
|
|
371
|
-
|
|
372
|
-
record = current;
|
|
373
|
-
}
|
|
374
|
-
return resultFromRecord(record);
|
|
516
|
+
return this.#present(record, resultFromRecord(record));
|
|
375
517
|
}
|
|
376
518
|
async statusResult(operationId, waitMs = 0, signal = new AbortController().signal) {
|
|
377
519
|
const boundedWait = Math.min(STATUS_LONG_POLL_MAX_MS, Math.max(0, waitMs));
|
|
378
520
|
let record = await this.#existingOperation(operationId);
|
|
379
521
|
const cached = this.#results.get(operationId);
|
|
380
|
-
if (cached !== undefined && TERMINAL_STATES.has(record.state))
|
|
381
|
-
return cached;
|
|
522
|
+
if (cached !== undefined && TERMINAL_STATES.has(record.state)) {
|
|
523
|
+
return this.#present(record, cached);
|
|
524
|
+
}
|
|
382
525
|
if (TERMINAL_STATES.has(record.state)) {
|
|
383
526
|
const recovered = await this.#driver.result?.(record, signal);
|
|
384
527
|
if (recovered !== undefined) {
|
|
385
|
-
this.#
|
|
386
|
-
|
|
528
|
+
const normalized = await this.#normalizeResultBilling(record, recovered);
|
|
529
|
+
this.#results.set(operationId, normalized);
|
|
530
|
+
return this.#present(record, normalized);
|
|
387
531
|
}
|
|
388
|
-
return resultFromRecord(record);
|
|
532
|
+
return this.#present(record, resultFromRecord(record));
|
|
389
533
|
}
|
|
390
534
|
if (record.resultExpiresAt !== null &&
|
|
391
535
|
Date.parse(record.resultExpiresAt) <= this.#now()) {
|
|
392
536
|
record = await this.#journal.transition(record.clientRequestId, record.state, "EXPIRED", { errorCode: "RESULT_EXPIRED" });
|
|
393
|
-
return resultFromRecord(record);
|
|
537
|
+
return this.#present(record, resultFromRecord(record));
|
|
394
538
|
}
|
|
395
539
|
let execution = this.#executions.get(operationId);
|
|
396
540
|
if (execution === undefined) {
|
|
397
541
|
record = await this.status(operationId, boundedWait, signal);
|
|
398
542
|
const statusResult = this.#results.get(operationId);
|
|
399
543
|
if (statusResult !== undefined && TERMINAL_STATES.has(record.state)) {
|
|
400
|
-
return statusResult;
|
|
544
|
+
return this.#present(record, statusResult);
|
|
401
545
|
}
|
|
402
546
|
execution = this.#executions.get(operationId);
|
|
403
|
-
if (execution === undefined)
|
|
404
|
-
return statusResult ?? resultFromRecord(record);
|
|
547
|
+
if (execution === undefined) {
|
|
548
|
+
return this.#present(record, statusResult ?? resultFromRecord(record));
|
|
549
|
+
}
|
|
405
550
|
}
|
|
406
551
|
if (!TERMINAL_STATES.has(record.state)) {
|
|
407
552
|
record = await this.#applyUpdate(record, execution.handle.snapshot());
|
|
408
553
|
}
|
|
409
|
-
if (boundedWait === 0)
|
|
410
|
-
return this.#results.get(operationId) ?? resultFromRecord(record);
|
|
554
|
+
if (boundedWait === 0) {
|
|
555
|
+
return this.#present(record, this.#results.get(operationId) ?? resultFromRecord(record));
|
|
556
|
+
}
|
|
411
557
|
const winner = await Promise.race([
|
|
412
558
|
execution.settled.then((value) => ({ kind: "completed", value })),
|
|
413
559
|
this.#sleep(boundedWait).then(() => ({ kind: "timeout" })),
|
|
414
560
|
]);
|
|
415
561
|
if (winner.kind === "completed")
|
|
416
|
-
return winner.value;
|
|
562
|
+
return this.#present(record, winner.value);
|
|
417
563
|
const current = await this.#existingOperation(operationId);
|
|
418
564
|
record = TERMINAL_STATES.has(current.state)
|
|
419
565
|
? current
|
|
420
566
|
: await this.#applyUpdate(current, execution.handle.snapshot());
|
|
421
|
-
return this.#results.get(operationId) ?? resultFromRecord(record);
|
|
567
|
+
return this.#present(record, this.#results.get(operationId) ?? resultFromRecord(record));
|
|
422
568
|
}
|
|
423
569
|
async cancelResult(operationId, signal = new AbortController().signal) {
|
|
570
|
+
let record = await this.#existingOperation(operationId);
|
|
424
571
|
const existing = this.#results.get(operationId);
|
|
425
|
-
if (existing !== undefined && existing.status !== "processing")
|
|
426
|
-
return existing;
|
|
572
|
+
if (existing !== undefined && existing.status !== "processing") {
|
|
573
|
+
return this.#present(record, existing);
|
|
574
|
+
}
|
|
427
575
|
const execution = this.#executions.get(operationId);
|
|
428
576
|
if (execution !== undefined) {
|
|
429
577
|
await execution.handle.cancel();
|
|
430
|
-
|
|
578
|
+
const settled = await execution.settled;
|
|
579
|
+
record = await this.#existingOperation(operationId);
|
|
580
|
+
return this.#present(record, settled);
|
|
431
581
|
}
|
|
432
|
-
|
|
433
|
-
return this.#results.get(operationId) ?? resultFromRecord(record);
|
|
582
|
+
record = await this.cancel(operationId, signal);
|
|
583
|
+
return this.#present(record, this.#results.get(operationId) ?? resultFromRecord(record));
|
|
434
584
|
}
|
|
435
585
|
async status(operationId, waitMs, signal = new AbortController().signal) {
|
|
436
586
|
const record = await this.#existingOperation(operationId);
|
|
@@ -448,7 +598,7 @@ export class OperationManager {
|
|
|
448
598
|
return record;
|
|
449
599
|
const updated = await this.#applyUpdate(record, update);
|
|
450
600
|
if (update.result !== undefined && updated.operationId !== null) {
|
|
451
|
-
this.#results.set(updated.operationId, update.result);
|
|
601
|
+
this.#results.set(updated.operationId, await this.#normalizeResultBilling(updated, update.result));
|
|
452
602
|
}
|
|
453
603
|
return updated;
|
|
454
604
|
}
|
|
@@ -462,7 +612,7 @@ export class OperationManager {
|
|
|
462
612
|
if (!isExecution(start)) {
|
|
463
613
|
const updated = await this.#applyUpdate(record, start);
|
|
464
614
|
if (start.result !== undefined && updated.operationId !== null) {
|
|
465
|
-
this.#results.set(updated.operationId, start.result);
|
|
615
|
+
this.#results.set(updated.operationId, await this.#normalizeResultBilling(updated, start.result));
|
|
466
616
|
}
|
|
467
617
|
return updated;
|
|
468
618
|
}
|
|
@@ -474,16 +624,17 @@ export class OperationManager {
|
|
|
474
624
|
let managed;
|
|
475
625
|
const settled = start.completed.then(async (completion) => {
|
|
476
626
|
const current = await this.#journal.loadByOperationId(operationId);
|
|
477
|
-
|
|
478
|
-
await this.#applyUpdate(current, completion.update)
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
627
|
+
const updated = current !== null && !TERMINAL_STATES.has(current.state)
|
|
628
|
+
? await this.#applyUpdate(current, completion.update)
|
|
629
|
+
: current ?? initial;
|
|
630
|
+
const normalized = await this.#normalizeResultBilling(updated, completion.result);
|
|
631
|
+
if (normalized.status !== "processing") {
|
|
632
|
+
this.#results.set(operationId, normalized);
|
|
482
633
|
}
|
|
483
634
|
else {
|
|
484
635
|
this.#results.delete(operationId);
|
|
485
636
|
}
|
|
486
|
-
return
|
|
637
|
+
return normalized;
|
|
487
638
|
}).catch(async (error) => {
|
|
488
639
|
const structured = error instanceof OmniBridgeError
|
|
489
640
|
? error
|
|
@@ -537,18 +688,10 @@ export class OperationManager {
|
|
|
537
688
|
}
|
|
538
689
|
#shouldReuseByRequestHash(record) {
|
|
539
690
|
if (!TERMINAL_STATES.has(record.state)) {
|
|
540
|
-
// Local uploads may resume mid-flight
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
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
|
-
}
|
|
691
|
+
// Local uploads may resume mid-flight. URL request identities must also
|
|
692
|
+
// reuse the existing record: before an operation id exists, the retry
|
|
693
|
+
// keeps the same server idempotency key; after it exists, #submit returns
|
|
694
|
+
// the record without calling parse again and callers continue by status.
|
|
552
695
|
return true;
|
|
553
696
|
}
|
|
554
697
|
if (record.state !== "COMPLETED" || record.operationId === null)
|
|
@@ -585,7 +728,9 @@ export class OperationManager {
|
|
|
585
728
|
return record;
|
|
586
729
|
}
|
|
587
730
|
async #applyUpdate(record, update) {
|
|
588
|
-
let current =
|
|
731
|
+
let current = update.result === undefined
|
|
732
|
+
? record
|
|
733
|
+
: await this.#bindResultBilling(record, update.result);
|
|
589
734
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
590
735
|
try {
|
|
591
736
|
return await this.#journal.transition(current.clientRequestId, current.state, update.state, update.patch ?? {});
|
|
@@ -663,18 +808,34 @@ export async function hydrateInlineUrlResult(result, artifactStore) {
|
|
|
663
808
|
...start,
|
|
664
809
|
resultDigest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
|
665
810
|
});
|
|
666
|
-
|
|
811
|
+
const local = retention.result();
|
|
812
|
+
return {
|
|
813
|
+
...result,
|
|
814
|
+
result: localResultToResultField(local),
|
|
815
|
+
local_result_cache: createFlatLocalResultCache({
|
|
816
|
+
resultId: local.resultId,
|
|
817
|
+
resultBytes: local.resultBytes,
|
|
818
|
+
expiresAt: local.expiresAt,
|
|
819
|
+
}),
|
|
820
|
+
};
|
|
667
821
|
}
|
|
668
822
|
// hydrateInlineUrlResult is a pure local-storage optimization on top of content the caller
|
|
669
823
|
// 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.
|
|
671
|
-
//
|
|
672
|
-
// they just lose local read_result/read_outline re-read capability for that particular result.
|
|
824
|
+
// turn an otherwise-successful URL parse into a failure. Preserve the delivered inline content
|
|
825
|
+
// and expose one exact warning instead of silently advertising unavailable local actions.
|
|
673
826
|
export async function hydrateInlineUrlResultSafely(result, artifactStore) {
|
|
674
827
|
try {
|
|
675
828
|
return await hydrateInlineUrlResult(result, artifactStore);
|
|
676
829
|
}
|
|
677
830
|
catch {
|
|
831
|
+
if ((result.status === "completed" || result.status === "cleanup_pending")
|
|
832
|
+
&& result.result?.kind === "inline") {
|
|
833
|
+
const { local_result_cache: _unavailableCache, ...delivered } = result;
|
|
834
|
+
return {
|
|
835
|
+
...delivered,
|
|
836
|
+
delivery_warning: LOCAL_ARTIFACT_RETENTION_WARNING,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
678
839
|
return result;
|
|
679
840
|
}
|
|
680
841
|
}
|
|
@@ -723,6 +884,13 @@ function remoteFailure(result) {
|
|
|
723
884
|
...(error.constraints === undefined ? {} : { constraints: error.constraints }),
|
|
724
885
|
});
|
|
725
886
|
}
|
|
887
|
+
function retainedFlatResultId(result) {
|
|
888
|
+
if (result.status !== "completed" && result.status !== "cleanup_pending") {
|
|
889
|
+
return undefined;
|
|
890
|
+
}
|
|
891
|
+
const cache = result.local_result_cache;
|
|
892
|
+
return cache !== undefined && "result_id" in cache ? cache.result_id : undefined;
|
|
893
|
+
}
|
|
726
894
|
function remoteResultUpdate(result, expectedOperationId) {
|
|
727
895
|
if (result.status === "failed")
|
|
728
896
|
throw remoteFailure(result);
|
|
@@ -770,6 +938,9 @@ function remoteResultUpdate(result, expectedOperationId) {
|
|
|
770
938
|
processingCopy: "deleted",
|
|
771
939
|
temporaryData: "deleted",
|
|
772
940
|
deliveryResult: "deleted_after_ack",
|
|
941
|
+
...(retainedFlatResultId(result) === undefined
|
|
942
|
+
? {}
|
|
943
|
+
: { resultId: retainedFlatResultId(result) }),
|
|
773
944
|
},
|
|
774
945
|
result,
|
|
775
946
|
};
|
|
@@ -793,6 +964,9 @@ function remoteResultUpdate(result, expectedOperationId) {
|
|
|
793
964
|
temporaryData: result.data_handling.temporary_data,
|
|
794
965
|
deliveryResult: "pending",
|
|
795
966
|
resultExpiresAt: result.cleanup_deadline,
|
|
967
|
+
...(retainedFlatResultId(result) === undefined
|
|
968
|
+
? {}
|
|
969
|
+
: { resultId: retainedFlatResultId(result) }),
|
|
796
970
|
},
|
|
797
971
|
result,
|
|
798
972
|
};
|
|
@@ -890,11 +1064,25 @@ function localResultValue(local) {
|
|
|
890
1064
|
next_cursor: local.nextCursor,
|
|
891
1065
|
};
|
|
892
1066
|
}
|
|
893
|
-
function
|
|
1067
|
+
function localResultCache(local) {
|
|
1068
|
+
if (local.kind === "bundle") {
|
|
1069
|
+
return {
|
|
1070
|
+
expires_at: local.expiresAt,
|
|
1071
|
+
discard_action: "discard_result",
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
return createFlatLocalResultCache({
|
|
1075
|
+
resultId: local.resultId,
|
|
1076
|
+
resultBytes: local.resultBytes,
|
|
1077
|
+
expiresAt: local.expiresAt,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
function completedLocalParse(local, billing) {
|
|
894
1081
|
return {
|
|
895
1082
|
status: "completed",
|
|
896
1083
|
operation_id: local.operationId,
|
|
897
1084
|
result: localResultValue(local),
|
|
1085
|
+
...(billing === undefined ? {} : { billing }),
|
|
898
1086
|
data_handling: {
|
|
899
1087
|
processing_copy: "deleted",
|
|
900
1088
|
temporary_data: "deleted",
|
|
@@ -902,19 +1090,15 @@ function completedLocalParse(local) {
|
|
|
902
1090
|
original_source: "unchanged",
|
|
903
1091
|
remote_content_retained: false,
|
|
904
1092
|
},
|
|
905
|
-
|
|
906
|
-
local_result_cache: {
|
|
907
|
-
expires_at: local.expiresAt,
|
|
908
|
-
discard_action: "discard_result",
|
|
909
|
-
},
|
|
910
|
-
} : {}),
|
|
1093
|
+
local_result_cache: localResultCache(local),
|
|
911
1094
|
};
|
|
912
1095
|
}
|
|
913
|
-
function cleanupPendingParse(local, cleanupDeadline) {
|
|
1096
|
+
function cleanupPendingParse(local, cleanupDeadline, billing) {
|
|
914
1097
|
return {
|
|
915
1098
|
status: "cleanup_pending",
|
|
916
1099
|
operation_id: local.operationId,
|
|
917
1100
|
result: localResultValue(local),
|
|
1101
|
+
...(billing === undefined ? {} : { billing }),
|
|
918
1102
|
cleanup_deadline: cleanupDeadline,
|
|
919
1103
|
data_handling: {
|
|
920
1104
|
processing_copy: "pending",
|
|
@@ -922,6 +1106,41 @@ function cleanupPendingParse(local, cleanupDeadline) {
|
|
|
922
1106
|
delivery_result: "pending",
|
|
923
1107
|
original_source: "unchanged",
|
|
924
1108
|
},
|
|
1109
|
+
local_result_cache: localResultCache(local),
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function withRecoveredRemoteResult(result, local) {
|
|
1113
|
+
if (result.status !== "completed" && result.status !== "cleanup_pending") {
|
|
1114
|
+
return result;
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
...result,
|
|
1118
|
+
result: localResultValue(local),
|
|
1119
|
+
local_result_cache: localResultCache(local),
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function recoveredRemoteParse(record, local) {
|
|
1123
|
+
if (record.operationId === null) {
|
|
1124
|
+
throw managerError("OPERATION_ID_UNAVAILABLE", "The remote parse operation identifier is unavailable.");
|
|
1125
|
+
}
|
|
1126
|
+
if (record.state === "CLEANUP_PENDING") {
|
|
1127
|
+
return {
|
|
1128
|
+
status: "cleanup_pending",
|
|
1129
|
+
operation_id: record.operationId,
|
|
1130
|
+
result: localResultValue(local),
|
|
1131
|
+
...(record.billing === null ? {} : { billing: toWireBilling(record.billing) }),
|
|
1132
|
+
cleanup_deadline: record.resultExpiresAt ?? record.expiresAt ?? new Date(0).toISOString(),
|
|
1133
|
+
data_handling: recordDataHandling(record),
|
|
1134
|
+
local_result_cache: localResultCache(local),
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
status: "completed",
|
|
1139
|
+
operation_id: record.operationId,
|
|
1140
|
+
result: localResultValue(local),
|
|
1141
|
+
...(record.billing === null ? {} : { billing: toWireBilling(record.billing) }),
|
|
1142
|
+
data_handling: recordDataHandling(record),
|
|
1143
|
+
local_result_cache: localResultCache(local),
|
|
925
1144
|
};
|
|
926
1145
|
}
|
|
927
1146
|
function canceledParse(operationId, cleanupDeadline) {
|
|
@@ -972,7 +1191,9 @@ function safeOperationStage(status, fallback) {
|
|
|
972
1191
|
return "canceled";
|
|
973
1192
|
if (status === "EXPIRED" || status === "DELIVERY_EXPIRED")
|
|
974
1193
|
return "expired";
|
|
975
|
-
if (status === "FAILED"
|
|
1194
|
+
if (status === "FAILED"
|
|
1195
|
+
|| status === "UNSUPPORTED"
|
|
1196
|
+
|| status === "SETTLEMENT_DENIED")
|
|
976
1197
|
return "failed";
|
|
977
1198
|
return safeJournalStage(fallback);
|
|
978
1199
|
}
|
|
@@ -1011,6 +1232,23 @@ function requireBundleResult(retention) {
|
|
|
1011
1232
|
}
|
|
1012
1233
|
return retention.bundleResult();
|
|
1013
1234
|
}
|
|
1235
|
+
async function requireExpectedBundleDetail(local, expectedDetail, retention) {
|
|
1236
|
+
if (local.kind === "bundle" && local.detail === expectedDetail)
|
|
1237
|
+
return local;
|
|
1238
|
+
try {
|
|
1239
|
+
await retention.abort();
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
// Preserve the exact-detail protocol failure over best-effort local cleanup.
|
|
1243
|
+
}
|
|
1244
|
+
throw managerError("IIIS_INVALID_RESPONSE", "IIIS returned a result for a different representation detail.", {
|
|
1245
|
+
operationCreated: true,
|
|
1246
|
+
fileUploaded: true,
|
|
1247
|
+
parserStarted: true,
|
|
1248
|
+
billed: false,
|
|
1249
|
+
contentReleased: false,
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1014
1252
|
export function createLocalParseOperationManager(options) {
|
|
1015
1253
|
const now = options.now ?? (() => new Date());
|
|
1016
1254
|
const openFile = options.openFile ?? openAllowedFile;
|
|
@@ -1033,6 +1271,30 @@ export function createLocalParseOperationManager(options) {
|
|
|
1033
1271
|
}
|
|
1034
1272
|
throw managerError("JOURNAL_STATE_CONFLICT", "The operation changed repeatedly while saving a delivery checkpoint.", { operationCreated: true, retryable: true });
|
|
1035
1273
|
}
|
|
1274
|
+
async function recoverArtifactAccess(record, action) {
|
|
1275
|
+
try {
|
|
1276
|
+
return await action();
|
|
1277
|
+
}
|
|
1278
|
+
catch (error) {
|
|
1279
|
+
const local = localFailure(error);
|
|
1280
|
+
throw new OmniBridgeError({
|
|
1281
|
+
code: local.code,
|
|
1282
|
+
message: local.message,
|
|
1283
|
+
failureScope: local.failureScope,
|
|
1284
|
+
sourceKind: local.sourceKind,
|
|
1285
|
+
userAction: local.userAction,
|
|
1286
|
+
requestId: local.requestId,
|
|
1287
|
+
operationCreated: local.operationCreated || record.operationId !== null,
|
|
1288
|
+
fileUploaded: local.fileUploaded || record.fileUploaded,
|
|
1289
|
+
parserStarted: local.parserStarted || record.parserStarted,
|
|
1290
|
+
billed: local.billed || record.billed,
|
|
1291
|
+
contentReleased: local.contentReleased || record.contentReleased,
|
|
1292
|
+
retryable: local.retryable,
|
|
1293
|
+
retryAfter: local.retryAfter,
|
|
1294
|
+
constraints: local.constraints,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1036
1298
|
// A retained logical bundle is reconstructed through its closed descriptor
|
|
1037
1299
|
// and keeps the stored representation tuple; without the descriptor the
|
|
1038
1300
|
// recovery fails safe instead of degrading the bundle to text.
|
|
@@ -1056,7 +1318,7 @@ export function createLocalParseOperationManager(options) {
|
|
|
1056
1318
|
contentReleased: record.contentReleased,
|
|
1057
1319
|
});
|
|
1058
1320
|
}
|
|
1059
|
-
const descriptor = await options.artifactStore.readBundleDescriptor(record.resultId);
|
|
1321
|
+
const descriptor = await recoverArtifactAccess(record, () => options.artifactStore.readBundleDescriptor(record.resultId));
|
|
1060
1322
|
if (descriptor === null || descriptor.kind !== "bundle") {
|
|
1061
1323
|
throw managerError("RESULT_NOT_AVAILABLE", "The retained local result bundle is unavailable.", {
|
|
1062
1324
|
operationCreated: true,
|
|
@@ -1068,11 +1330,11 @@ export function createLocalParseOperationManager(options) {
|
|
|
1068
1330
|
}
|
|
1069
1331
|
return descriptor;
|
|
1070
1332
|
}
|
|
1071
|
-
const preview = await options.artifactStore.read(record.resultId, undefined, 2_048);
|
|
1333
|
+
const preview = await recoverArtifactAccess(record, () => options.artifactStore.read(record.resultId, undefined, 2_048));
|
|
1072
1334
|
if (preview.resultBytes <= INLINE_RESULT_MAX_BYTES) {
|
|
1073
1335
|
const recovered = preview.nextCursor === undefined
|
|
1074
1336
|
? preview
|
|
1075
|
-
: await options.artifactStore.read(record.resultId, undefined, INLINE_RESULT_MAX_BYTES);
|
|
1337
|
+
: await recoverArtifactAccess(record, () => options.artifactStore.read(record.resultId, undefined, INLINE_RESULT_MAX_BYTES));
|
|
1076
1338
|
if (recovered.nextCursor !== undefined) {
|
|
1077
1339
|
throw managerError("LOCAL_RESULT_INTEGRITY_FAILED", "The retained inline result was not fully readable.", {
|
|
1078
1340
|
operationCreated: true,
|
|
@@ -1101,6 +1363,16 @@ export function createLocalParseOperationManager(options) {
|
|
|
1101
1363
|
...(preview.nextCursor === undefined ? {} : { nextCursor: preview.nextCursor }),
|
|
1102
1364
|
};
|
|
1103
1365
|
}
|
|
1366
|
+
async function preflightArtifactDelivery(input, recovery) {
|
|
1367
|
+
const delivery = recovery?.resultDeliveryEffective ?? input.resultDelivery ?? "auto";
|
|
1368
|
+
if (delivery !== "artifact")
|
|
1369
|
+
return;
|
|
1370
|
+
if (options.artifactStore.preflightWrite === undefined
|
|
1371
|
+
|| options.artifactStore.mintReadCursor === undefined) {
|
|
1372
|
+
throw managerError("ARTIFACT_PREFLIGHT_FAILED", "The local result store cannot provide artifact delivery.");
|
|
1373
|
+
}
|
|
1374
|
+
await options.artifactStore.preflightWrite();
|
|
1375
|
+
}
|
|
1104
1376
|
async function startExecution(input, recovery) {
|
|
1105
1377
|
// Recovery keeps the stored representation tuple exactly: status, download
|
|
1106
1378
|
// (recoverLocalArtifact), and ACK all operate on the representation the
|
|
@@ -1136,6 +1408,7 @@ export function createLocalParseOperationManager(options) {
|
|
|
1136
1408
|
throw managerError("SOURCE_CHANGED_DURING_SUBMISSION", "The local source changed before the upload operation was created.", { retryable: true });
|
|
1137
1409
|
}
|
|
1138
1410
|
try {
|
|
1411
|
+
await preflightArtifactDelivery(input, recovery);
|
|
1139
1412
|
// D2-D Task 14: the normalized detail reaches the grant boundary so
|
|
1140
1413
|
// CubeGrantClient selects the exact direct profile BEFORE constructing
|
|
1141
1414
|
// the v3 request and requires the v3 response tuple before any upload.
|
|
@@ -1147,6 +1420,9 @@ export function createLocalParseOperationManager(options) {
|
|
|
1147
1420
|
output: "markdown",
|
|
1148
1421
|
...(representation.detail === "text" ? {} : { detail: representation.detail }),
|
|
1149
1422
|
}, input.clientRequestId, input.signal, { journal: false });
|
|
1423
|
+
if (isBillingProfile(granted.directProfile)) {
|
|
1424
|
+
await options.journal.bindDirectProfile(input.clientRequestId, granted.directProfile);
|
|
1425
|
+
}
|
|
1150
1426
|
}
|
|
1151
1427
|
catch (error) {
|
|
1152
1428
|
await opened.close().catch(() => undefined);
|
|
@@ -1208,9 +1484,17 @@ export function createLocalParseOperationManager(options) {
|
|
|
1208
1484
|
await (context?.progress ?? NOOP_PROGRESS).report(value, total, message, latestProgress ?? undefined);
|
|
1209
1485
|
},
|
|
1210
1486
|
};
|
|
1487
|
+
const expectedDetail = granted?.requestedDetail ?? representation.detail;
|
|
1488
|
+
if (expectedDetail !== representation.detail) {
|
|
1489
|
+
await opened?.close().catch(() => undefined);
|
|
1490
|
+
opened = undefined;
|
|
1491
|
+
throw managerError("CUBE_PROTOCOL_ERROR", "Cube returned a grant for a different representation detail.", { operationCreated: true });
|
|
1492
|
+
}
|
|
1211
1493
|
const operation = {
|
|
1212
1494
|
operationId,
|
|
1213
1495
|
operationToken,
|
|
1496
|
+
directProfile: granted?.directProfile ?? recovery?.directProfile ?? null,
|
|
1497
|
+
expectedDetail,
|
|
1214
1498
|
...(granted === undefined || opened === undefined
|
|
1215
1499
|
? {}
|
|
1216
1500
|
: {
|
|
@@ -1255,18 +1539,19 @@ export function createLocalParseOperationManager(options) {
|
|
|
1255
1539
|
recovery?.state === "ACK_PENDING" ||
|
|
1256
1540
|
recovery?.state === "CLEANUP_PENDING";
|
|
1257
1541
|
let local;
|
|
1542
|
+
let released;
|
|
1258
1543
|
if (deliveryRecovery && recovery.resultId !== null) {
|
|
1259
1544
|
local = await recoverLocalArtifact(recovery);
|
|
1260
1545
|
}
|
|
1261
1546
|
else {
|
|
1262
1547
|
if (deliveryRecovery) {
|
|
1263
|
-
await options.iiisClient.downloadResult(operation, progress);
|
|
1548
|
+
released = await options.iiisClient.downloadResult(operation, progress);
|
|
1264
1549
|
}
|
|
1265
1550
|
else if (recovery?.state === "UPLOADING" || recovery?.state === "PROCESSING") {
|
|
1266
|
-
await options.iiisClient.recoverAndWait(operation);
|
|
1551
|
+
released = await options.iiisClient.recoverAndWait(operation);
|
|
1267
1552
|
}
|
|
1268
1553
|
else {
|
|
1269
|
-
await options.iiisClient.uploadAndWait(operation);
|
|
1554
|
+
released = await options.iiisClient.uploadAndWait(operation);
|
|
1270
1555
|
}
|
|
1271
1556
|
// Non-text results require the durable BundleLocalResult (both named
|
|
1272
1557
|
// parts verified and fsynced); ACK_PENDING is persisted only after
|
|
@@ -1275,6 +1560,27 @@ export function createLocalParseOperationManager(options) {
|
|
|
1275
1560
|
? requireBundleResult(retention)
|
|
1276
1561
|
: retention.result();
|
|
1277
1562
|
}
|
|
1563
|
+
if (representation.detail !== "text") {
|
|
1564
|
+
local = await requireExpectedBundleDetail(local, representation.detail, retention);
|
|
1565
|
+
}
|
|
1566
|
+
const directProfile = released?.directProfile
|
|
1567
|
+
?? recovery?.directProfile
|
|
1568
|
+
?? operation.directProfile
|
|
1569
|
+
?? null;
|
|
1570
|
+
if (released?.directProfile !== undefined
|
|
1571
|
+
&& released.directProfile !== (operation.directProfile ?? null)) {
|
|
1572
|
+
throw journalIntegrityError("IIIS changed the selected direct profile.", recovery);
|
|
1573
|
+
}
|
|
1574
|
+
const recoveredBilling = recovery?.billing === null || recovery?.billing === undefined
|
|
1575
|
+
? null
|
|
1576
|
+
: toWireBilling(recovery.billing);
|
|
1577
|
+
const wireBilling = requireWireBillingForSettledProfile(directProfile, true, released?.billing ?? recoveredBilling);
|
|
1578
|
+
if (wireBilling !== undefined) {
|
|
1579
|
+
await options.journal.bindSettlementFacts(input.clientRequestId, {
|
|
1580
|
+
directProfile,
|
|
1581
|
+
billing: toJournalBilling(wireBilling),
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1278
1584
|
const cleanupDeadline = recovery?.resultExpiresAt ?? new Date(now().getTime() + DELIVERY_TTL_SECONDS * 1000).toISOString();
|
|
1279
1585
|
const resultPatch = {
|
|
1280
1586
|
fileUploaded: true,
|
|
@@ -1325,7 +1631,7 @@ export function createLocalParseOperationManager(options) {
|
|
|
1325
1631
|
deliveryResult: "deleted_after_ack",
|
|
1326
1632
|
},
|
|
1327
1633
|
},
|
|
1328
|
-
result: completedLocalParse(local),
|
|
1634
|
+
result: completedLocalParse(local, wireBilling),
|
|
1329
1635
|
};
|
|
1330
1636
|
}
|
|
1331
1637
|
catch (error) {
|
|
@@ -1349,6 +1655,7 @@ export function createLocalParseOperationManager(options) {
|
|
|
1349
1655
|
result: processingParse(operationId, stage, latestPercent, confirmed.contentReleased === true),
|
|
1350
1656
|
};
|
|
1351
1657
|
}
|
|
1658
|
+
assertDirectTerminalSnapshot(operation.directProfile ?? null, inspected);
|
|
1352
1659
|
const patch = {
|
|
1353
1660
|
...confirmed,
|
|
1354
1661
|
fileUploaded: confirmed.fileUploaded === true || inspected.fileUploaded,
|
|
@@ -1388,7 +1695,9 @@ export function createLocalParseOperationManager(options) {
|
|
|
1388
1695
|
},
|
|
1389
1696
|
};
|
|
1390
1697
|
}
|
|
1391
|
-
if (inspected.status === "FAILED"
|
|
1698
|
+
if (inspected.status === "FAILED"
|
|
1699
|
+
|| inspected.status === "UNSUPPORTED"
|
|
1700
|
+
|| inspected.status === "SETTLEMENT_DENIED") {
|
|
1392
1701
|
const failed = managerError(inspected.status, `The Omni operation ended with status ${inspected.status}.`, {
|
|
1393
1702
|
operationCreated: true,
|
|
1394
1703
|
fileUploaded: patch.fileUploaded,
|
|
@@ -1444,12 +1753,14 @@ export function createLocalParseOperationManager(options) {
|
|
|
1444
1753
|
state: "FAILED",
|
|
1445
1754
|
patch: {
|
|
1446
1755
|
...failurePatch(stable),
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1449
|
-
//
|
|
1450
|
-
//
|
|
1451
|
-
|
|
1452
|
-
|
|
1756
|
+
// Recovery starts from persisted monotonic facts. A protocol
|
|
1757
|
+
// rejection cannot make a confirmed upload, parser start,
|
|
1758
|
+
// settlement, or release become false merely because the invalid
|
|
1759
|
+
// response itself was not trusted.
|
|
1760
|
+
fileUploaded: recovery?.fileUploaded === true || stable.fileUploaded,
|
|
1761
|
+
parserStarted: recovery?.parserStarted === true || stable.parserStarted,
|
|
1762
|
+
billed: checkpointBilled || recovery?.billed === true || stable.billed,
|
|
1763
|
+
contentReleased: recovery?.contentReleased === true || stable.contentReleased,
|
|
1453
1764
|
},
|
|
1454
1765
|
},
|
|
1455
1766
|
result: {
|
|
@@ -1492,6 +1803,7 @@ export function createLocalParseOperationManager(options) {
|
|
|
1492
1803
|
assertSameRepresentation(input, recovery);
|
|
1493
1804
|
}
|
|
1494
1805
|
const context = remoteContext(input.context);
|
|
1806
|
+
await preflightArtifactDelivery(input, recovery);
|
|
1495
1807
|
const result = await options.remoteClient.parse(context.source, input.clientRequestId, input.signal ?? new AbortController().signal, representation.detail);
|
|
1496
1808
|
const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
|
|
1497
1809
|
return remoteResultUpdate(hydrated, recovery?.operationId ?? undefined);
|
|
@@ -1507,9 +1819,30 @@ export function createLocalParseOperationManager(options) {
|
|
|
1507
1819
|
if (record.operationId === null)
|
|
1508
1820
|
return undefined;
|
|
1509
1821
|
if (record.sourceKind === "url") {
|
|
1510
|
-
if (options.remoteClient === undefined)
|
|
1511
|
-
|
|
1822
|
+
if (options.remoteClient === undefined) {
|
|
1823
|
+
if (record.resultId === null || record.state !== "CLEANUP_PENDING") {
|
|
1824
|
+
return undefined;
|
|
1825
|
+
}
|
|
1826
|
+
return {
|
|
1827
|
+
state: record.state,
|
|
1828
|
+
result: recoveredRemoteParse(record, await recoverLocalArtifact(record)),
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1512
1831
|
const result = await options.remoteClient.status(record.operationId, waitMs, signal);
|
|
1832
|
+
if (record.resultId !== null && record.state === "CLEANUP_PENDING") {
|
|
1833
|
+
let local;
|
|
1834
|
+
try {
|
|
1835
|
+
local = await recoverLocalArtifact(record);
|
|
1836
|
+
}
|
|
1837
|
+
catch {
|
|
1838
|
+
// A missing or corrupt retained copy must not block authoritative
|
|
1839
|
+
// remote status recovery. Rehydrate the returned body when it is
|
|
1840
|
+
// available, preserving the existing safe warning behavior.
|
|
1841
|
+
}
|
|
1842
|
+
if (local !== undefined) {
|
|
1843
|
+
return remoteResultUpdate(withRecoveredRemoteResult(result, local), record.operationId);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1513
1846
|
const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
|
|
1514
1847
|
return remoteResultUpdate(hydrated, record.operationId);
|
|
1515
1848
|
}
|
|
@@ -1540,6 +1873,8 @@ export function createLocalParseOperationManager(options) {
|
|
|
1540
1873
|
const operation = {
|
|
1541
1874
|
operationId: record.operationId,
|
|
1542
1875
|
operationToken: record.operationToken,
|
|
1876
|
+
directProfile: record.directProfile,
|
|
1877
|
+
expectedDetail: record.detail,
|
|
1543
1878
|
retention: {
|
|
1544
1879
|
async reset() { },
|
|
1545
1880
|
async begin() { },
|
|
@@ -1552,6 +1887,7 @@ export function createLocalParseOperationManager(options) {
|
|
|
1552
1887
|
const inspected = options.iiisClient.cancelOperation === undefined
|
|
1553
1888
|
? await options.iiisClient.inspectOperation(operation)
|
|
1554
1889
|
: await options.iiisClient.cancelOperation(operation);
|
|
1890
|
+
assertDirectTerminalSnapshot(operation.directProfile ?? null, inspected);
|
|
1555
1891
|
const patch = {
|
|
1556
1892
|
fileUploaded: inspected.fileUploaded,
|
|
1557
1893
|
parserStarted: inspected.parserStarted,
|
|
@@ -1585,7 +1921,9 @@ export function createLocalParseOperationManager(options) {
|
|
|
1585
1921
|
},
|
|
1586
1922
|
};
|
|
1587
1923
|
}
|
|
1588
|
-
if (inspected.status === "FAILED"
|
|
1924
|
+
if (inspected.status === "FAILED"
|
|
1925
|
+
|| inspected.status === "UNSUPPORTED"
|
|
1926
|
+
|| inspected.status === "SETTLEMENT_DENIED") {
|
|
1589
1927
|
return {
|
|
1590
1928
|
state: "FAILED",
|
|
1591
1929
|
patch: { ...patch, errorCode: inspected.status },
|
|
@@ -1611,7 +1949,16 @@ export function createLocalParseOperationManager(options) {
|
|
|
1611
1949
|
if (record.operationId === null || record.state !== "COMPLETED")
|
|
1612
1950
|
return undefined;
|
|
1613
1951
|
if (record.sourceKind === "local" && record.resultId !== null) {
|
|
1614
|
-
return completedLocalParse(await recoverLocalArtifact(record));
|
|
1952
|
+
return completedLocalParse(await recoverLocalArtifact(record), record.billing === null ? undefined : toWireBilling(record.billing));
|
|
1953
|
+
}
|
|
1954
|
+
if (record.sourceKind === "url" && record.resultId !== null) {
|
|
1955
|
+
try {
|
|
1956
|
+
return recoveredRemoteParse(record, await recoverLocalArtifact(record));
|
|
1957
|
+
}
|
|
1958
|
+
catch (error) {
|
|
1959
|
+
if (options.remoteClient === undefined)
|
|
1960
|
+
throw error;
|
|
1961
|
+
}
|
|
1615
1962
|
}
|
|
1616
1963
|
if (record.sourceKind === "url" && options.remoteClient !== undefined) {
|
|
1617
1964
|
const result = await options.remoteClient.status(record.operationId, 0, signal);
|
|
@@ -1624,6 +1971,18 @@ export function createLocalParseOperationManager(options) {
|
|
|
1624
1971
|
return new OperationManager({
|
|
1625
1972
|
journal: options.journal,
|
|
1626
1973
|
driver,
|
|
1974
|
+
presentResult: (record, canonicalResult) => presentRetainedResult(canonicalResult, record.resultDeliveryEffective, async (resultId, byteOffset) => {
|
|
1975
|
+
if (options.artifactStore.mintReadCursor === undefined) {
|
|
1976
|
+
throw managerError("LOCAL_RESULT_INTEGRITY_FAILED", "The retained result cursor capability is unavailable.", {
|
|
1977
|
+
operationCreated: record.operationId !== null,
|
|
1978
|
+
fileUploaded: record.fileUploaded,
|
|
1979
|
+
parserStarted: record.parserStarted,
|
|
1980
|
+
billed: record.billed,
|
|
1981
|
+
contentReleased: record.contentReleased,
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
return options.artifactStore.mintReadCursor(resultId, byteOffset);
|
|
1985
|
+
}),
|
|
1627
1986
|
now: () => now().getTime(),
|
|
1628
1987
|
sleep: options.sleep,
|
|
1629
1988
|
});
|