codex-overleaf-link 2.6.0 → 2.6.5
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 +9 -9
- package/README.zh-CN.md +9 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +70 -0
- package/extension/src/content/composerAttachments.js +35 -8
- package/extension/src/content/contentRuntime.js +200 -51
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +129 -42
- package/extension/src/content/generated/content.bundle.js +1283 -331
- package/extension/src/content/generated/content.bundle.meta.json +3 -3
- package/extension/src/content/localSkillsPanel.js +4 -2
- package/extension/src/content/markdownText.js +11 -9
- package/extension/src/content/modelPicker.js +28 -19
- package/extension/src/content/nativeCompatibilityController.js +23 -9
- package/extension/src/content/pageBridgeClient.js +5 -5
- package/extension/src/content/panelMaintenance.js +4 -0
- package/extension/src/content/panelRenderer.js +4 -1
- package/extension/src/content/providerSettingsDialog.js +26 -18
- package/extension/src/content/recentProjects.js +26 -16
- package/extension/src/content/runQueueScheduler.js +19 -9
- package/extension/src/content/runResultActions.js +51 -4
- package/extension/src/content/runReviewController.js +9 -3
- package/extension/src/content/scopedPersistencePanelState.js +34 -25
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sharedSessionViewSync.js +9 -1
- package/extension/src/content/writebackOrchestrator.js +266 -50
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/overleafEditor.js +12 -3
- package/extension/src/page/overleafProjectSnapshot.js +56 -3
- package/extension/src/page/snapshotRouter.js +75 -66
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +152 -40
- package/extension/src/pageBridge.js +17 -7
- package/extension/src/shared/agentTranscript.js +23 -2
- package/extension/src/shared/auditRecords.js +1 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/composerAttachmentDraft.js +22 -7
- package/extension/src/shared/failureReasons.js +15 -0
- package/extension/src/shared/i18n.js +16 -0
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/projectFiles.js +2 -1
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/staleGuard.js +58 -0
- package/extension/src/shared/storageMigration.js +104 -1
- package/extension/styles/panel.css +191 -63
- package/native-host/src/codexSessionRunner.js +1 -0
- package/native-host/src/debugLog.js +27 -3
- package/native-host/src/index.js +42 -11
- package/native-host/src/localSkills.js +1 -1
- package/native-host/src/mirrorWorkspace.js +47 -3
- package/native-host/src/nativeEnvironment.js +27 -5
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
- package/native-host/src/taskRunnerRuntime.js +11 -3
- package/native-host/src/updateManager.js +24 -6
- package/native-host/src/writingStyleRuntime.js +1 -1
- package/package.json +1 -1
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
function readActiveEditorText() {
|
|
118
|
-
return deps.readActiveEditorText?.()
|
|
118
|
+
return deps.readActiveEditorText?.();
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function waitForActiveEditorText(filePath, timeoutMs, options = {}) {
|
|
@@ -152,12 +152,12 @@
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
function replaceActiveEditorText(text) {
|
|
156
|
-
return deps.replaceActiveEditorText?.(text) || { ok: false, reason: 'Editor adapter is unavailable' };
|
|
155
|
+
function replaceActiveEditorText(text, options) {
|
|
156
|
+
return deps.replaceActiveEditorText?.(text, options) || { ok: false, reason: 'Editor adapter is unavailable' };
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
function replaceActiveEditorPatches(patches, nextContent) {
|
|
160
|
-
return deps.replaceActiveEditorPatches?.(patches, nextContent) || { ok: false, reason: 'Editor adapter is unavailable' };
|
|
159
|
+
function replaceActiveEditorPatches(patches, nextContent, options) {
|
|
160
|
+
return deps.replaceActiveEditorPatches?.(patches, nextContent, options) || { ok: false, reason: 'Editor adapter is unavailable' };
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
function collectElements(selector, limit) {
|
|
@@ -266,6 +266,11 @@
|
|
|
266
266
|
fallbackUserMessage: 'The file changed while Codex was working.',
|
|
267
267
|
fallbackNextAction: 'Review the current file, then rerun the task.'
|
|
268
268
|
},
|
|
269
|
+
document_identity_mismatch: {
|
|
270
|
+
stage: 'preflight', severity: 'blocked', defaultRetryable: true,
|
|
271
|
+
fallbackUserMessage: 'Codex could not confirm that the editor content belongs to the target file, so it was not used.',
|
|
272
|
+
fallbackNextAction: 'Wait for the target file to open and settle, then retry. Reload Overleaf if this continues.'
|
|
273
|
+
},
|
|
269
274
|
patch_anchor_not_found: {
|
|
270
275
|
stage: 'preflight', severity: 'blocked', defaultRetryable: true,
|
|
271
276
|
fallbackUserMessage: 'The edit anchor no longer matches current Overleaf content.',
|
|
@@ -451,6 +456,10 @@
|
|
|
451
456
|
const trackedChangeCaptures = [];
|
|
452
457
|
const safeBaseFiles = normalizeBaseFilesForSafety(options.baseFiles);
|
|
453
458
|
const baseFileLookup = window.CodexOverleafStaleGuard?.buildBaseFileLookup(safeBaseFiles);
|
|
459
|
+
// Binary and unread entries are not independent empty-text references.
|
|
460
|
+
const provenanceReferences = new Map((Array.isArray(safeBaseFiles) ? safeBaseFiles : [])
|
|
461
|
+
.filter(file => file.kind !== 'binary' && typeof file.content === 'string')
|
|
462
|
+
.map(file => [file.path, file.content]));
|
|
454
463
|
const baseBinaryFileLookup = buildBaseBinaryFileLookup(safeBaseFiles);
|
|
455
464
|
// Recheck project identity between awaited operations and before each write.
|
|
456
465
|
const runProjectId = typeof options.runProjectId === 'string' ? options.runProjectId : '';
|
|
@@ -490,8 +499,16 @@
|
|
|
490
499
|
const deadlineFailure = () => Number.isFinite(options.deadlineAt) && now() >= options.deadlineAt
|
|
491
500
|
? { ok: false, code: 'writeback_deadline_exceeded', changedDocument: false,
|
|
492
501
|
reason: 'The undo time budget expired before this operation could start.' } : null;
|
|
493
|
-
const editOptions = () => ({ baseFileLookup, runProjectId, deadlineAt: options.deadlineAt,
|
|
502
|
+
const editOptions = () => ({ baseFileLookup, provenanceReferences, runProjectId, deadlineAt: options.deadlineAt,
|
|
494
503
|
reviewGuard: options.reviewGuard, reviewAwait: options.reviewAwait,
|
|
504
|
+
recheckEditDispatch: () => {
|
|
505
|
+
if (readWriteCancellationSequence() !== cancelBaselineSequence) return cancelledSkipResult;
|
|
506
|
+
if (!writeGuardSurface) return { ok: false, code: 'editor_project_id_unavailable', changedDocument: false };
|
|
507
|
+
const editorProjectId = writeGuardSurface.getEditorProjectIdPageSide(runProjectId);
|
|
508
|
+
const code = !runProjectId || !editorProjectId ? 'editor_project_id_unavailable'
|
|
509
|
+
: editorProjectId !== runProjectId ? 'aborted_project_changed' : '';
|
|
510
|
+
return code ? writeGuardSurface.abortDispatchResult(code, runProjectId || null, editorProjectId).skipped[0].result : null;
|
|
511
|
+
},
|
|
495
512
|
recheckWriteProject: writeGuardSurface ? () => {
|
|
496
513
|
const expired = options.reviewGuard?.() || deadlineFailure();
|
|
497
514
|
return expired ? { applied: [], skipped: [{ operation: {}, result: expired }] }
|
|
@@ -611,11 +628,54 @@
|
|
|
611
628
|
const isUntouchedOpenFailure = result => result?.ok === false && result.failure?.code === 'target_file_open_failed'
|
|
612
629
|
&& result.failure.changedDocument === false;
|
|
613
630
|
|
|
631
|
+
function documentIdentityContext(filePath, projectId = treeOperations.getProjectId?.()) {
|
|
632
|
+
const target = (deps.resolveTargetDocument || treeOperations.resolveTargetDocument)?.(filePath, { projectId });
|
|
633
|
+
return { projectId, targetDocumentId: target?.status === 'known' ? target.documentId : '' };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function documentProvenance(filePath, text, references, context = documentIdentityContext(filePath)) {
|
|
637
|
+
const capture = (deps.captureEditorDocument || treeOperations.captureEditorDocument)?.({ path: filePath, projectId: context.projectId });
|
|
638
|
+
const target = (deps.resolveTargetDocument || treeOperations.resolveTargetDocument)?.(filePath, { projectId: context.projectId });
|
|
639
|
+
if ((target?.status === 'known' && context.targetDocumentId && target.documentId !== context.targetDocumentId)
|
|
640
|
+
|| (context.projectId && treeOperations.getProjectId?.() !== context.projectId))
|
|
641
|
+
return { ok: false, code: 'document_identity_mismatch', identityState: 'mismatch' };
|
|
642
|
+
if (capture?.identityState === 'mismatch' || capture?.reason === 'navigation_conflict') return capture;
|
|
643
|
+
if (capture?.ok) {
|
|
644
|
+
const id = capture.documentIdentity.documentId;
|
|
645
|
+
if (context.targetDocumentId && context.targetDocumentId !== id)
|
|
646
|
+
return { ok: false, code: 'document_identity_mismatch', identityState: 'mismatch' };
|
|
647
|
+
context.targetDocumentId = id;
|
|
648
|
+
return { ok: true, identityState: 'match', capture };
|
|
649
|
+
}
|
|
650
|
+
if (typeof text !== 'string') return { ok: false, code: 'document_identity_mismatch', identityState: 'fallback' };
|
|
651
|
+
return window.CodexOverleafStaleGuard?.assessDocumentProvenance?.({ path: filePath, text, references })
|
|
652
|
+
|| { ok: true, identityState: 'fallback' };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function settleDocumentProvenance(filePath, references, context, deadlineAt = Infinity) {
|
|
656
|
+
const deadline = Math.min(deadlineAt, Date.now() + TreeOperations.ACTIVE_EDITOR_STABILITY_MS);
|
|
657
|
+
let result;
|
|
658
|
+
do {
|
|
659
|
+
result = documentProvenance(filePath, readActiveEditorText(), references, context);
|
|
660
|
+
if (result.identityState === 'match' || result.identityState === 'mismatch' || Date.now() >= deadline) return result;
|
|
661
|
+
await delay(Math.min(50, deadline - Date.now()));
|
|
662
|
+
} while (Date.now() <= deadline);
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function documentMismatchResult(filePath, provenance) {
|
|
667
|
+
const failure = buildPageFailure('document_identity_mismatch', { file: filePath,
|
|
668
|
+
operationType: 'edit', changedDocument: false,
|
|
669
|
+
evidence: { matchedPath: provenance.matchedPath, writeStarted: false } });
|
|
670
|
+
return { ...provenance, reason: failure.userMessage, failure };
|
|
671
|
+
}
|
|
672
|
+
|
|
614
673
|
async function applyEditOperation(operation, options = {}) {
|
|
615
674
|
const reviewBlock = options.reviewGuard?.();
|
|
616
675
|
if (reviewBlock) return reviewBlock;
|
|
617
676
|
const currentPath = getActiveFilePath();
|
|
618
|
-
const
|
|
677
|
+
const identityContext = documentIdentityContext(operation.path, options.runProjectId);
|
|
678
|
+
const editorReady = await awaitReview(options, () => ensureEditorReadyForOperation(operation, options.baseFileLookup, currentPath, options.provenanceReferences, identityContext, options.deadlineAt));
|
|
619
679
|
if (!editorReady.ok) {
|
|
620
680
|
return editorReady;
|
|
621
681
|
}
|
|
@@ -639,7 +699,7 @@
|
|
|
639
699
|
options.baseFileLookup
|
|
640
700
|
) || { ok: true };
|
|
641
701
|
if (!freshness.ok) {
|
|
642
|
-
const ready = await awaitReview(options, () => waitForFreshEditorTextForOperation(operation, options.baseFileLookup, 1500));
|
|
702
|
+
const ready = await awaitReview(options, () => waitForFreshEditorTextForOperation(operation, options.baseFileLookup, 1500, identityContext, options.provenanceReferences));
|
|
643
703
|
if (options.reviewGuard?.()) return options.reviewGuard();
|
|
644
704
|
if (!ready.ok) {
|
|
645
705
|
const decorated = attachWritebackDebug(freshness, 'stale_guard', operation, options.baseFileLookup, current, {
|
|
@@ -728,6 +788,7 @@
|
|
|
728
788
|
};
|
|
729
789
|
}
|
|
730
790
|
|
|
791
|
+
await awaitReview(options, () => settleDocumentProvenance(operation.path, options.provenanceReferences, identityContext, options.deadlineAt));
|
|
731
792
|
if (options.noTraceUndo === true) {
|
|
732
793
|
const blocked = typeof options.recheckWriteProject === 'function'
|
|
733
794
|
? await options.recheckWriteProject()
|
|
@@ -743,24 +804,40 @@
|
|
|
743
804
|
|
|
744
805
|
const finalReviewBlock = options.reviewGuard?.();
|
|
745
806
|
if (finalReviewBlock) return finalReviewBlock;
|
|
807
|
+
if (Number.isFinite(options.deadlineAt) && now() >= options.deadlineAt)
|
|
808
|
+
return { ok: false, code: 'writeback_deadline_exceeded', changedDocument: false };
|
|
809
|
+
if (options.noTraceUndo !== true) {
|
|
810
|
+
const dispatchBlock = options.recheckEditDispatch?.();
|
|
811
|
+
if (dispatchBlock) return dispatchBlock;
|
|
812
|
+
const reviewing = getReviewingState({});
|
|
813
|
+
if (trackReviewingChanges && !isReviewingConfirmedForWrite(reviewing))
|
|
814
|
+
return buildReviewingRequiredBlockedResult([operation], reviewing).skipped[0].result;
|
|
815
|
+
if (!trackReviewingChanges && !isEditingConfirmedForNoTraceUndo(reviewing))
|
|
816
|
+
return buildEditingRequiredBlockedResult([operation], reviewing).skipped[0].result;
|
|
817
|
+
}
|
|
746
818
|
// Read and dispatch synchronously against the same document. Navigation,
|
|
747
819
|
// freshness polling and Undo mode/project checks can all have awaited.
|
|
748
|
-
|
|
820
|
+
const observedText = readActiveEditorText();
|
|
821
|
+
const provenance = documentProvenance(operation.path, observedText, options.provenanceReferences, identityContext);
|
|
822
|
+
const dispatchText = provenance.capture?.text ?? observedText;
|
|
823
|
+
if (!provenance.ok) return documentMismatchResult(operation.path, provenance);
|
|
824
|
+
if (getActiveFilePath() !== operation.path || dispatchText !== current) {
|
|
749
825
|
const reason = 'The editor changed before the edit could be applied; the document was not overwritten.';
|
|
750
826
|
return { ok: false, code: 'stale_source_changed', reason,
|
|
751
827
|
failure: buildPageFailure('stale_source_changed', { file: operation.path,
|
|
752
828
|
operationType: options.noTraceUndo ? 'undo' : 'edit', changedDocument: false, userMessage: reason }) };
|
|
753
829
|
}
|
|
754
830
|
const result = Array.isArray(operation.patches) && operation.patches.length
|
|
755
|
-
? replaceActiveEditorPatches(preparedPatches.patches, nextContent)
|
|
756
|
-
: replaceActiveEditorText(nextContent);
|
|
831
|
+
? replaceActiveEditorPatches(preparedPatches.patches, nextContent, { expectedCapture: provenance.capture })
|
|
832
|
+
: replaceActiveEditorText(nextContent, { expectedCapture: provenance.capture });
|
|
757
833
|
if (!result.ok) {
|
|
758
834
|
return result;
|
|
759
835
|
}
|
|
760
836
|
|
|
761
837
|
const verifyWaitMs = Math.min(deps.writeVerifyWaitMs ?? 2500, Number.isFinite(options.deadlineAt)
|
|
762
838
|
? Math.max(0, options.deadlineAt - now()) : Infinity);
|
|
763
|
-
const verified = await verifyActiveEditorText(nextContent, operation.path, verifyWaitMs, current
|
|
839
|
+
const verified = await verifyActiveEditorText(nextContent, operation.path, verifyWaitMs, current,
|
|
840
|
+
{ ...identityContext, requireIdentity: provenance.identityState === 'match', references: options.provenanceReferences });
|
|
764
841
|
if (!verified.ok) {
|
|
765
842
|
const decorated = attachWritebackDebug(verified, 'write_verification', operation, options.baseFileLookup, readActiveEditorText(), {
|
|
766
843
|
initialActivePath: currentPath,
|
|
@@ -849,17 +926,19 @@
|
|
|
849
926
|
};
|
|
850
927
|
}
|
|
851
928
|
|
|
852
|
-
async function verifyActiveEditorText(expected, filePath, waitMs = 1000, before) {
|
|
929
|
+
async function verifyActiveEditorText(expected, filePath, waitMs = 1000, before, identityContext = documentIdentityContext(filePath)) {
|
|
853
930
|
const deadline = Date.now() + waitMs;
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
931
|
+
const fallbackAt = Math.min(deadline, Date.now() + TreeOperations.ACTIVE_EDITOR_STABILITY_MS);
|
|
932
|
+
let actual, provenance, identityConfirmed = false;
|
|
933
|
+
while (true) {
|
|
857
934
|
actual = readActiveEditorText();
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
};
|
|
935
|
+
provenance = documentProvenance(filePath, actual, identityContext.references, identityContext);
|
|
936
|
+
actual = provenance.capture?.text ?? actual;
|
|
937
|
+
identityConfirmed = provenance.ok && getActiveFilePath() === filePath
|
|
938
|
+
&& (provenance.identityState === 'match' || (!identityContext.requireIdentity && Date.now() >= fallbackAt));
|
|
939
|
+
if (identityConfirmed && actual === expected) return { ok: true };
|
|
940
|
+
if (Date.now() >= deadline) break;
|
|
941
|
+
await delay(Math.min(50, deadline - Date.now()));
|
|
863
942
|
}
|
|
864
943
|
return {
|
|
865
944
|
ok: false,
|
|
@@ -867,7 +946,8 @@
|
|
|
867
946
|
reason: `${filePath || '当前文件'} 写入后读回内容和 Codex 预期不一致,已停止把这次操作标记为成功。请刷新 Overleaf 后重试。`,
|
|
868
947
|
expectedLength: String(expected || '').length,
|
|
869
948
|
actualLength: String(actual || '').length,
|
|
870
|
-
evidence: describeReadbackMismatch(expected, actual, before, filePath)
|
|
949
|
+
evidence: { ...describeReadbackMismatch(expected, actual, before, filePath),
|
|
950
|
+
activeMatchesTarget: identityConfirmed, identityState: provenance.identityState }
|
|
871
951
|
};
|
|
872
952
|
}
|
|
873
953
|
|
|
@@ -897,7 +977,8 @@
|
|
|
897
977
|
return hash.toString(16).padStart(8, '0');
|
|
898
978
|
}
|
|
899
979
|
|
|
900
|
-
async function ensureEditorReadyForOperation(operation, baseFileLookup, initialActivePath
|
|
980
|
+
async function ensureEditorReadyForOperation(operation, baseFileLookup, initialActivePath, provenanceReferences,
|
|
981
|
+
identityContext = documentIdentityContext(operation.path), deadlineAt = Infinity) {
|
|
901
982
|
if (!operation?.path) {
|
|
902
983
|
return {
|
|
903
984
|
ok: true,
|
|
@@ -912,6 +993,7 @@
|
|
|
912
993
|
const currentText = readActiveEditorText();
|
|
913
994
|
const previousEditorIdentity = getActiveEditorIdentity();
|
|
914
995
|
const previousSignature = contentSignature(currentText);
|
|
996
|
+
const initialProvenance = documentProvenance(filePath, currentText, provenanceReferences, identityContext);
|
|
915
997
|
|
|
916
998
|
const opened = await openFileByPath(filePath, { force: initialActivePath === filePath });
|
|
917
999
|
if (!opened.ok) {
|
|
@@ -956,7 +1038,11 @@
|
|
|
956
1038
|
}
|
|
957
1039
|
|
|
958
1040
|
return waitForEditorContentForPath(filePath, previousEditorIdentity, baseFileLookup, 9000, {
|
|
1041
|
+
identityContext,
|
|
1042
|
+
deadlineAt,
|
|
959
1043
|
initialActivePath,
|
|
1044
|
+
initialProvenance,
|
|
1045
|
+
provenanceReferences,
|
|
960
1046
|
previousSignature,
|
|
961
1047
|
forceSettledOpen: true,
|
|
962
1048
|
openedMethod: opened.method || ''
|
|
@@ -964,11 +1050,13 @@
|
|
|
964
1050
|
}
|
|
965
1051
|
|
|
966
1052
|
async function waitForEditorContentForPath(filePath, previousEditorIdentity, baseFileLookup, timeoutMs, options = {}) {
|
|
967
|
-
const deadline = Date.now() + timeoutMs;
|
|
1053
|
+
const deadline = Math.min(options.deadlineAt ?? Infinity, Date.now() + timeoutMs);
|
|
968
1054
|
const startedAt = Date.now();
|
|
1055
|
+
const identityDeadline = Math.min(deadline, startedAt + Math.min(1500, writebackOpenSettleMs || TreeOperations.ACTIVE_EDITOR_STABILITY_MS));
|
|
969
1056
|
const minSettleMs = options.forceSettledOpen ? writebackOpenSettleMs : 0;
|
|
970
1057
|
let lastText = readActiveEditorText();
|
|
971
1058
|
let lastActivePath = getActiveFilePath();
|
|
1059
|
+
let mismatch = null;
|
|
972
1060
|
while (Date.now() < deadline) {
|
|
973
1061
|
lastActivePath = getActiveFilePath();
|
|
974
1062
|
lastText = readActiveEditorText();
|
|
@@ -977,21 +1065,28 @@
|
|
|
977
1065
|
const settledLongEnough = Date.now() - startedAt >= minSettleMs;
|
|
978
1066
|
const contentChangedFromPrevious = !options.previousSignature
|
|
979
1067
|
|| contentSignature(lastText) !== options.previousSignature;
|
|
1068
|
+
const provenance = documentProvenance(filePath, lastText, options.provenanceReferences, options.identityContext);
|
|
1069
|
+
lastText = provenance.capture?.text ?? lastText;
|
|
1070
|
+
mismatch = provenance.ok ? null : provenance;
|
|
980
1071
|
// Confirm navigation independently of source freshness. A stale target
|
|
981
1072
|
// can be correctly open; identical baseline bytes alone cannot prove a
|
|
982
1073
|
// switch. Content movement remains the fallback for editor lifecycles
|
|
983
1074
|
// whose document identity cannot be observed reliably.
|
|
984
|
-
const switchConfirmed = identityChanged
|
|
985
|
-
|| options.initialActivePath === filePath
|
|
1075
|
+
const switchConfirmed = provenance.identityState === 'match' || identityChanged
|
|
1076
|
+
|| (options.initialActivePath === filePath && Boolean(previousEditorIdentity)
|
|
1077
|
+
&& options.initialProvenance?.ok === true && provenance.ok)
|
|
986
1078
|
|| (contentChangedFromPrevious && settledLongEnough);
|
|
987
|
-
if (activeFileMatches && switchConfirmed && settledLongEnough
|
|
1079
|
+
if (activeFileMatches && switchConfirmed && settledLongEnough && provenance.ok
|
|
1080
|
+
&& (provenance.identityState === 'match' || Date.now() >= identityDeadline)) {
|
|
988
1081
|
return {
|
|
989
1082
|
ok: true,
|
|
990
|
-
text: lastText
|
|
1083
|
+
text: lastText,
|
|
1084
|
+
capture: provenance.capture
|
|
991
1085
|
};
|
|
992
1086
|
}
|
|
993
|
-
await delay(100);
|
|
1087
|
+
await delay(Math.max(0, Math.min(100, deadline - Date.now())));
|
|
994
1088
|
}
|
|
1089
|
+
if (mismatch) return documentMismatchResult(filePath, mismatch);
|
|
995
1090
|
// §9.1: if the active file ended up on a different document, this is
|
|
996
1091
|
// target_file_not_active; if the file did become active but the editor
|
|
997
1092
|
// document never settled, this is target_editor_not_ready.
|
|
@@ -1036,21 +1131,30 @@
|
|
|
1036
1131
|
const minSettleMs = options.forceSettledOpen ? writebackOpenSettleMs : 0;
|
|
1037
1132
|
let lastText = readActiveEditorText();
|
|
1038
1133
|
let lastActivePath = getActiveFilePath();
|
|
1134
|
+
const identityContext = options.identityContext || documentIdentityContext(filePath);
|
|
1135
|
+
let mismatch = null;
|
|
1039
1136
|
while (Date.now() < deadline) {
|
|
1040
1137
|
lastActivePath = getActiveFilePath();
|
|
1041
1138
|
lastText = readActiveEditorText();
|
|
1139
|
+
const provenance = documentProvenance(filePath, lastText, options.references, identityContext);
|
|
1140
|
+
lastText = provenance.capture?.text ?? lastText;
|
|
1141
|
+
mismatch = provenance.ok ? null : provenance;
|
|
1042
1142
|
const activeFileMatches = lastActivePath === filePath;
|
|
1043
1143
|
const identityChanged = Boolean(previousEditorIdentity) && activeEditorIdentityChanged(previousEditorIdentity);
|
|
1044
1144
|
const settledLongEnough = Date.now() - startedAt >= minSettleMs;
|
|
1045
1145
|
const sameFileReopenSettled = options.initialActivePath === filePath && settledLongEnough;
|
|
1046
|
-
if (activeFileMatches && settledLongEnough &&
|
|
1146
|
+
if (activeFileMatches && settledLongEnough && provenance.ok
|
|
1147
|
+
&& (provenance.identityState === 'match' || Date.now() - startedAt >= TreeOperations.ACTIVE_EDITOR_STABILITY_MS)
|
|
1148
|
+
&& (provenance.identityState === 'match' || identityChanged || sameFileReopenSettled)) {
|
|
1047
1149
|
return {
|
|
1048
1150
|
ok: true,
|
|
1049
|
-
text: lastText
|
|
1151
|
+
text: lastText,
|
|
1152
|
+
capture: provenance.capture
|
|
1050
1153
|
};
|
|
1051
1154
|
}
|
|
1052
|
-
await delay(100);
|
|
1155
|
+
await delay(Math.max(0, Math.min(100, deadline - Date.now())));
|
|
1053
1156
|
}
|
|
1157
|
+
if (mismatch) return documentMismatchResult(filePath, mismatch);
|
|
1054
1158
|
return {
|
|
1055
1159
|
ok: false,
|
|
1056
1160
|
code: 'editor_document_not_switched',
|
|
@@ -1065,24 +1169,27 @@
|
|
|
1065
1169
|
return normalized.trim().length === 0 ? '' : normalized;
|
|
1066
1170
|
}
|
|
1067
1171
|
|
|
1068
|
-
async function waitForFreshEditorTextForOperation(operation, baseFileLookup, waitMs = 1500) {
|
|
1172
|
+
async function waitForFreshEditorTextForOperation(operation, baseFileLookup, waitMs = 1500, identityContext, references) {
|
|
1069
1173
|
const deadline = Date.now() + Math.max(0, Number(waitMs) || 0);
|
|
1070
1174
|
while (Date.now() < deadline) {
|
|
1071
1175
|
if (operation?.path && getActiveFilePath() !== operation.path) {
|
|
1072
1176
|
await delay(60);
|
|
1073
1177
|
continue;
|
|
1074
1178
|
}
|
|
1075
|
-
const
|
|
1179
|
+
const observed = readActiveEditorText();
|
|
1180
|
+
const provenance = documentProvenance(operation.path, observed, references, identityContext);
|
|
1181
|
+
const text = provenance.capture?.text ?? observed;
|
|
1076
1182
|
const freshness = window.CodexOverleafStaleGuard?.checkOperationFreshness(
|
|
1077
1183
|
operation,
|
|
1078
1184
|
text,
|
|
1079
1185
|
baseFileLookup
|
|
1080
1186
|
) || { ok: true };
|
|
1081
|
-
if (freshness.ok) {
|
|
1187
|
+
if (freshness.ok && provenance.ok) {
|
|
1082
1188
|
return {
|
|
1083
1189
|
ok: true,
|
|
1084
1190
|
text,
|
|
1085
|
-
freshness
|
|
1191
|
+
freshness,
|
|
1192
|
+
capture: provenance.capture
|
|
1086
1193
|
};
|
|
1087
1194
|
}
|
|
1088
1195
|
await delay(60);
|
|
@@ -1431,6 +1538,7 @@
|
|
|
1431
1538
|
const expected = options.baseFileLookup?.get(operation.path);
|
|
1432
1539
|
const text = proof?.v === 1 && proof.kind === 'text' && typeof expected === 'string';
|
|
1433
1540
|
const binary = proof?.v === 1 && proof.kind === 'binary' && /^[a-f0-9]{64}$/i.test(proof.sha256 || '');
|
|
1541
|
+
const identityContext = documentIdentityContext(operation.path, projectId);
|
|
1434
1542
|
const isCurrent = () => Boolean(projectId) && projectId === options.runProjectId
|
|
1435
1543
|
&& treeOperations.getProjectId?.() === projectId && readWriteCancellationSequence() === cancellation && !options.reviewGuard?.();
|
|
1436
1544
|
if (!isCurrent() || (!text && !binary) || typeof deps.deleteTextFile !== 'function') {
|
|
@@ -1438,7 +1546,7 @@
|
|
|
1438
1546
|
reason: 'Verified created-file deletion is unavailable for this run.' };
|
|
1439
1547
|
}
|
|
1440
1548
|
if (text && projectPathExists(operation.path)) {
|
|
1441
|
-
const current = await readCurrentTextFileForFreshness(operation.path);
|
|
1549
|
+
const current = await readCurrentTextFileForFreshness(operation.path, options.baseFileLookup);
|
|
1442
1550
|
if (!current.ok || current.text !== expected || !isCurrent()) {
|
|
1443
1551
|
return { ok: false, code: 'undo_operation_failed', changedDocument: false,
|
|
1444
1552
|
reason: 'The created file changed after this run and was left untouched.' };
|
|
@@ -1447,9 +1555,10 @@
|
|
|
1447
1555
|
const result = await deps.deleteTextFile(operation, {
|
|
1448
1556
|
isCurrent, undoCreatedFile: true, deadlineAt: options.deadlineAt,
|
|
1449
1557
|
expectedContent: text ? expected : undefined,
|
|
1450
|
-
prepareTextPreimage: text ? () => readCurrentTextFileForFreshness(operation.path) : undefined,
|
|
1558
|
+
prepareTextPreimage: text ? () => readCurrentTextFileForFreshness(operation.path, options.baseFileLookup) : undefined,
|
|
1451
1559
|
expectedSha256: binary ? proof.sha256.toLowerCase() : undefined,
|
|
1452
1560
|
canDelete: () => isCurrent() && (binary || (treeOperations.getActiveFilePath?.() === operation.path
|
|
1561
|
+
&& documentProvenance(operation.path, readActiveEditorText(), options.baseFileLookup, identityContext).ok
|
|
1453
1562
|
&& readActiveEditorText() === expected))
|
|
1454
1563
|
});
|
|
1455
1564
|
if (!result.ok) return result;
|
|
@@ -1552,7 +1661,7 @@
|
|
|
1552
1661
|
};
|
|
1553
1662
|
}
|
|
1554
1663
|
|
|
1555
|
-
const current = await readCurrentTextFileForFreshness(operation.path);
|
|
1664
|
+
const current = await readCurrentTextFileForFreshness(operation.path, baseFileLookup);
|
|
1556
1665
|
if (!current.ok) {
|
|
1557
1666
|
return current;
|
|
1558
1667
|
}
|
|
@@ -1579,13 +1688,14 @@
|
|
|
1579
1688
|
return { ok: true };
|
|
1580
1689
|
}
|
|
1581
1690
|
|
|
1582
|
-
async function readCurrentTextFileForFreshness(filePath) {
|
|
1691
|
+
async function readCurrentTextFileForFreshness(filePath, references) {
|
|
1583
1692
|
const normalizedPath = normalizeSafeProjectPath(filePath);
|
|
1584
1693
|
if (!normalizedPath) {
|
|
1585
1694
|
return invalidProjectPathResult('freshness path');
|
|
1586
1695
|
}
|
|
1587
1696
|
const currentPath = getActiveFilePath();
|
|
1588
1697
|
const previousEditorIdentity = getActiveEditorIdentity();
|
|
1698
|
+
const identityContext = documentIdentityContext(normalizedPath);
|
|
1589
1699
|
const opened = await openFileByPath(normalizedPath, { force: true });
|
|
1590
1700
|
if (!opened.ok) {
|
|
1591
1701
|
return {
|
|
@@ -1595,6 +1705,8 @@
|
|
|
1595
1705
|
};
|
|
1596
1706
|
}
|
|
1597
1707
|
const ready = await waitForEditorDocumentForPath(normalizedPath, previousEditorIdentity, 9000, {
|
|
1708
|
+
identityContext,
|
|
1709
|
+
references,
|
|
1598
1710
|
initialActivePath: currentPath,
|
|
1599
1711
|
forceSettledOpen: true
|
|
1600
1712
|
});
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
document,
|
|
59
59
|
getActiveFilePathFromEditorStore,
|
|
60
60
|
getActiveEditorIdentity,
|
|
61
|
+
getCodeMirrorEditorView,
|
|
62
|
+
getCodeMirrorEditorViews: () => codeMirrorEditorViewCandidates().map(([, view]) => view).filter(isCodeMirrorEditorView),
|
|
61
63
|
normalizePath: normalizeSafeProjectPath,
|
|
62
64
|
readActiveEditorText,
|
|
63
65
|
window
|
|
@@ -97,6 +99,11 @@
|
|
|
97
99
|
projectSnapshotBridge = window.CodexOverleafProjectSnapshot.create({
|
|
98
100
|
buildProjectFileList: snapshotRouter.buildProjectFileList,
|
|
99
101
|
buildProjectSnapshot: snapshotRouter.buildProjectSnapshot,
|
|
102
|
+
getSnapshotEditorCaptures: snapshotRouter.getSnapshotEditorCaptures,
|
|
103
|
+
captureEditorDocument: treeOperations.captureEditorDocument,
|
|
104
|
+
isCaptureCurrent: treeOperations.isCaptureCurrent,
|
|
105
|
+
getActiveFilePath,
|
|
106
|
+
getActiveEditorIdentity,
|
|
100
107
|
getProjectId,
|
|
101
108
|
normalizePath: normalizeSafeProjectPath,
|
|
102
109
|
window
|
|
@@ -1675,21 +1682,21 @@
|
|
|
1675
1682
|
return editorAdapter.readActiveEditorText();
|
|
1676
1683
|
}
|
|
1677
1684
|
|
|
1678
|
-
function replaceActiveEditorText(text) {
|
|
1679
|
-
return editorAdapter.replaceActiveEditorText(text);
|
|
1685
|
+
function replaceActiveEditorText(text, options) {
|
|
1686
|
+
return editorAdapter.replaceActiveEditorText(text, options);
|
|
1680
1687
|
}
|
|
1681
1688
|
|
|
1682
|
-
function replaceActiveEditorPatches(patches, nextContent) {
|
|
1683
|
-
return editorAdapter.replaceActiveEditorPatches(patches, nextContent);
|
|
1689
|
+
function replaceActiveEditorPatches(patches, nextContent, options) {
|
|
1690
|
+
return editorAdapter.replaceActiveEditorPatches(patches, nextContent, options);
|
|
1684
1691
|
}
|
|
1685
1692
|
|
|
1686
1693
|
function getCodeMirrorEditorView() {
|
|
1687
1694
|
return findCodeMirrorEditorView()?.view || null;
|
|
1688
1695
|
}
|
|
1689
1696
|
|
|
1690
|
-
function
|
|
1697
|
+
function codeMirrorEditorViewCandidates() {
|
|
1691
1698
|
const store = getOverleafUnstableStore();
|
|
1692
|
-
|
|
1699
|
+
return [
|
|
1693
1700
|
['store:editor.view', readStoreValue(store, 'editor.view')],
|
|
1694
1701
|
['store:editor.view.camel', readStoreValue(store, 'editor.view.camel')],
|
|
1695
1702
|
['store:editor', readStoreValue(store, 'editor').view],
|
|
@@ -1698,7 +1705,10 @@
|
|
|
1698
1705
|
['window._ide.editorView', window._ide?.editorView]
|
|
1699
1706
|
];
|
|
1700
1707
|
|
|
1701
|
-
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function findCodeMirrorEditorView() {
|
|
1711
|
+
for (const [source, candidate] of codeMirrorEditorViewCandidates()) {
|
|
1702
1712
|
if (isCodeMirrorEditorView(candidate)) {
|
|
1703
1713
|
return {
|
|
1704
1714
|
view: candidate,
|
|
@@ -166,6 +166,9 @@
|
|
|
166
166
|
"timeout": {
|
|
167
167
|
"message": "timed out"
|
|
168
168
|
},
|
|
169
|
+
"native_request_timeout": {
|
|
170
|
+
"code": "native_request_timeout"
|
|
171
|
+
},
|
|
169
172
|
"output_limit": {
|
|
170
173
|
"message": "output limit exceeded"
|
|
171
174
|
},
|
|
@@ -182,7 +185,7 @@
|
|
|
182
185
|
"message": "unsupported_parameter reasoning.summary"
|
|
183
186
|
},
|
|
184
187
|
"storage_quota": {
|
|
185
|
-
"message": "quota"
|
|
188
|
+
"message": "storage quota"
|
|
186
189
|
},
|
|
187
190
|
"checkpoint": {
|
|
188
191
|
"message": "checkpoint"
|
|
@@ -220,6 +223,9 @@
|
|
|
220
223
|
"provider_rate_limited": {
|
|
221
224
|
"message": "provider_rate_limited"
|
|
222
225
|
},
|
|
226
|
+
"provider_quota_exceeded": {
|
|
227
|
+
"message": "provider_quota_exceeded"
|
|
228
|
+
},
|
|
223
229
|
"provider_connection_timeout": {
|
|
224
230
|
"message": "provider_connection_timeout"
|
|
225
231
|
},
|
|
@@ -865,6 +871,14 @@
|
|
|
865
871
|
nextEn: 'Switch between Responses and Chat Completions, or test again with Auto detection.',
|
|
866
872
|
code: 'provider_protocol_incompatible'
|
|
867
873
|
},
|
|
874
|
+
{
|
|
875
|
+
pattern: /provider_quota_exceeded|insufficient_quota|exceeded your current quota|(?:billing|usage|spend(?:ing)?|credit)[_ -]?(?:hard[_ -]?)?limit[_ -]?(?:reached|exceeded)|(?:billing|plan).{0,40}(?:quota|limit|exhausted)|(?:quota|credits?).{0,40}(?:billing|plan|exhausted|insufficient)/i,
|
|
876
|
+
conclusionZh: '模型服务的配额或账单限额已达到。',
|
|
877
|
+
conclusionEn: 'The model provider quota or billing limit was reached.',
|
|
878
|
+
nextZh: '请检查模型服务账户和套餐,或切换模型或模型服务后重试。',
|
|
879
|
+
nextEn: 'Check the provider account and plan, or switch model or provider before retrying.',
|
|
880
|
+
code: 'provider_quota_exceeded'
|
|
881
|
+
},
|
|
868
882
|
{
|
|
869
883
|
pattern: /provider_rate_limited|rate limit was reached/i,
|
|
870
884
|
conclusionZh: '这轮没有完成:第三方模型服务触发了限流。',
|
|
@@ -951,6 +965,13 @@
|
|
|
951
965
|
// failure data alongside the human-readable text. The string text is kept
|
|
952
966
|
// verbatim because it has mode-aware nuance (e.g. "No files were written"
|
|
953
967
|
// vs "This run did not start") the catalog's fallback strings don't capture.
|
|
968
|
+
if (context.code === 'native_request_timeout' || /native_request_timeout/i.test(text)) {
|
|
969
|
+
return {
|
|
970
|
+
conclusion: textFor(locale, '本机服务没有响应。', 'The local service did not respond.'),
|
|
971
|
+
nextStep: textFor(locale, '请重试,或打开「诊断」检查本机连接。', 'Retry, or open Diagnostics to check the local connection.'),
|
|
972
|
+
failureCode: 'native_request_timeout'
|
|
973
|
+
};
|
|
974
|
+
}
|
|
954
975
|
if (context.code === 'codex_local_skill_unavailable' || /(?:The )?selected local Codex skill is unavailable/i.test(text)) {
|
|
955
976
|
return {
|
|
956
977
|
conclusion: textFor(locale, '所选 Codex 本地技能不可用或已关闭,本轮没有启动。', 'The selected local Codex skill is unavailable or disabled. This run did not start.'),
|
|
@@ -1044,7 +1065,7 @@
|
|
|
1044
1065
|
failureCode: 'native_protocol_incompatible'
|
|
1045
1066
|
};
|
|
1046
1067
|
}
|
|
1047
|
-
if (/
|
|
1068
|
+
if (/kQuotaBytes|QUOTA_BYTES|QuotaExceededError|storage quota/i.test(text)) {
|
|
1048
1069
|
return {
|
|
1049
1070
|
conclusion: textFor(locale, 'Codex 结果已经生成,但本地会话记录超出 Chrome 存储配额。', 'Codex produced a result, but local session history exceeded Chrome storage quota.'),
|
|
1050
1071
|
nextStep: textFor(locale, '请删除一些旧 session,或刷新扩展后重试;这不是论文分析本身失败。', 'Delete older sessions or reload the extension and retry. The paper analysis itself did not fail.'),
|
|
@@ -261,6 +261,7 @@
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
function summarizeMirror(mirror = {}) {
|
|
264
|
+
mirror = mirror ?? { status: 'unavailable' };
|
|
264
265
|
const files = Array.isArray(mirror.files) ? mirror.files : [];
|
|
265
266
|
const skippedFiles = Array.isArray(mirror.skippedFiles) ? mirror.skippedFiles : [];
|
|
266
267
|
return removeEmptySummaryFields({
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const MIN_NATIVE_VERSION = '1.0.0';
|
|
13
13
|
const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
|
|
14
14
|
const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
|
|
15
|
-
const BUILD_TARGET_VERSION = '2.6.
|
|
15
|
+
const BUILD_TARGET_VERSION = '2.6.5';
|
|
16
16
|
const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
|
|
17
17
|
const REQUIRED_CAPABILITIES = Object.freeze([
|
|
18
18
|
'bridgePing',
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
'use strict';
|
|
6
6
|
const EPOCH_KEY = 'composer-attachment-draft:clear-epoch';
|
|
7
7
|
const normalize = value => SessionState.normalizeComposerAttachmentsForPersistence(value);
|
|
8
|
+
function normalizeOmitted(value) {
|
|
9
|
+
const names = (Array.isArray(value?.names) ? value.names : [])
|
|
10
|
+
.filter(name => typeof name === 'string')
|
|
11
|
+
.map(name => name.replace(/\0/g, '').trim().slice(0, 180))
|
|
12
|
+
.filter(Boolean).slice(0, 8);
|
|
13
|
+
return { count: names.length, names };
|
|
14
|
+
}
|
|
8
15
|
const scopeKey = scope => JSON.stringify([scope?.accountScopeId || '', scope?.projectId || '']);
|
|
9
16
|
function checkedScope(scope) {
|
|
10
17
|
const accountScopeId = String(scope?.accountScopeId || '').trim();
|
|
@@ -42,25 +49,28 @@
|
|
|
42
49
|
throw failure('account_scope_unavailable');
|
|
43
50
|
}
|
|
44
51
|
const revision = Number(previous?.revision) || 0;
|
|
45
|
-
result = { epoch, clearedAt, revision, attachments: normalize(previous?.attachments) };
|
|
52
|
+
result = { epoch, clearedAt, revision, attachments: normalize(previous?.attachments), omittedAttachments: normalizeOmitted(previous?.omittedAttachments) };
|
|
46
53
|
if (!value) return;
|
|
47
54
|
const attachments = normalize(value.attachments);
|
|
55
|
+
const omittedAttachments = normalizeOmitted(value.omittedAttachments);
|
|
56
|
+
const hasDraft = attachments.length || omittedAttachments.count;
|
|
48
57
|
if (value.epoch !== epoch) {
|
|
49
58
|
// A clear performed by this page also invalidates its old base.
|
|
50
59
|
// Its empty draft needs no write; an old nonempty draft cannot return.
|
|
51
|
-
if (!
|
|
60
|
+
if (!hasDraft) return;
|
|
52
61
|
throw failure('composer_draft_cleared');
|
|
53
62
|
}
|
|
54
63
|
if (value.revision !== revision) {
|
|
55
64
|
// Writing what another tab already stored changes nothing (for example
|
|
56
65
|
// both tabs clearing an empty draft on send); only a real edit conflicts.
|
|
57
|
-
if (JSON.stringify(attachments) === JSON.stringify(result.attachments)
|
|
66
|
+
if (JSON.stringify(attachments) === JSON.stringify(result.attachments)
|
|
67
|
+
&& JSON.stringify(omittedAttachments) === JSON.stringify(result.omittedAttachments)) return;
|
|
58
68
|
throw failure('composer_draft_conflict');
|
|
59
69
|
}
|
|
60
|
-
if (
|
|
70
|
+
if (hasDraft && value.unbased && clearedAt && value.changedAt <= clearedAt) {
|
|
61
71
|
throw failure('composer_draft_cleared');
|
|
62
72
|
}
|
|
63
|
-
result = { epoch, clearedAt, revision: revision + 1, attachments };
|
|
73
|
+
result = { epoch, clearedAt, revision: revision + 1, attachments, omittedAttachments };
|
|
64
74
|
store.put({ id, type: 'composer-attachment-draft', ...scope, ...result });
|
|
65
75
|
} catch (caught) { error = caught; tx.abort(); }
|
|
66
76
|
};
|
|
@@ -120,7 +130,11 @@
|
|
|
120
130
|
syncScope();
|
|
121
131
|
if (hydrated) return;
|
|
122
132
|
// Loading is allowed to finish after a local add/remove. Local edits win.
|
|
123
|
-
if (!changedVersion && !options.getAttachments().length)
|
|
133
|
+
if (!changedVersion && !options.getAttachments().length) {
|
|
134
|
+
options.restore(normalize(attachments));
|
|
135
|
+
const omitted = normalizeOmitted(base?.omittedAttachments);
|
|
136
|
+
if (omitted.count) options.onRestoreOmittedAttachments?.(omitted);
|
|
137
|
+
}
|
|
124
138
|
hydrated = true; publish();
|
|
125
139
|
}
|
|
126
140
|
function changed() {
|
|
@@ -138,10 +152,11 @@
|
|
|
138
152
|
if (captured !== generation || scopeKey(options.getScope()) !== key || changedVersion === savedVersion) return;
|
|
139
153
|
const version = changedVersion;
|
|
140
154
|
const attachments = normalize(options.getAttachments());
|
|
155
|
+
const omittedAttachments = normalizeOmitted({ names: options.getOmittedAttachmentNames?.() });
|
|
141
156
|
const unbased = dirtyUnbased, changedAt = dirtySince;
|
|
142
157
|
if (!base) await load(scope);
|
|
143
158
|
if (captured !== generation || scopeKey(options.getScope()) !== key) return;
|
|
144
|
-
const written = await options.write(checkedScope(scope), { ...base, attachments, unbased, changedAt });
|
|
159
|
+
const written = await options.write(checkedScope(scope), { ...base, attachments, omittedAttachments, unbased, changedAt });
|
|
145
160
|
if (captured !== generation) return;
|
|
146
161
|
base = written; savedVersion = version;
|
|
147
162
|
if (changedVersion === version) { dirtySince = 0; dirtyUnbased = false; }
|