codex-overleaf-link 2.5.1 → 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 +149 -398
- package/README.zh-CN.md +153 -404
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +148 -3
- package/extension/src/backgroundSharedSessions.js +55 -2
- package/extension/src/backgroundUpdateCoordinator.js +128 -12
- package/extension/src/content/activeTurnControl.js +164 -7
- package/extension/src/content/composerAttachments.js +46 -10
- package/extension/src/content/composerPanel.js +92 -12
- package/extension/src/content/contentRuntime.js +1114 -321
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +77 -34
- package/extension/src/content/diagnosticsExport.js +1 -1
- package/extension/src/content/diagnosticsPanel.js +147 -42
- package/extension/src/content/generated/content.bundle.js +5761 -1237
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +4 -0
- package/extension/src/content/localSkillsPanel.js +6 -4
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +53 -14
- package/extension/src/content/modelPicker.js +29 -19
- package/extension/src/content/nativeCompatibilityController.js +52 -19
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +7 -7
- package/extension/src/content/panelMaintenance.js +10 -2
- package/extension/src/content/panelRenderer.js +55 -28
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectSettingsCoordinator.js +30 -6
- package/extension/src/content/providerSettingsDialog.js +28 -19
- package/extension/src/content/recentProjects.js +30 -19
- package/extension/src/content/runActivitySummary.js +122 -14
- package/extension/src/content/runCompletionMeta.js +194 -6
- package/extension/src/content/runController.js +2 -1
- package/extension/src/content/runFailureNotice.js +8 -1
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +27 -13
- package/extension/src/content/runRecoveryActions.js +2 -1
- package/extension/src/content/runResultActions.js +73 -24
- package/extension/src/content/runReviewController.js +102 -37
- package/extension/src/content/runTimelineView.js +224 -25
- package/extension/src/content/scopedPersistencePanelState.js +124 -46
- package/extension/src/content/selectionContextView.js +30 -3
- package/extension/src/content/sensitivePreflight.js +14 -1
- package/extension/src/content/sessionManager.js +49 -3
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +15 -2
- package/extension/src/content/settingsWorkbench.js +127 -89
- package/extension/src/content/sharedSessionViewSync.js +13 -4
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateNotice.js +60 -6
- package/extension/src/content/writebackOrchestrator.js +301 -72
- package/extension/src/content/writingStyleSettings.js +70 -17
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/overleafCapabilities.js +4 -1
- 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/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +9 -1
- package/extension/src/page/treeOperations.js +200 -19
- package/extension/src/page/writebackRouter.js +172 -113
- package/extension/src/pageBridge.js +124 -73
- package/extension/src/popup.js +1 -1
- package/extension/src/shared/agentTranscript.js +372 -18
- package/extension/src/shared/auditRecords.js +3 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/compileAdapter.js +58 -17
- package/extension/src/shared/composerAttachmentDraft.js +27 -7
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/failureReasons.js +57 -3
- package/extension/src/shared/i18n.js +315 -29
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +2 -1
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
- package/extension/src/shared/sessionState.js +63 -4
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/sharedSessionBridge.js +81 -13
- package/extension/src/shared/staleGuard.js +130 -63
- package/extension/src/shared/storageDb.js +35 -3
- package/extension/src/shared/storageMigration.js +111 -2
- package/extension/src/shared/storageRunActions.js +62 -8
- package/extension/src/shared/undoOperations.js +126 -28
- package/extension/src/shared/updateRuntimeIdentity.js +16 -2
- package/extension/src/shared/writebackSettlement.js +21 -3
- package/extension/styles/panel.css +1293 -549
- package/native-host/src/bootstrapUpdate.js +52 -16
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +10 -1
- package/native-host/src/chatCompletionsBridge.js +29 -2
- package/native-host/src/codexPromptAssembly.js +86 -118
- package/native-host/src/codexSessionRunner.js +127 -17
- 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/providerContextError.js +20 -0
- 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 +28 -8
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/updateManager.js +144 -21
- package/native-host/src/writingStyleRuntime.js +1 -1
- package/package.json +5 -8
|
@@ -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.',
|
|
@@ -367,12 +372,12 @@
|
|
|
367
372
|
const trackReviewingChanges = options.requireReviewing === true && hasOperations;
|
|
368
373
|
let reviewingPolicy = null;
|
|
369
374
|
if (options.requireReviewing === true && hasOperations) {
|
|
370
|
-
const reviewing = await ensureReviewing({ waitMs: 1800 });
|
|
375
|
+
const reviewing = await ensureReviewing({ waitMs: 1800, runProjectId: options.runProjectId });
|
|
371
376
|
if (!reviewing.ok) {
|
|
372
377
|
return buildReviewingRequiredBlockedResult(operations, reviewing);
|
|
373
378
|
}
|
|
374
379
|
} else if (options.requireEditing === true && hasOperations) {
|
|
375
|
-
const editing = await ensureEditing({ waitMs: 1800 });
|
|
380
|
+
const editing = await ensureEditing({ waitMs: 1800, runProjectId: options.runProjectId });
|
|
376
381
|
if (!editing.ok) {
|
|
377
382
|
return buildEditingRequiredBlockedResult(operations, editing);
|
|
378
383
|
}
|
|
@@ -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, {
|
|
@@ -676,13 +736,13 @@
|
|
|
676
736
|
return decorated;
|
|
677
737
|
}
|
|
678
738
|
current = ready.text;
|
|
679
|
-
freshness =
|
|
739
|
+
freshness = ready.freshness;
|
|
680
740
|
}
|
|
681
741
|
|
|
682
742
|
let nextContent = null;
|
|
683
743
|
let preparedPatches;
|
|
684
744
|
if (Array.isArray(operation.patches) && operation.patches.length) {
|
|
685
|
-
preparedPatches = window.CodexOverleafStaleGuard?.prepareOperationPatches(operation, current, options.baseFileLookup)
|
|
745
|
+
preparedPatches = freshness.preparedPatches || window.CodexOverleafStaleGuard?.prepareOperationPatches(operation, current, options.baseFileLookup)
|
|
686
746
|
|| { ok: true, patches: operation.patches };
|
|
687
747
|
const patched = preparedPatches.ok ? applyTextPatches(current, preparedPatches.patches) : preparedPatches;
|
|
688
748
|
if (!patched.ok) {
|
|
@@ -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()
|
|
@@ -736,12 +797,6 @@
|
|
|
736
797
|
operationType: 'undo', changedDocument: false,
|
|
737
798
|
userMessage: 'The project identity guard is unavailable; the undo write was not applied.' }) };
|
|
738
799
|
if (blocked) return blocked.skipped?.[0]?.result || blocked;
|
|
739
|
-
if (getActiveFilePath() !== operation.path || readActiveEditorText() !== current) {
|
|
740
|
-
const reason = 'The editor changed while confirming Editing mode; the undo write was not applied.';
|
|
741
|
-
return { ok: false, code: 'stale_source_changed', reason,
|
|
742
|
-
failure: buildPageFailure('stale_source_changed', { file: operation.path,
|
|
743
|
-
operationType: 'undo', changedDocument: false, userMessage: reason }) };
|
|
744
|
-
}
|
|
745
800
|
if (!isEditingConfirmedForNoTraceUndo(getReviewingState({})))
|
|
746
801
|
return buildNoTraceUndoBlockedResult([operation], { ok: false, code: 'editing_not_confirmed',
|
|
747
802
|
reason: 'Overleaf left Editing mode before the undo write.' }).skipped[0].result;
|
|
@@ -749,16 +804,40 @@
|
|
|
749
804
|
|
|
750
805
|
const finalReviewBlock = options.reviewGuard?.();
|
|
751
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
|
+
}
|
|
818
|
+
// Read and dispatch synchronously against the same document. Navigation,
|
|
819
|
+
// freshness polling and Undo mode/project checks can all have awaited.
|
|
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) {
|
|
825
|
+
const reason = 'The editor changed before the edit could be applied; the document was not overwritten.';
|
|
826
|
+
return { ok: false, code: 'stale_source_changed', reason,
|
|
827
|
+
failure: buildPageFailure('stale_source_changed', { file: operation.path,
|
|
828
|
+
operationType: options.noTraceUndo ? 'undo' : 'edit', changedDocument: false, userMessage: reason }) };
|
|
829
|
+
}
|
|
752
830
|
const result = Array.isArray(operation.patches) && operation.patches.length
|
|
753
|
-
? replaceActiveEditorPatches(preparedPatches.patches, nextContent)
|
|
754
|
-
: replaceActiveEditorText(nextContent);
|
|
831
|
+
? replaceActiveEditorPatches(preparedPatches.patches, nextContent, { expectedCapture: provenance.capture })
|
|
832
|
+
: replaceActiveEditorText(nextContent, { expectedCapture: provenance.capture });
|
|
755
833
|
if (!result.ok) {
|
|
756
834
|
return result;
|
|
757
835
|
}
|
|
758
836
|
|
|
759
837
|
const verifyWaitMs = Math.min(deps.writeVerifyWaitMs ?? 2500, Number.isFinite(options.deadlineAt)
|
|
760
838
|
? Math.max(0, options.deadlineAt - now()) : Infinity);
|
|
761
|
-
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 });
|
|
762
841
|
if (!verified.ok) {
|
|
763
842
|
const decorated = attachWritebackDebug(verified, 'write_verification', operation, options.baseFileLookup, readActiveEditorText(), {
|
|
764
843
|
initialActivePath: currentPath,
|
|
@@ -827,6 +906,7 @@
|
|
|
827
906
|
}),
|
|
828
907
|
verified: true,
|
|
829
908
|
verifiedContent: nextContent,
|
|
909
|
+
...(preparedPatches ? { appliedPatches: preparedPatches.patches } : {}),
|
|
830
910
|
trackedChanges: mergeTrackedChangeRefs(trackedChanges)
|
|
831
911
|
};
|
|
832
912
|
}
|
|
@@ -840,22 +920,25 @@
|
|
|
840
920
|
...result,
|
|
841
921
|
verified: true,
|
|
842
922
|
verifiedContent: nextContent,
|
|
923
|
+
...(preparedPatches ? { appliedPatches: preparedPatches.patches } : {}),
|
|
843
924
|
trackedChangeCapture,
|
|
844
925
|
trackedChanges: mergeTrackedChangeRefs(trackedChanges)
|
|
845
926
|
};
|
|
846
927
|
}
|
|
847
928
|
|
|
848
|
-
async function verifyActiveEditorText(expected, filePath, waitMs = 1000, before) {
|
|
929
|
+
async function verifyActiveEditorText(expected, filePath, waitMs = 1000, before, identityContext = documentIdentityContext(filePath)) {
|
|
849
930
|
const deadline = Date.now() + waitMs;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
931
|
+
const fallbackAt = Math.min(deadline, Date.now() + TreeOperations.ACTIVE_EDITOR_STABILITY_MS);
|
|
932
|
+
let actual, provenance, identityConfirmed = false;
|
|
933
|
+
while (true) {
|
|
853
934
|
actual = readActiveEditorText();
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
};
|
|
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()));
|
|
859
942
|
}
|
|
860
943
|
return {
|
|
861
944
|
ok: false,
|
|
@@ -863,7 +946,8 @@
|
|
|
863
946
|
reason: `${filePath || '当前文件'} 写入后读回内容和 Codex 预期不一致,已停止把这次操作标记为成功。请刷新 Overleaf 后重试。`,
|
|
864
947
|
expectedLength: String(expected || '').length,
|
|
865
948
|
actualLength: String(actual || '').length,
|
|
866
|
-
evidence: describeReadbackMismatch(expected, actual, before, filePath)
|
|
949
|
+
evidence: { ...describeReadbackMismatch(expected, actual, before, filePath),
|
|
950
|
+
activeMatchesTarget: identityConfirmed, identityState: provenance.identityState }
|
|
867
951
|
};
|
|
868
952
|
}
|
|
869
953
|
|
|
@@ -893,7 +977,8 @@
|
|
|
893
977
|
return hash.toString(16).padStart(8, '0');
|
|
894
978
|
}
|
|
895
979
|
|
|
896
|
-
async function ensureEditorReadyForOperation(operation, baseFileLookup, initialActivePath
|
|
980
|
+
async function ensureEditorReadyForOperation(operation, baseFileLookup, initialActivePath, provenanceReferences,
|
|
981
|
+
identityContext = documentIdentityContext(operation.path), deadlineAt = Infinity) {
|
|
897
982
|
if (!operation?.path) {
|
|
898
983
|
return {
|
|
899
984
|
ok: true,
|
|
@@ -908,6 +993,7 @@
|
|
|
908
993
|
const currentText = readActiveEditorText();
|
|
909
994
|
const previousEditorIdentity = getActiveEditorIdentity();
|
|
910
995
|
const previousSignature = contentSignature(currentText);
|
|
996
|
+
const initialProvenance = documentProvenance(filePath, currentText, provenanceReferences, identityContext);
|
|
911
997
|
|
|
912
998
|
const opened = await openFileByPath(filePath, { force: initialActivePath === filePath });
|
|
913
999
|
if (!opened.ok) {
|
|
@@ -952,7 +1038,11 @@
|
|
|
952
1038
|
}
|
|
953
1039
|
|
|
954
1040
|
return waitForEditorContentForPath(filePath, previousEditorIdentity, baseFileLookup, 9000, {
|
|
1041
|
+
identityContext,
|
|
1042
|
+
deadlineAt,
|
|
955
1043
|
initialActivePath,
|
|
1044
|
+
initialProvenance,
|
|
1045
|
+
provenanceReferences,
|
|
956
1046
|
previousSignature,
|
|
957
1047
|
forceSettledOpen: true,
|
|
958
1048
|
openedMethod: opened.method || ''
|
|
@@ -960,52 +1050,43 @@
|
|
|
960
1050
|
}
|
|
961
1051
|
|
|
962
1052
|
async function waitForEditorContentForPath(filePath, previousEditorIdentity, baseFileLookup, timeoutMs, options = {}) {
|
|
963
|
-
const deadline = Date.now() + timeoutMs;
|
|
1053
|
+
const deadline = Math.min(options.deadlineAt ?? Infinity, Date.now() + timeoutMs);
|
|
964
1054
|
const startedAt = Date.now();
|
|
1055
|
+
const identityDeadline = Math.min(deadline, startedAt + Math.min(1500, writebackOpenSettleMs || TreeOperations.ACTIVE_EDITOR_STABILITY_MS));
|
|
965
1056
|
const minSettleMs = options.forceSettledOpen ? writebackOpenSettleMs : 0;
|
|
966
1057
|
let lastText = readActiveEditorText();
|
|
967
1058
|
let lastActivePath = getActiveFilePath();
|
|
1059
|
+
let mismatch = null;
|
|
968
1060
|
while (Date.now() < deadline) {
|
|
969
1061
|
lastActivePath = getActiveFilePath();
|
|
970
1062
|
lastText = readActiveEditorText();
|
|
971
1063
|
const activeFileMatches = lastActivePath === filePath;
|
|
972
1064
|
const identityChanged = Boolean(previousEditorIdentity) && activeEditorIdentityChanged(previousEditorIdentity);
|
|
973
|
-
const baseComparison = compareEditorTextToBase(filePath, lastText, baseFileLookup);
|
|
974
|
-
const baseMatches = baseComparison.matches;
|
|
975
1065
|
const settledLongEnough = Date.now() - startedAt >= minSettleMs;
|
|
976
1066
|
const contentChangedFromPrevious = !options.previousSignature
|
|
977
1067
|
|| contentSignature(lastText) !== options.previousSignature;
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const switchConfirmed = identityChanged
|
|
992
|
-
|| baseMatches
|
|
993
|
-
|| (options.initialActivePath === filePath
|
|
994
|
-
&& options.previousSignature
|
|
995
|
-
&& contentSignature(lastText) === options.previousSignature
|
|
996
|
-
&& settledLongEnough)
|
|
997
|
-
|| (!baseComparison.known && contentChangedFromPrevious && settledLongEnough);
|
|
998
|
-
const contentNoLongerLooksLikePreviousDocument = contentChangedFromPrevious
|
|
999
|
-
|| baseMatches
|
|
1000
|
-
|| options.initialActivePath === filePath;
|
|
1001
|
-
if (activeFileMatches && switchConfirmed && settledLongEnough && contentNoLongerLooksLikePreviousDocument) {
|
|
1068
|
+
const provenance = documentProvenance(filePath, lastText, options.provenanceReferences, options.identityContext);
|
|
1069
|
+
lastText = provenance.capture?.text ?? lastText;
|
|
1070
|
+
mismatch = provenance.ok ? null : provenance;
|
|
1071
|
+
// Confirm navigation independently of source freshness. A stale target
|
|
1072
|
+
// can be correctly open; identical baseline bytes alone cannot prove a
|
|
1073
|
+
// switch. Content movement remains the fallback for editor lifecycles
|
|
1074
|
+
// whose document identity cannot be observed reliably.
|
|
1075
|
+
const switchConfirmed = provenance.identityState === 'match' || identityChanged
|
|
1076
|
+
|| (options.initialActivePath === filePath && Boolean(previousEditorIdentity)
|
|
1077
|
+
&& options.initialProvenance?.ok === true && provenance.ok)
|
|
1078
|
+
|| (contentChangedFromPrevious && settledLongEnough);
|
|
1079
|
+
if (activeFileMatches && switchConfirmed && settledLongEnough && provenance.ok
|
|
1080
|
+
&& (provenance.identityState === 'match' || Date.now() >= identityDeadline)) {
|
|
1002
1081
|
return {
|
|
1003
1082
|
ok: true,
|
|
1004
|
-
text: lastText
|
|
1083
|
+
text: lastText,
|
|
1084
|
+
capture: provenance.capture
|
|
1005
1085
|
};
|
|
1006
1086
|
}
|
|
1007
|
-
await delay(100);
|
|
1087
|
+
await delay(Math.max(0, Math.min(100, deadline - Date.now())));
|
|
1008
1088
|
}
|
|
1089
|
+
if (mismatch) return documentMismatchResult(filePath, mismatch);
|
|
1009
1090
|
// §9.1: if the active file ended up on a different document, this is
|
|
1010
1091
|
// target_file_not_active; if the file did become active but the editor
|
|
1011
1092
|
// document never settled, this is target_editor_not_ready.
|
|
@@ -1050,21 +1131,30 @@
|
|
|
1050
1131
|
const minSettleMs = options.forceSettledOpen ? writebackOpenSettleMs : 0;
|
|
1051
1132
|
let lastText = readActiveEditorText();
|
|
1052
1133
|
let lastActivePath = getActiveFilePath();
|
|
1134
|
+
const identityContext = options.identityContext || documentIdentityContext(filePath);
|
|
1135
|
+
let mismatch = null;
|
|
1053
1136
|
while (Date.now() < deadline) {
|
|
1054
1137
|
lastActivePath = getActiveFilePath();
|
|
1055
1138
|
lastText = readActiveEditorText();
|
|
1139
|
+
const provenance = documentProvenance(filePath, lastText, options.references, identityContext);
|
|
1140
|
+
lastText = provenance.capture?.text ?? lastText;
|
|
1141
|
+
mismatch = provenance.ok ? null : provenance;
|
|
1056
1142
|
const activeFileMatches = lastActivePath === filePath;
|
|
1057
1143
|
const identityChanged = Boolean(previousEditorIdentity) && activeEditorIdentityChanged(previousEditorIdentity);
|
|
1058
1144
|
const settledLongEnough = Date.now() - startedAt >= minSettleMs;
|
|
1059
1145
|
const sameFileReopenSettled = options.initialActivePath === filePath && settledLongEnough;
|
|
1060
|
-
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)) {
|
|
1061
1149
|
return {
|
|
1062
1150
|
ok: true,
|
|
1063
|
-
text: lastText
|
|
1151
|
+
text: lastText,
|
|
1152
|
+
capture: provenance.capture
|
|
1064
1153
|
};
|
|
1065
1154
|
}
|
|
1066
|
-
await delay(100);
|
|
1155
|
+
await delay(Math.max(0, Math.min(100, deadline - Date.now())));
|
|
1067
1156
|
}
|
|
1157
|
+
if (mismatch) return documentMismatchResult(filePath, mismatch);
|
|
1068
1158
|
return {
|
|
1069
1159
|
ok: false,
|
|
1070
1160
|
code: 'editor_document_not_switched',
|
|
@@ -1074,69 +1164,32 @@
|
|
|
1074
1164
|
};
|
|
1075
1165
|
}
|
|
1076
1166
|
|
|
1077
|
-
function editorContentMatchesBase(filePath, content, baseFileLookup) {
|
|
1078
|
-
return compareEditorTextToBase(filePath, content, baseFileLookup).matches;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
function compareEditorTextToBase(filePath, content, baseFileLookup) {
|
|
1082
|
-
const normalizedPath = normalizeSafeProjectPath(filePath);
|
|
1083
|
-
const current = normalizeEditorTextForComparison(content);
|
|
1084
|
-
if (!baseFileLookup || !normalizedPath || !baseFileLookup.has(normalizedPath)) {
|
|
1085
|
-
return {
|
|
1086
|
-
known: false,
|
|
1087
|
-
hasContent: false,
|
|
1088
|
-
matches: false,
|
|
1089
|
-
baseLength: null,
|
|
1090
|
-
currentLength: current.length
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
const base = normalizeEditorTextForComparison(baseFileLookup.get(normalizedPath));
|
|
1094
|
-
const matches = current === base;
|
|
1095
|
-
if (!matches && base.trim().length > 0) {
|
|
1096
|
-
console.warn('[codex-overleaf] editorContentMatchesBase MISMATCH', filePath,
|
|
1097
|
-
'base.length:', base.length, 'current.length:', current.length,
|
|
1098
|
-
'base[0..80]:', JSON.stringify(base.slice(0, 80)),
|
|
1099
|
-
'current[0..80]:', JSON.stringify(current.slice(0, 80)));
|
|
1100
|
-
}
|
|
1101
|
-
return {
|
|
1102
|
-
known: true,
|
|
1103
|
-
hasContent: base.trim().length > 0,
|
|
1104
|
-
matches,
|
|
1105
|
-
baseLength: base.length,
|
|
1106
|
-
currentLength: current.length
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
function hasBaseFileContent(filePath, baseFileLookup) {
|
|
1111
|
-
if (!baseFileLookup || !filePath || !baseFileLookup.has(filePath)) {
|
|
1112
|
-
return false;
|
|
1113
|
-
}
|
|
1114
|
-
const content = baseFileLookup.get(filePath);
|
|
1115
|
-
return typeof content === 'string' && content.trim().length > 0;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
1167
|
function normalizeEditorTextForComparison(value) {
|
|
1119
1168
|
const normalized = String(value ?? '').replace(/\r\n/g, '\n');
|
|
1120
1169
|
return normalized.trim().length === 0 ? '' : normalized;
|
|
1121
1170
|
}
|
|
1122
1171
|
|
|
1123
|
-
async function waitForFreshEditorTextForOperation(operation, baseFileLookup, waitMs = 1500) {
|
|
1172
|
+
async function waitForFreshEditorTextForOperation(operation, baseFileLookup, waitMs = 1500, identityContext, references) {
|
|
1124
1173
|
const deadline = Date.now() + Math.max(0, Number(waitMs) || 0);
|
|
1125
1174
|
while (Date.now() < deadline) {
|
|
1126
1175
|
if (operation?.path && getActiveFilePath() !== operation.path) {
|
|
1127
1176
|
await delay(60);
|
|
1128
1177
|
continue;
|
|
1129
1178
|
}
|
|
1130
|
-
const
|
|
1179
|
+
const observed = readActiveEditorText();
|
|
1180
|
+
const provenance = documentProvenance(operation.path, observed, references, identityContext);
|
|
1181
|
+
const text = provenance.capture?.text ?? observed;
|
|
1131
1182
|
const freshness = window.CodexOverleafStaleGuard?.checkOperationFreshness(
|
|
1132
1183
|
operation,
|
|
1133
1184
|
text,
|
|
1134
1185
|
baseFileLookup
|
|
1135
1186
|
) || { ok: true };
|
|
1136
|
-
if (freshness.ok) {
|
|
1187
|
+
if (freshness.ok && provenance.ok) {
|
|
1137
1188
|
return {
|
|
1138
1189
|
ok: true,
|
|
1139
|
-
text
|
|
1190
|
+
text,
|
|
1191
|
+
freshness,
|
|
1192
|
+
capture: provenance.capture
|
|
1140
1193
|
};
|
|
1141
1194
|
}
|
|
1142
1195
|
await delay(60);
|
|
@@ -1485,6 +1538,7 @@
|
|
|
1485
1538
|
const expected = options.baseFileLookup?.get(operation.path);
|
|
1486
1539
|
const text = proof?.v === 1 && proof.kind === 'text' && typeof expected === 'string';
|
|
1487
1540
|
const binary = proof?.v === 1 && proof.kind === 'binary' && /^[a-f0-9]{64}$/i.test(proof.sha256 || '');
|
|
1541
|
+
const identityContext = documentIdentityContext(operation.path, projectId);
|
|
1488
1542
|
const isCurrent = () => Boolean(projectId) && projectId === options.runProjectId
|
|
1489
1543
|
&& treeOperations.getProjectId?.() === projectId && readWriteCancellationSequence() === cancellation && !options.reviewGuard?.();
|
|
1490
1544
|
if (!isCurrent() || (!text && !binary) || typeof deps.deleteTextFile !== 'function') {
|
|
@@ -1492,7 +1546,7 @@
|
|
|
1492
1546
|
reason: 'Verified created-file deletion is unavailable for this run.' };
|
|
1493
1547
|
}
|
|
1494
1548
|
if (text && projectPathExists(operation.path)) {
|
|
1495
|
-
const current = await readCurrentTextFileForFreshness(operation.path);
|
|
1549
|
+
const current = await readCurrentTextFileForFreshness(operation.path, options.baseFileLookup);
|
|
1496
1550
|
if (!current.ok || current.text !== expected || !isCurrent()) {
|
|
1497
1551
|
return { ok: false, code: 'undo_operation_failed', changedDocument: false,
|
|
1498
1552
|
reason: 'The created file changed after this run and was left untouched.' };
|
|
@@ -1501,9 +1555,10 @@
|
|
|
1501
1555
|
const result = await deps.deleteTextFile(operation, {
|
|
1502
1556
|
isCurrent, undoCreatedFile: true, deadlineAt: options.deadlineAt,
|
|
1503
1557
|
expectedContent: text ? expected : undefined,
|
|
1504
|
-
prepareTextPreimage: text ? () => readCurrentTextFileForFreshness(operation.path) : undefined,
|
|
1558
|
+
prepareTextPreimage: text ? () => readCurrentTextFileForFreshness(operation.path, options.baseFileLookup) : undefined,
|
|
1505
1559
|
expectedSha256: binary ? proof.sha256.toLowerCase() : undefined,
|
|
1506
1560
|
canDelete: () => isCurrent() && (binary || (treeOperations.getActiveFilePath?.() === operation.path
|
|
1561
|
+
&& documentProvenance(operation.path, readActiveEditorText(), options.baseFileLookup, identityContext).ok
|
|
1507
1562
|
&& readActiveEditorText() === expected))
|
|
1508
1563
|
});
|
|
1509
1564
|
if (!result.ok) return result;
|
|
@@ -1606,7 +1661,7 @@
|
|
|
1606
1661
|
};
|
|
1607
1662
|
}
|
|
1608
1663
|
|
|
1609
|
-
const current = await readCurrentTextFileForFreshness(operation.path);
|
|
1664
|
+
const current = await readCurrentTextFileForFreshness(operation.path, baseFileLookup);
|
|
1610
1665
|
if (!current.ok) {
|
|
1611
1666
|
return current;
|
|
1612
1667
|
}
|
|
@@ -1633,13 +1688,14 @@
|
|
|
1633
1688
|
return { ok: true };
|
|
1634
1689
|
}
|
|
1635
1690
|
|
|
1636
|
-
async function readCurrentTextFileForFreshness(filePath) {
|
|
1691
|
+
async function readCurrentTextFileForFreshness(filePath, references) {
|
|
1637
1692
|
const normalizedPath = normalizeSafeProjectPath(filePath);
|
|
1638
1693
|
if (!normalizedPath) {
|
|
1639
1694
|
return invalidProjectPathResult('freshness path');
|
|
1640
1695
|
}
|
|
1641
1696
|
const currentPath = getActiveFilePath();
|
|
1642
1697
|
const previousEditorIdentity = getActiveEditorIdentity();
|
|
1698
|
+
const identityContext = documentIdentityContext(normalizedPath);
|
|
1643
1699
|
const opened = await openFileByPath(normalizedPath, { force: true });
|
|
1644
1700
|
if (!opened.ok) {
|
|
1645
1701
|
return {
|
|
@@ -1649,6 +1705,8 @@
|
|
|
1649
1705
|
};
|
|
1650
1706
|
}
|
|
1651
1707
|
const ready = await waitForEditorDocumentForPath(normalizedPath, previousEditorIdentity, 9000, {
|
|
1708
|
+
identityContext,
|
|
1709
|
+
references,
|
|
1652
1710
|
initialActivePath: currentPath,
|
|
1653
1711
|
forceSettledOpen: true
|
|
1654
1712
|
});
|
|
@@ -1895,6 +1953,7 @@
|
|
|
1895
1953
|
ok: false,
|
|
1896
1954
|
code: reviewing?.code || 'reviewing_not_enabled',
|
|
1897
1955
|
reason: reviewing?.reason || 'Overleaf Reviewing/Track Changes was not confirmed before writing. Codex did not change this file.',
|
|
1956
|
+
evidence: reviewing?.evidence, failure: reviewing?.failure,
|
|
1898
1957
|
reviewing: reviewing?.reviewing || null
|
|
1899
1958
|
}
|
|
1900
1959
|
})),
|