codex-overleaf-link 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -392
- package/README.zh-CN.md +154 -393
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +283 -55
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +129 -16
- package/extension/src/backgroundUpdateCoordinator.js +242 -29
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +171 -8
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +104 -47
- package/extension/src/content/composerPanel.js +110 -12
- package/extension/src/content/contentRuntime.js +1555 -926
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +48 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +11077 -3412
- package/extension/src/content/generated/content.bundle.meta.json +14 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +10 -0
- package/extension/src/content/localSkillsPanel.js +104 -14
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +47 -11
- package/extension/src/content/modelPicker.js +8 -4
- package/extension/src/content/nativeCompatibilityController.js +74 -30
- package/extension/src/content/otWarmMirror.js +5 -6
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +17 -4
- package/extension/src/content/panelMaintenance.js +46 -14
- package/extension/src/content/panelRenderer.js +57 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +67 -6
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +73 -14
- package/extension/src/content/recentProjects.js +15 -8
- package/extension/src/content/runActivitySummary.js +160 -18
- package/extension/src/content/runCompletionMeta.js +532 -0
- package/extension/src/content/runController.js +39 -15
- package/extension/src/content/runFailureNotice.js +29 -13
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +157 -47
- package/extension/src/content/runRecoveryActions.js +147 -0
- package/extension/src/content/runResultActions.js +28 -143
- package/extension/src/content/runReviewController.js +633 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +367 -175
- package/extension/src/content/scopedPersistencePanelState.js +134 -20
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +153 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +50 -4
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +22 -5
- package/extension/src/content/settingsWorkbench.js +132 -89
- package/extension/src/content/sharedSessionViewSync.js +49 -11
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +118 -12
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +116 -94
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +114 -26
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +103 -155
- package/extension/src/pageBridge.js +126 -69
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +373 -17
- package/extension/src/shared/auditRecords.js +4 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +129 -66
- package/extension/src/shared/composerAttachmentDraft.js +156 -0
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +349 -4
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +590 -24
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- 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/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +132 -28
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +118 -27
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +166 -52
- package/extension/src/shared/storageDb.js +90 -26
- package/extension/src/shared/storageMigration.js +78 -112
- package/extension/src/shared/storageRunActions.js +267 -30
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +253 -32
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +17 -3
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +58 -40
- package/extension/styles/panel.css +1123 -493
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +269 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +20 -1
- package/native-host/src/chatCompletionsBridge.js +48 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +114 -127
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +234 -198
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +92 -57
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +409 -46
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +5 -8
- package/scripts/install-managed.mjs +1 -0
|
@@ -7,12 +7,23 @@
|
|
|
7
7
|
})(typeof window !== 'undefined' ? window : globalThis, function activeTurnControlFactory() {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
+
// Startup has an owner before a Native request exists. This identity is only
|
|
11
|
+
// for ownership probes; nativeRequestId remains reserved for the actual RPC.
|
|
12
|
+
function getStartupRequestId(sessionId, runId) {
|
|
13
|
+
return `startup:${encodeURIComponent(sessionId)}:${encodeURIComponent(runId)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
function create(options = {}) {
|
|
11
17
|
const runtime = options.chrome?.runtime;
|
|
12
18
|
let port = null;
|
|
13
19
|
let binding = null;
|
|
14
20
|
let reconnectTimer = null;
|
|
15
21
|
let destroyed = false;
|
|
22
|
+
let bindingWaiter = null;
|
|
23
|
+
let bindingSequence = 0;
|
|
24
|
+
let recoveryTimer = null;
|
|
25
|
+
let recoveryGeneration = 0;
|
|
26
|
+
const inventoryRetries = new Map();
|
|
16
27
|
|
|
17
28
|
function connect() {
|
|
18
29
|
if (destroyed || port || typeof runtime?.connect !== 'function') {
|
|
@@ -21,6 +32,11 @@
|
|
|
21
32
|
try {
|
|
22
33
|
const nextPort = runtime.connect({ name: 'codex-overleaf/run-owner' });
|
|
23
34
|
port = nextPort;
|
|
35
|
+
nextPort.onMessage?.addListener?.(message => {
|
|
36
|
+
if (port !== nextPort || message?.type !== 'bound' || !bindingWaiter || !binding) return;
|
|
37
|
+
if (['requestId', 'projectKey', 'clientRunId', 'sessionId'].every(key => message[key] === binding[key])
|
|
38
|
+
&& message.browserOnly === true && message.ownerToken === binding.ownerToken) bindingWaiter(message.ok !== false);
|
|
39
|
+
});
|
|
24
40
|
nextPort.onDisconnect?.addListener?.(() => {
|
|
25
41
|
if (port === nextPort) {
|
|
26
42
|
port = null;
|
|
@@ -46,20 +62,39 @@
|
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
function bind(value = {}) {
|
|
65
|
+
bindingWaiter?.(false);
|
|
49
66
|
binding = {
|
|
50
67
|
requestId: String(value.requestId || ''),
|
|
51
68
|
projectKey: String(value.projectKey || ''),
|
|
52
69
|
clientRunId: String(value.clientRunId || ''),
|
|
53
|
-
sessionId: String(value.sessionId || '')
|
|
70
|
+
sessionId: String(value.sessionId || ''),
|
|
71
|
+
...(value.browserOnly === true ? { browserOnly: true, ownerToken: String(value.ownerToken || `browser-${++bindingSequence}`) } : {})
|
|
54
72
|
};
|
|
55
73
|
connect();
|
|
56
74
|
postBinding('bind', binding);
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
function
|
|
60
|
-
|
|
77
|
+
async function bindBrowserRun(value) {
|
|
78
|
+
bind({ ...value, browserOnly: true });
|
|
79
|
+
if (destroyed || !port) return false;
|
|
80
|
+
return new Promise(resolve => {
|
|
81
|
+
const timeout = setTimeout(() => complete(false), 5000);
|
|
82
|
+
const complete = ok => {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
if (bindingWaiter === complete) bindingWaiter = null;
|
|
85
|
+
resolve(ok);
|
|
86
|
+
};
|
|
87
|
+
bindingWaiter = complete;
|
|
88
|
+
postBinding('bind', binding);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function release(requestId, ownerToken) {
|
|
93
|
+
if (!binding || (requestId && binding.requestId !== requestId)
|
|
94
|
+
|| (ownerToken && ownerToken !== binding.ownerToken)) {
|
|
61
95
|
return;
|
|
62
96
|
}
|
|
97
|
+
bindingWaiter?.(false);
|
|
63
98
|
postBinding('release', binding);
|
|
64
99
|
binding = null;
|
|
65
100
|
}
|
|
@@ -86,14 +121,97 @@
|
|
|
86
121
|
return response?.ok === true;
|
|
87
122
|
}
|
|
88
123
|
|
|
124
|
+
async function knownJournals(requestIds, owners = [], onRetry) {
|
|
125
|
+
if (typeof runtime?.sendMessage !== 'function') return null;
|
|
126
|
+
const known = new Set();
|
|
127
|
+
for (let offset = 0; offset < requestIds.length; offset += 64) {
|
|
128
|
+
const ids = requestIds.slice(offset, offset + 64);
|
|
129
|
+
const response = await runtime.sendMessage({ type: 'codex-overleaf/run-journal/known',
|
|
130
|
+
requestIds: ids, owners: owners.slice(offset, offset + 64) });
|
|
131
|
+
// An older background cannot rule out browser-only owners.
|
|
132
|
+
const needsStartupInventory = owners.slice(offset, offset + 64).some(owner => owner.preDispatch === true);
|
|
133
|
+
if (!response?.ok || !Array.isArray(response.known) || response.ownersChecked !== true
|
|
134
|
+
|| (needsStartupInventory && response.preDispatchChecked !== true)) {
|
|
135
|
+
if (Number.isFinite(response?.retryAfterMs) && response.retryAfterMs > 0) onRetry?.(response.retryAfterMs);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
for (const id of response.known) if (ids.includes(id)) known.add(id);
|
|
139
|
+
}
|
|
140
|
+
return known;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Native journals and live browser-only owners are separate evidence. A
|
|
144
|
+
// missing journal alone cannot establish that another page stopped retrying.
|
|
145
|
+
async function recoverOrphanedRuns(projectKey, options, result, isCurrent) {
|
|
146
|
+
const now = Date.now(), candidates = [];
|
|
147
|
+
let recheckAfterMs = Infinity;
|
|
148
|
+
const sessions = () => options.listSessions?.() || [];
|
|
149
|
+
const requestIdFor = (session, record) => record.nativeRequestId || getStartupRequestId(session.id, record.id);
|
|
150
|
+
const eligible = (session, record) => record.status === 'running' && session.id && record.id
|
|
151
|
+
&& !options.isLiveRun?.(record, session)
|
|
152
|
+
&& (!session.projectId || session.projectId === projectKey)
|
|
153
|
+
&& (!options.accountScopeId || !session.accountScopeId || session.accountScopeId === options.accountScopeId)
|
|
154
|
+
&& (!record.runProjectId || record.runProjectId === projectKey);
|
|
155
|
+
for (const session of sessions()) {
|
|
156
|
+
for (const record of session.runs || []) {
|
|
157
|
+
if (!eligible(session, record)) continue;
|
|
158
|
+
const started = Date.parse(record.startedAt);
|
|
159
|
+
if (!Number.isFinite(started)) continue;
|
|
160
|
+
const remaining = started + (options.orphanGraceMs ?? 60000) - now;
|
|
161
|
+
if (remaining > 0) { recheckAfterMs = Math.min(recheckAfterMs, remaining); continue; }
|
|
162
|
+
candidates.push({ session, record, requestId: requestIdFor(session, record) });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
result.recheckAfterMs = Number.isFinite(recheckAfterMs) ? recheckAfterMs : null;
|
|
166
|
+
if (!candidates.length) return;
|
|
167
|
+
const known = await knownJournals(candidates.map(item => item.requestId), candidates.map(({ session, record, requestId }) => ({
|
|
168
|
+
requestId, projectKey, clientRunId: record.id, sessionId: session.id,
|
|
169
|
+
...(!record.nativeRequestId ? { preDispatch: true } : {})
|
|
170
|
+
})), delay => {
|
|
171
|
+
const key = `${options.accountScopeId || ''}:${projectKey}`;
|
|
172
|
+
const attempts = inventoryRetries.get(key) || 0;
|
|
173
|
+
if (attempts >= 3) return;
|
|
174
|
+
inventoryRetries.set(key, attempts + 1);
|
|
175
|
+
result.recheckAfterMs = Math.min(result.recheckAfterMs ?? Infinity, delay);
|
|
176
|
+
}).catch(() => null);
|
|
177
|
+
if (!known || !isCurrent()) return;
|
|
178
|
+
inventoryRetries.delete(`${options.accountScopeId || ''}:${projectKey}`);
|
|
179
|
+
for (const { session, record, requestId } of candidates) {
|
|
180
|
+
const currentSession = sessions().find(value => value.id === session.id);
|
|
181
|
+
// State normalization can replace run objects while inventory is pending.
|
|
182
|
+
if (!currentSession?.runs?.includes(record) || !eligible(currentSession, record)
|
|
183
|
+
|| requestIdFor(currentSession, record) !== requestId || known.has(requestId)) continue;
|
|
184
|
+
if (!record.runProjectId) record.runProjectId = projectKey;
|
|
185
|
+
options.normalizeInterruptedRun?.(record);
|
|
186
|
+
record.status = 'interrupted';
|
|
187
|
+
record.interruptedDraft = { requestId, threadId: currentSession.codexThreadId || '',
|
|
188
|
+
turnId: record.codexTurnId || '', lastEventSeq: record.nativeEventSeq || 0, reason: 'extension_reloaded' };
|
|
189
|
+
options.pauseSessionQueue?.(currentSession, 'page_reload');
|
|
190
|
+
result.changed = true;
|
|
191
|
+
result.recoveredRuns.push({ sessionId: currentSession.id, runId: record.id,
|
|
192
|
+
projectId: projectKey, accountScopeId: options.accountScopeId });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function cancelRecovery() {
|
|
197
|
+
recoveryGeneration++;
|
|
198
|
+
clearTimeout(recoveryTimer);
|
|
199
|
+
recoveryTimer = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
89
202
|
async function recoverJournals(options = {}) {
|
|
203
|
+
cancelRecovery();
|
|
204
|
+
const generation = recoveryGeneration;
|
|
205
|
+
const isCurrent = () => !destroyed && generation === recoveryGeneration && options.isCurrent?.() !== false;
|
|
90
206
|
const projectKey = String(options.projectKey || '');
|
|
91
207
|
if (!projectKey) {
|
|
92
208
|
return { changed: false, acknowledgeIds: [] };
|
|
93
209
|
}
|
|
94
210
|
const journals = await listJournals(projectKey).catch(() => []);
|
|
211
|
+
if (!isCurrent()) return { changed: false, acknowledgeIds: [], recoveredRuns: [] };
|
|
95
212
|
let changed = false;
|
|
96
213
|
const acknowledgeIds = [];
|
|
214
|
+
const recoveredRuns = [];
|
|
97
215
|
for (const journal of journals) {
|
|
98
216
|
if (!journal?.terminal && !journal?.ownerLost) {
|
|
99
217
|
continue;
|
|
@@ -104,7 +222,8 @@
|
|
|
104
222
|
continue;
|
|
105
223
|
}
|
|
106
224
|
const session = options.findSession?.(journal.sessionId);
|
|
107
|
-
if (!session)
|
|
225
|
+
if (!session || (session.projectId && session.projectId !== projectKey)
|
|
226
|
+
|| (options.accountScopeId && session.accountScopeId && session.accountScopeId !== options.accountScopeId)) {
|
|
108
227
|
continue;
|
|
109
228
|
}
|
|
110
229
|
let record = (session.runs || []).find(run => run.id === journal.clientRunId);
|
|
@@ -114,9 +233,19 @@
|
|
|
114
233
|
session.runs = [...(session.runs || []), record].slice(-20);
|
|
115
234
|
}
|
|
116
235
|
}
|
|
117
|
-
if (!record) {
|
|
236
|
+
if (!record || options.isLiveRun?.(record, session)) {
|
|
118
237
|
continue;
|
|
119
238
|
}
|
|
239
|
+
if (record.runProjectId && record.runProjectId !== projectKey) continue;
|
|
240
|
+
if (!record.runProjectId) record.runProjectId = projectKey;
|
|
241
|
+
if (journal.operation === 'compact') {
|
|
242
|
+
record.operation = 'compact';
|
|
243
|
+
if (!record.codexThreadId) record.codexThreadId = String(journal.threadId || '');
|
|
244
|
+
if (session.contextUsage?.threadId === record.codexThreadId
|
|
245
|
+
&& Date.parse(session.contextUsage.observedAt || '') < Date.parse(record.startedAt || '')) {
|
|
246
|
+
session.contextUsage = null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
120
249
|
if (record.status === 'running') {
|
|
121
250
|
options.normalizeInterruptedRun?.(record);
|
|
122
251
|
}
|
|
@@ -136,6 +265,9 @@
|
|
|
136
265
|
record.nativeEventSeq = sequence;
|
|
137
266
|
continue;
|
|
138
267
|
}
|
|
268
|
+
// Non-display state (for example context usage) must also recover
|
|
269
|
+
// from the journal, after the owning thread/turn has been bound.
|
|
270
|
+
options.onRawEvent?.(raw, { record, session, sequence, journal });
|
|
139
271
|
const recovered = options.mapEvent?.(raw, journal);
|
|
140
272
|
if (recovered) {
|
|
141
273
|
if (recovered.kind === 'stream') {
|
|
@@ -163,12 +295,24 @@
|
|
|
163
295
|
options.pauseSessionQueue?.(session, 'page_reload');
|
|
164
296
|
changed = true;
|
|
165
297
|
acknowledgeIds.push(journal.requestId);
|
|
298
|
+
recoveredRuns.push({ sessionId: session.id, runId: record.id,
|
|
299
|
+
projectId: projectKey, accountScopeId: options.accountScopeId });
|
|
300
|
+
}
|
|
301
|
+
const result = { changed, acknowledgeIds, recoveredRuns };
|
|
302
|
+
await recoverOrphanedRuns(projectKey, options, result, isCurrent);
|
|
303
|
+
if (isCurrent() && result.recheckAfterMs !== null && typeof options.onRecheck === 'function') {
|
|
304
|
+
recoveryTimer = setTimeout(() => {
|
|
305
|
+
recoveryTimer = null;
|
|
306
|
+
if (isCurrent()) Promise.resolve(options.onRecheck()).catch(() => {});
|
|
307
|
+
}, Math.min(result.recheckAfterMs, 2147483647));
|
|
166
308
|
}
|
|
167
|
-
return
|
|
309
|
+
return result;
|
|
168
310
|
}
|
|
169
311
|
|
|
170
312
|
function destroy() {
|
|
171
313
|
destroyed = true;
|
|
314
|
+
cancelRecovery();
|
|
315
|
+
bindingWaiter?.(false);
|
|
172
316
|
clearTimeout(reconnectTimer);
|
|
173
317
|
reconnectTimer = null;
|
|
174
318
|
try {
|
|
@@ -181,8 +325,27 @@
|
|
|
181
325
|
}
|
|
182
326
|
|
|
183
327
|
connect();
|
|
184
|
-
return { acknowledge, bind,
|
|
328
|
+
return { acknowledge, bind, bindBrowserRun, cancelRecovery, destroy, getOwner: () => binding ? { ...binding } : null,
|
|
329
|
+
knownJournals, listJournals, recoverJournals, release };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function createInterruptedRun(journal, projectKey, statusText) {
|
|
333
|
+
return { id: journal.clientRunId, task: journal.task || 'interrupted task', mode: journal.mode || 'ask',
|
|
334
|
+
...(journal.operation === 'compact' ? { operation: 'compact', codexThreadId: String(journal.threadId || '') } : {}),
|
|
335
|
+
model: journal.model || '', status: 'interrupted', statusText, runProjectId: projectKey,
|
|
336
|
+
startedAt: journal.createdAt || new Date().toISOString(), finishedAt: journal.updatedAt || new Date().toISOString(),
|
|
337
|
+
events: [], attachments: [], undoOperations: [], undoTrackedChanges: [], undoExpectedFiles: [] };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function mapRecoveredEvent(raw, journal, options) {
|
|
341
|
+
const activity = options.mapActivity(raw);
|
|
342
|
+
if (!activity?.visible || activity.kind === 'technical') return null;
|
|
343
|
+
return { kind: activity.kind || 'activity', title: options.sanitizeText(activity.title) || 'Event',
|
|
344
|
+
status: activity.status || 'running', detail: options.sanitizeValue(activity.detail),
|
|
345
|
+
timestamp: raw.timestamp || journal.updatedAt || new Date().toISOString(),
|
|
346
|
+
streamKey: options.sanitizeText(activity.streamKey), streamRole: options.sanitizeText(activity.streamRole),
|
|
347
|
+
appendText: options.sanitizeText(activity.appendText), replaceText: activity.replaceText };
|
|
185
348
|
}
|
|
186
349
|
|
|
187
|
-
return { create };
|
|
350
|
+
return { create, createInterruptedRun, mapRecoveredEvent, getStartupRequestId };
|
|
188
351
|
});
|
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
*/
|
|
86
86
|
function normalizeSkippedFailure(item) {
|
|
87
87
|
if (!FailureReasons || !FailureReasons.normalizeFailureReason) return null;
|
|
88
|
-
const
|
|
88
|
+
const normalize = FailureReasons.normalizeWritebackFailure || FailureReasons.normalizeFailureReason;
|
|
89
|
+
const failure = normalize(item?.result, item?.operation || {}, { locale: getLocale() });
|
|
89
90
|
if (!failure || !failure.code) return null;
|
|
90
91
|
const localized = FailureReasons.localizeFailureReason
|
|
91
92
|
? FailureReasons.localizeFailureReason(failure, getLocale(), failureReasonI18nLookup)
|
|
@@ -98,7 +99,7 @@
|
|
|
98
99
|
|
|
99
100
|
function pickPrimarySkippedFailure(failures) {
|
|
100
101
|
if (!FailureReasons || !FailureReasons.selectPrimaryFailure) return null;
|
|
101
|
-
return FailureReasons.selectPrimaryFailure(failures);
|
|
102
|
+
return (FailureReasons.selectPrimaryWritebackFailure || FailureReasons.selectPrimaryFailure)(failures);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
function failureReasonI18nLookup(key) {
|
|
@@ -134,6 +135,8 @@
|
|
|
134
135
|
const code = result.code || '';
|
|
135
136
|
const filePath = result.reasonParams?.filePath || operation.path || operation.from || operation.to || '';
|
|
136
137
|
const withDebug = reason => appendApplyResultDebug(reason, result);
|
|
138
|
+
const catalogReason = formatCatalogFailureReason(result, filePath);
|
|
139
|
+
if (catalogReason) return withDebug(catalogReason);
|
|
137
140
|
if (key === 'missingBaseFile' || code === 'missing_base_file') {
|
|
138
141
|
const target = filePath || tx('this file', '这个文件');
|
|
139
142
|
return withDebug(tx(
|
|
@@ -236,82 +239,52 @@
|
|
|
236
239
|
return parts.join(', ');
|
|
237
240
|
}
|
|
238
241
|
|
|
242
|
+
function formatCatalogFailureReason(result, path) {
|
|
243
|
+
if (!FailureReasons?.formatFailureReasonForDisplay) return '';
|
|
244
|
+
// Compact historical settlements can omit canonical validation fields.
|
|
245
|
+
// Their display is still recoverable without re-normalizing stored facts.
|
|
246
|
+
const failure = result.failure && typeof result.failure === 'object'
|
|
247
|
+
? { ...result.failure, file: result.failure.file || result.file || path }
|
|
248
|
+
: FailureReasons.normalizeFailureReason(result, { path: result.file || path });
|
|
249
|
+
const display = FailureReasons.formatFailureReasonForDisplay(failure, getLocale(), failureReasonI18nLookup);
|
|
250
|
+
return display.localized ? display.message : '';
|
|
251
|
+
}
|
|
252
|
+
|
|
239
253
|
function formatBridgeResultReason(result = {}, fallbackPath = '') {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return localizeVisibleReason(result.reason || result.error || result.code || tr('unknownReason'));
|
|
244
|
-
}
|
|
245
|
-
if (code === 'missing_tracked_changes') {
|
|
246
|
-
return 'No matching Overleaf tracked-change records were found for this run, so Codex did not undo with text patches.';
|
|
247
|
-
}
|
|
248
|
-
if (code === 'tracked_change_file_open_failed') {
|
|
249
|
-
return path
|
|
250
|
-
? `Could not open ${path} to find this run's tracked changes. Handle them manually in the Overleaf review tools.`
|
|
251
|
-
: 'Could not open the file to find this run\'s tracked changes. Handle them manually in the Overleaf review tools.';
|
|
252
|
-
}
|
|
253
|
-
if (code === 'tracked_change_not_found') {
|
|
254
|
-
return 'No matching tracked changes were found on the Overleaf page for this run.';
|
|
255
|
-
}
|
|
256
|
-
if (code === 'tracked_change_reject_control_not_found') {
|
|
257
|
-
return path
|
|
258
|
-
? `Some tracked changes in ${path} remain, but the matching Reject button was not found. Reject them manually in Overleaf.`
|
|
259
|
-
: 'Some tracked changes remain, but the matching Reject button was not found. Reject them manually in Overleaf.';
|
|
260
|
-
}
|
|
261
|
-
if (code === 'tracked_change_reject_not_confirmed') {
|
|
262
|
-
return 'Codex clicked Reject, but Overleaf still shows this tracked change. Reject it manually in the Overleaf review panel.';
|
|
263
|
-
}
|
|
264
|
-
if (code === 'tracked_change_accept_control_not_found') {
|
|
265
|
-
return path
|
|
266
|
-
? `Some tracked changes in ${path} remain, but the matching Accept button was not found. Accept them manually in Overleaf.`
|
|
267
|
-
: 'Some tracked changes remain, but the matching Accept button was not found. Accept them manually in Overleaf.';
|
|
268
|
-
}
|
|
269
|
-
if (code === 'tracked_change_accept_not_confirmed') {
|
|
270
|
-
return 'Codex clicked Accept, but Overleaf still shows this tracked change. Accept it manually in the Overleaf review panel.';
|
|
271
|
-
}
|
|
272
|
-
if (code === 'tracked_change_editor_undo_open_failed') {
|
|
273
|
-
return path ? `Could not open ${path} for Overleaf native undo.` : 'Could not open the file for Overleaf native undo.';
|
|
274
|
-
}
|
|
275
|
-
if (code === 'tracked_change_editor_undo_current_mismatch') {
|
|
276
|
-
return path
|
|
277
|
-
? `${path} no longer matches this run's written content, so Codex did not use Overleaf native undo.`
|
|
278
|
-
: 'The current content no longer matches this run\'s written content, so Codex did not use Overleaf native undo.';
|
|
279
|
-
}
|
|
280
|
-
if (code === 'editor_undo_control_not_found') {
|
|
281
|
-
return path
|
|
282
|
-
? `Could not find Overleaf's editor Undo button to undo this run in ${path}.`
|
|
283
|
-
: 'Could not find Overleaf\'s editor Undo button to undo this run.';
|
|
284
|
-
}
|
|
285
|
-
if (code === 'editor_undo_no_progress') {
|
|
286
|
-
return path
|
|
287
|
-
? `Codex clicked Overleaf Undo, but ${path} did not change.`
|
|
288
|
-
: 'Codex clicked Overleaf Undo, but the file did not change.';
|
|
289
|
-
}
|
|
290
|
-
if (code === 'editor_undo_max_iterations') {
|
|
291
|
-
return path
|
|
292
|
-
? `${path} did not return to its pre-run content after repeated Overleaf native undo steps. Codex stopped to avoid undoing other edits.`
|
|
293
|
-
: 'The file did not return to its pre-run content after repeated Overleaf native undo steps. Codex stopped to avoid undoing other edits.';
|
|
294
|
-
}
|
|
295
|
-
if (code === 'tracked_change_undo_max_iterations') {
|
|
296
|
-
return path
|
|
297
|
-
? `${path} still has tracked changes left. Codex stopped to avoid rejecting unrelated edits.`
|
|
298
|
-
: 'Tracked changes are still left. Codex stopped to avoid rejecting unrelated edits.';
|
|
299
|
-
}
|
|
300
|
-
if (code === 'tracked_change_undo_verify_open_failed') {
|
|
301
|
-
return path
|
|
302
|
-
? `Could not open ${path} after undo to verify content. Reload Overleaf and check manually.`
|
|
303
|
-
: 'Could not open the file after undo to verify content. Reload Overleaf and check manually.';
|
|
304
|
-
}
|
|
305
|
-
if (code === 'tracked_change_undo_verify_failed') {
|
|
306
|
-
return path
|
|
307
|
-
? `${path} did not return to its pre-run content after rejecting tracked changes. Check this run's changes in the Overleaf review panel.`
|
|
308
|
-
: 'The file did not return to its pre-run content after rejecting tracked changes. Check this run\'s changes in the Overleaf review panel.';
|
|
309
|
-
}
|
|
310
|
-
return localizeVisibleReason(result.reason || result.error || result.code || tr('unknownReason'));
|
|
254
|
+
const path = result.file || result.path || fallbackPath || '';
|
|
255
|
+
return formatCatalogFailureReason(result, path)
|
|
256
|
+
|| localizeVisibleReason(result.reason || result.error || result.code || tr('unknownReason'));
|
|
311
257
|
}
|
|
312
258
|
|
|
259
|
+
const runtimeOwnedReasonPairs = [
|
|
260
|
+
['The selected provider no longer exists.', '所选模型服务已不存在。'],
|
|
261
|
+
['The selected provider has no usable revision.', '所选模型服务没有可用的配置版本。'],
|
|
262
|
+
['The selected provider revision was not captured for this run.', '本轮任务未记录所选模型服务的配置版本。'],
|
|
263
|
+
['The selected provider changed after this run was submitted.', '本轮任务提交后,所选模型服务的配置已发生变化。'],
|
|
264
|
+
['The captured selection is invalid. Attach it again.', '已记录的选区无效,请重新添加。'],
|
|
265
|
+
['Provider revision must be a finite non-negative integer.', '模型服务配置版本必须是有效的非负整数。'],
|
|
266
|
+
['A stable matching Overleaf account is required for shared history.', '共享历史需要稳定且匹配的 Overleaf 账号。'],
|
|
267
|
+
['The background storage connection was lost.', '后台存储连接已断开。'],
|
|
268
|
+
['Shared history is unavailable. Reload the extension and this page.', '共享历史不可用,请重新加载扩展和当前页面。'],
|
|
269
|
+
['The shared history lock disconnected.', '共享历史的存储锁连接已断开。'],
|
|
270
|
+
['Another page is saving history. Retry after it finishes.', '其他页面正在保存历史,请待保存完成后重试。'],
|
|
271
|
+
['The shared history lock was lost before completion.', '操作完成前,共享历史的存储锁已丢失。'],
|
|
272
|
+
['A stable account scope is required for durable persistence.', '持久保存需要稳定的账号范围。'],
|
|
273
|
+
['A cross-realm browser lock is required for durable persistence.', '持久保存需要能够跨页面协调的浏览器锁。'],
|
|
274
|
+
['Shared session storage is unavailable.', '共享会话存储不可用。'],
|
|
275
|
+
['Native host request failed.', '本地主机请求失败。'],
|
|
276
|
+
['Native host disconnected', '本地主机连接已断开。']
|
|
277
|
+
];
|
|
278
|
+
|
|
313
279
|
function localizeVisibleReason(reason) {
|
|
314
280
|
const text = String(reason || '').trim();
|
|
281
|
+
for (const [en, zh] of runtimeOwnedReasonPairs) {
|
|
282
|
+
if (text === en || text === zh) return tx(en, zh);
|
|
283
|
+
}
|
|
284
|
+
const scoped = /^(?:Scoped persistence failed: |限定范围的持久保存失败:)(invalid_commit|stale_view|revision_conflict|queue_revision_conflict|invalid_queue_mutation|queue_claim_conflict)$/.exec(text);
|
|
285
|
+
if (scoped) return tx(`Scoped persistence failed: ${scoped[1]}`, `限定范围的持久保存失败:${scoped[1]}`);
|
|
286
|
+
const localized = FailureReasons?.localizeFailureMessage?.(text, getLocale(), failureReasonI18nLookup);
|
|
287
|
+
if (localized && localized !== text) return localized;
|
|
315
288
|
if (!text || getLocale() !== 'en') {
|
|
316
289
|
return text;
|
|
317
290
|
}
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
const deadlineAt = Number.isFinite(input.deadlineAt) && input.deadlineAt > 0 ? input.deadlineAt : null;
|
|
18
18
|
const invalidDeadline = input.deadlineAt !== undefined && deadlineAt === null;
|
|
19
19
|
const expired = () => invalidDeadline || (deadlineAt !== null && now() >= deadlineAt);
|
|
20
|
-
let textBatch = [], blocked = false;
|
|
20
|
+
let textBatch = [], blocked = false, cancelled = false, halted = false;
|
|
21
21
|
function skipUndispatched(operations) {
|
|
22
22
|
const result = normalizeTextResult({ ok: false,
|
|
23
|
-
code: blocked ? 'writeback_tail_not_started' : 'writeback_deadline_exceeded',
|
|
24
|
-
error:
|
|
23
|
+
code: cancelled ? 'codex_cancelled' : blocked ? 'writeback_tail_not_started' : 'writeback_deadline_exceeded',
|
|
24
|
+
error: cancelled ? 'The run was cancelled. This operation was not dispatched.'
|
|
25
|
+
: halted ? 'An earlier writeback stopped this file plan. This operation was not dispatched.'
|
|
26
|
+
: blocked ? 'An earlier writeback is unconfirmed. This operation was not dispatched.'
|
|
25
27
|
: invalidDeadline ? 'The writeback deadline is invalid. This operation was not dispatched.'
|
|
26
28
|
: 'The writeback deadline has elapsed. This operation was not dispatched.',
|
|
27
29
|
changedDocument: false }, operations);
|
|
@@ -32,26 +34,38 @@
|
|
|
32
34
|
if (!textBatch.length) return;
|
|
33
35
|
const operations = textBatch;
|
|
34
36
|
textBatch = [];
|
|
35
|
-
if (blocked || expired()) { skipUndispatched(operations); return; }
|
|
36
|
-
|
|
37
|
+
if (blocked || cancelled || expired()) { skipUndispatched(operations); return; }
|
|
38
|
+
let response;
|
|
39
|
+
try { response = await callPageBridge('applyOperations', {
|
|
37
40
|
operations,
|
|
38
41
|
baseFiles: input.baseFiles || [],
|
|
39
42
|
reviewingPolicy: input.reviewingPolicy || '',
|
|
43
|
+
...(input.reviewOwnerToken ? { reviewOwnerToken: input.reviewOwnerToken } : {}),
|
|
40
44
|
...(deadlineAt !== null ? { deadlineAt } : {}),
|
|
41
45
|
requireReviewing: input.requireReviewing === true,
|
|
42
46
|
requireEditing: input.requireEditing === true,
|
|
43
47
|
retryCreates: input.retryCreates === true,
|
|
44
48
|
runProjectId: input.runProjectId || ''
|
|
45
|
-
});
|
|
46
|
-
|
|
49
|
+
}); } catch (error) {
|
|
50
|
+
if (error?.code !== 'codex_cancelled' && error?.cancelled !== true) throw error;
|
|
51
|
+
cancelled = true;
|
|
52
|
+
// A later cancelled batch must not discard earlier write/Undo evidence.
|
|
53
|
+
if (!error.requestId && !results.length) throw error;
|
|
54
|
+
response = { ok: false, code: 'codex_cancelled', error: error.message,
|
|
55
|
+
requestId: error.requestId, changedDocument: error.changedDocument !== false };
|
|
56
|
+
}
|
|
57
|
+
const recovered = ['page_bridge_timeout', 'codex_cancelled'].includes(response?.code) && response.requestId
|
|
47
58
|
? await recoverTextReceipt(response, operations, input.runProjectId || '', deadlineAt)
|
|
48
59
|
: response;
|
|
49
60
|
const result = normalizeTextResult(recovered, operations);
|
|
61
|
+
if (response?.code === 'codex_cancelled' && response.changedDocument === false) result.receiptUnconfirmed = false;
|
|
50
62
|
results.push(result);
|
|
51
|
-
|
|
63
|
+
cancelled ||= hasCancellation(result);
|
|
64
|
+
halted ||= result.haltRemainingOperations === true;
|
|
65
|
+
blocked = halted || result.receiptUnconfirmed === true;
|
|
52
66
|
}
|
|
53
67
|
for (const operation of Array.isArray(input.operations) ? input.operations : []) {
|
|
54
|
-
if (blocked || expired()) {
|
|
68
|
+
if (blocked || cancelled || expired()) {
|
|
55
69
|
skipUndispatched([operation]);
|
|
56
70
|
continue;
|
|
57
71
|
}
|
|
@@ -60,12 +74,13 @@
|
|
|
60
74
|
continue;
|
|
61
75
|
}
|
|
62
76
|
await flushTextBatch();
|
|
63
|
-
if (blocked || expired()) {
|
|
77
|
+
if (blocked || cancelled || expired()) {
|
|
64
78
|
skipUndispatched([operation]);
|
|
65
79
|
continue;
|
|
66
80
|
}
|
|
67
81
|
const binaryResult = await applyBinaryOperation(operation, input);
|
|
68
82
|
results.push(binaryResult);
|
|
83
|
+
cancelled ||= hasCancellation(binaryResult);
|
|
69
84
|
blocked = binaryResult.receiptUnconfirmed === true;
|
|
70
85
|
}
|
|
71
86
|
await flushTextBatch();
|
|
@@ -84,14 +99,16 @@
|
|
|
84
99
|
receipt = await callPageBridge('getWritebackReceipt', { requestId: timeout.requestId, runProjectId,
|
|
85
100
|
...(deadlineAt !== null ? { deadlineAt } : {}) });
|
|
86
101
|
} catch (error) {
|
|
87
|
-
if (error?.code === 'codex_cancelled' || error?.cancelled === true)
|
|
102
|
+
if (error?.code === 'codex_cancelled' || error?.cancelled === true) {
|
|
103
|
+
recoveryError = error.message || 'Receipt settlement was cancelled.'; break;
|
|
104
|
+
}
|
|
88
105
|
recoveryError = error?.message || String(error);
|
|
89
106
|
if (!isTransientReceiptFailure(error)) break;
|
|
90
107
|
transportFailed = true;
|
|
91
108
|
}
|
|
92
109
|
if (!transportFailed) {
|
|
93
110
|
if (receipt?.code === 'codex_cancelled' || receipt?.cancelled === true) {
|
|
94
|
-
|
|
111
|
+
recoveryError = receipt.error || 'Receipt settlement was cancelled.'; break;
|
|
95
112
|
}
|
|
96
113
|
if (receipt?.ok !== true) {
|
|
97
114
|
recoveryError = receipt?.error?.message || receipt?.error || receipt?.reason
|
|
@@ -130,8 +147,9 @@
|
|
|
130
147
|
const transferId = `asset_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
131
148
|
const assetRef = operation.assetRef || null;
|
|
132
149
|
const safeOperation = sanitizeOperation(operation);
|
|
133
|
-
let pageBegun = false, commitAttempted = false;
|
|
150
|
+
let beginAttempted = false, pageBegun = false, commitAttempted = false;
|
|
134
151
|
try {
|
|
152
|
+
beginAttempted = true;
|
|
135
153
|
const begin = await callPageBridge('binaryUploadBegin', {
|
|
136
154
|
transferId,
|
|
137
155
|
path: operation.path,
|
|
@@ -139,6 +157,7 @@
|
|
|
139
157
|
sha256: assetRef?.sha256 || operation.sha256 || '',
|
|
140
158
|
mimeType: assetRef?.mimeType || operation.mimeType || 'application/octet-stream',
|
|
141
159
|
overwrite: operation.type === 'overwrite-binary',
|
|
160
|
+
previousSha256: operation.type === 'overwrite-binary' ? await binaryBaselineHash(operation, input.baseFiles) : undefined,
|
|
142
161
|
runProjectId: input.runProjectId || ''
|
|
143
162
|
});
|
|
144
163
|
if (!begin?.ok) throw responseError(begin, 'binary_upload_begin_failed');
|
|
@@ -164,6 +183,8 @@
|
|
|
164
183
|
if (!committed?.ok) throw responseError(committed, committed?.code || 'binary_upload_failed');
|
|
165
184
|
return { ok: true, applied: [{ operation: safeOperation, result: committed }], skipped: [] };
|
|
166
185
|
} catch (error) {
|
|
186
|
+
// A commit rejected before dispatch still leaves staged bytes to clean up.
|
|
187
|
+
if (error.requestDispatched === false) commitAttempted = false;
|
|
167
188
|
let changedDocument = error?.changedDocument === true
|
|
168
189
|
|| (commitAttempted && error?.changedDocument !== false);
|
|
169
190
|
if (commitAttempted && changedDocument) {
|
|
@@ -176,6 +197,7 @@
|
|
|
176
197
|
|| receipt.runProjectId !== input.runProjectId) break;
|
|
177
198
|
if (receipt.result?.ok === true) {
|
|
178
199
|
return { ok: true, applied: [{ operation: safeOperation, result: receipt.result }], skipped: [],
|
|
200
|
+
cancelled: error.code === 'codex_cancelled' || error.cancelled === true,
|
|
179
201
|
receiptRecovery: { transferId, recovered: true, initialError: error.message } };
|
|
180
202
|
}
|
|
181
203
|
if (receipt.state === 'completed' && receipt.changedDocument === false) {
|
|
@@ -186,7 +208,8 @@
|
|
|
186
208
|
await delay(Math.min(500, Math.max(1, deadline - now())));
|
|
187
209
|
} while (now() <= deadline);
|
|
188
210
|
}
|
|
189
|
-
|
|
211
|
+
const uncertainBegin = beginAttempted && error.code === 'codex_cancelled' && error.requestDispatched !== false;
|
|
212
|
+
if (!commitAttempted && (pageBegun || uncertainBegin)) {
|
|
190
213
|
await callPageBridge('binaryUploadAbort', { transferId, runProjectId: input.runProjectId || '' }).catch(() => null);
|
|
191
214
|
}
|
|
192
215
|
const code = error.code || 'binary_upload_failed';
|
|
@@ -203,6 +226,9 @@
|
|
|
203
226
|
ok: false,
|
|
204
227
|
code,
|
|
205
228
|
reason: error.message || 'Binary asset upload failed.',
|
|
229
|
+
...(Array.isArray(error.createdDirectories) ? { createdDirectories: error.createdDirectories,
|
|
230
|
+
folderChanged: error.folderChanged === true, fileUploadAttempted: error.fileUploadAttempted === true,
|
|
231
|
+
directoryCleanupRequired: error.createdDirectories.length > 0 } : {}),
|
|
206
232
|
changedDocument,
|
|
207
233
|
failure: { code, stage: 'write', file: operation.path, changedDocument }
|
|
208
234
|
}
|
|
@@ -249,6 +275,7 @@
|
|
|
249
275
|
const result = response && typeof response === 'object' ? { ...response } : { ok: false };
|
|
250
276
|
result.applied = Array.isArray(result.applied) ? result.applied : [];
|
|
251
277
|
result.skipped = Array.isArray(result.skipped) ? result.skipped.slice() : [];
|
|
278
|
+
if (result.skipped.some(entry => entry.result?.receiptUnconfirmed === true)) result.receiptUnconfirmed = true;
|
|
252
279
|
const key = operation => [operation?.type, operation?.path, operation?.to || ''].join('\0');
|
|
253
280
|
const known = new Map();
|
|
254
281
|
for (const entry of [...result.applied, ...result.skipped]) {
|
|
@@ -286,6 +313,7 @@
|
|
|
286
313
|
for (const result of results) {
|
|
287
314
|
merged.applied.push(...(Array.isArray(result?.applied) ? result.applied : []));
|
|
288
315
|
merged.skipped.push(...(Array.isArray(result?.skipped) ? result.skipped : []));
|
|
316
|
+
if (result?.haltRemainingOperations) merged.haltRemainingOperations = true;
|
|
289
317
|
// Text writeback returns the Reviewing references captured by the page
|
|
290
318
|
// bridge. They are lifecycle data, not diagnostics: dropping them makes
|
|
291
319
|
// recordUndoFromApply classify a successful Track write as legacy Undo,
|
|
@@ -322,6 +350,31 @@
|
|
|
322
350
|
return safe;
|
|
323
351
|
}
|
|
324
352
|
|
|
353
|
+
function hasCancellation(result) {
|
|
354
|
+
return result?.cancelled === true || result?.code === 'codex_cancelled'
|
|
355
|
+
|| result?.skipped?.some(entry => entry.result?.code === 'codex_cancelled');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function binaryBaselineHash(operation, baseFiles = []) {
|
|
359
|
+
const valid = value => typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
|
|
360
|
+
const base = (baseFiles || []).find(file => file?.path === operation.path);
|
|
361
|
+
let captured = valid(base?.sha256) ? base.sha256.toLowerCase() : '';
|
|
362
|
+
if (typeof base?.contentBase64 === 'string' && globalThis.crypto?.subtle && typeof atob === 'function') {
|
|
363
|
+
const bytes = Uint8Array.from(atob(base.contentBase64), value => value.charCodeAt(0));
|
|
364
|
+
const hash = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
|
365
|
+
const digest = Array.from(new Uint8Array(hash), value => value.toString(16).padStart(2, '0')).join('');
|
|
366
|
+
if (captured && captured !== digest) throw codedError('binary_asset_baseline_conflict', 'The captured binary baseline has conflicting content and hash.');
|
|
367
|
+
captured = digest;
|
|
368
|
+
}
|
|
369
|
+
if (operation.previousSha256 != null) {
|
|
370
|
+
if (!valid(operation.previousSha256) || (captured && captured !== operation.previousSha256.toLowerCase())) {
|
|
371
|
+
throw codedError('binary_asset_baseline_conflict', 'The Native and browser binary baselines differ. Read the project again before replacement.');
|
|
372
|
+
}
|
|
373
|
+
return operation.previousSha256.toLowerCase();
|
|
374
|
+
}
|
|
375
|
+
return captured;
|
|
376
|
+
}
|
|
377
|
+
|
|
325
378
|
function isTransientReceiptFailure(value) {
|
|
326
379
|
const code = value?.code || value?.error?.code;
|
|
327
380
|
if (code) return ['page_bridge_timeout', 'page_bridge_unavailable', 'page_bridge_transport_error',
|
|
@@ -337,6 +390,11 @@
|
|
|
337
390
|
response?.reason || response?.error?.message || response?.error || 'Asset transfer failed.'
|
|
338
391
|
);
|
|
339
392
|
if (typeof response?.changedDocument === 'boolean') error.changedDocument = response.changedDocument;
|
|
393
|
+
if (Array.isArray(response?.createdDirectories)) {
|
|
394
|
+
error.createdDirectories = response.createdDirectories;
|
|
395
|
+
error.folderChanged = response.folderChanged === true;
|
|
396
|
+
error.fileUploadAttempted = response.fileUploadAttempted === true;
|
|
397
|
+
}
|
|
340
398
|
error.stage = response?.failure?.stage || 'write';
|
|
341
399
|
return error;
|
|
342
400
|
}
|