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.
Files changed (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -0,0 +1,269 @@
1
+ 'use strict';
2
+
3
+ // Coordination for the one compatible 2.5.0 -> 2.5.1 Bootstrap revision.
4
+ // All mutations run under updateManager's existing cross-process lock.
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const crypto = require('node:crypto');
8
+ const vm = require('node:vm');
9
+ const { spawn } = require('node:child_process');
10
+ const Files = require('./bootstrapMigration');
11
+ const Contract = require('./bootstrapMigrationContract');
12
+ const { updateError } = require('./updateTrust');
13
+
14
+ const BOOTSTRAP_DEADLINE_MS = 120000;
15
+ const MAX_AUTOMATIC_ROLLBACK_ATTEMPTS = 3;
16
+ const BOOTSTRAP_STATES = new Set(['bootstrap_pending', 'bootstrap_rolling_back']);
17
+ const backupRoot = context => path.join(context.updatesRoot, 'bootstrap-migrations');
18
+ const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
19
+
20
+ function migrationHint(context, journal) {
21
+ if (!journal || !['awaiting_health', ...BOOTSTRAP_STATES].includes(journal.state)) return null;
22
+ if (journal.bootstrapMigration) return journal.bootstrapMigration;
23
+ if (journal.sourceVersion !== '2.5.0' || journal.targetVersion !== '2.5.1') return null;
24
+ // This exact release pair requires migration even when discovery data was
25
+ // lost. Missing evidence must not downgrade an update into a false success.
26
+ return { phase: 'pending', targetRevision: '3.1' };
27
+ }
28
+
29
+ function bindBootstrapMigration(context, journal, deps) {
30
+ if (journal?.state !== 'awaiting_health' || journal.bootstrapMigration || !migrationHint(context, journal)) return journal;
31
+ const candidate = readJson(path.join(context.updatesRoot, 'candidate.json'));
32
+ let verificationError = '';
33
+ try { deps.assertAuthorization(journal); verifiedDescriptor(journal, candidate, deps); }
34
+ catch (error) { verificationError = error.code || 'update_bootstrap_manifest_mismatch'; }
35
+ const next = { ...journal, state: 'bootstrap_pending', bootstrapMigration: {
36
+ phase: 'pending', targetRevision: '3.1', reloadCount: 0, verificationError,
37
+ manifestBase64: candidate?.manifestBase64 || '', signatureBase64: candidate?.signatureBase64 || ''
38
+ }, updatedAt: new Date().toISOString() };
39
+ deps.writeJournal(next);
40
+ return next;
41
+ }
42
+
43
+ function verifiedDescriptor(journal, evidence, deps) {
44
+ const bytes = Buffer.from(evidence?.manifestBase64 || '', 'base64');
45
+ if (!journal.manifestSha256 || hash(bytes) !== journal.manifestSha256) {
46
+ throw updateError('update_bootstrap_manifest_mismatch', 'Bootstrap migration does not match the applied release.');
47
+ }
48
+ const manifest = deps.verifyManifest(bytes, Buffer.from(evidence?.signatureBase64 || '', 'base64'));
49
+ if (manifest.version !== journal.targetVersion || manifest.updateBundle?.sha256 !== journal.bundleSha256
50
+ || manifest.updateBundle?.size !== journal.bundleSize) {
51
+ throw updateError('update_bootstrap_manifest_mismatch', 'Bootstrap migration does not match the update archive.');
52
+ }
53
+ const descriptor = Contract.validateBootstrapMigrationDescriptor(manifest.bootstrapMigration, { targetVersion: journal.targetVersion });
54
+ if (!descriptor.sourceVersions.includes(journal.sourceVersion)) {
55
+ throw updateError('update_bootstrap_source_version_mismatch', 'This installation has no compatible Bootstrap migration.');
56
+ }
57
+ return descriptor;
58
+ }
59
+
60
+ function payloadRootFor(context, journal) {
61
+ return path.join(context.nativeRoot, 'versions', journal.targetVersion, 'native-host', 'bootstrap-payload');
62
+ }
63
+
64
+ function loadPlan(context, journal) {
65
+ const root = backupRoot(context);
66
+ if (!fs.existsSync(path.join(root, journal.id, 'state.json'))) return null;
67
+ const plan = Files.loadBootstrapMigration({ backupRoot: root, transactionId: journal.id });
68
+ if (plan.extensionRoot !== context.extensionRoot || plan.payloadRoot !== payloadRootFor(context, journal)
69
+ || plan.sourceVersion !== journal.sourceVersion || plan.targetVersion !== journal.targetVersion) {
70
+ throw updateError('update_bootstrap_transaction_mismatch', 'Bootstrap recovery belongs to a different installation.');
71
+ }
72
+ return plan;
73
+ }
74
+
75
+ async function advanceBootstrapMigration(context, journal, params, deps, options = {}) {
76
+ if (!journal || !migrationHint(context, journal)) return { state: 'not_required' };
77
+ if (params.transactionId !== journal.id) throw updateError('update_transaction_mismatch', 'Bootstrap update transaction changed.');
78
+ journal = bindBootstrapMigration(context, journal, deps);
79
+ if (journal.state !== 'bootstrap_pending') {
80
+ throw updateError('update_transaction_state_changed', 'Bootstrap migration is no longer waiting to continue.');
81
+ }
82
+ if (deps.runningVersion !== journal.targetVersion) {
83
+ throw updateError('update_bootstrap_native_restart_required', 'The target Native Host must start before updating Bootstrap.');
84
+ }
85
+ deps.assertAuthorization(journal);
86
+ const descriptor = verifiedDescriptor(journal, journal.bootstrapMigration, deps);
87
+ const payloadRoot = payloadRootFor(context, journal);
88
+ Contract.assertBootstrapManifestShape(JSON.parse(fs.readFileSync(path.join(context.extensionRoot, 'manifest.json'), 'utf8')),
89
+ { version: journal.targetVersion });
90
+ Contract.assertBootstrapManifestShape(JSON.parse(fs.readFileSync(path.join(payloadRoot, 'manifest.template.json'), 'utf8')),
91
+ { version: journal.targetVersion });
92
+ // A syntax failure must be found before changing the only background entry.
93
+ for (const file of descriptor.files) {
94
+ const bytes = fs.readFileSync(path.join(payloadRoot, file.path));
95
+ if (hash(bytes) !== file.targetSha256) throw updateError('update_bootstrap_payload_mismatch', 'Bootstrap payload changed after verification.');
96
+ if (file.path.endsWith('.js')) new vm.Script(bytes.toString('utf8'), { filename: file.path });
97
+ }
98
+ deps.verifyArchive(journal);
99
+ let plan = loadPlan(context, journal);
100
+ if (!plan) plan = Files.prepareBootstrapMigration({
101
+ descriptor, payloadRoot, extensionRoot: context.extensionRoot, backupRoot: backupRoot(context),
102
+ transactionId: journal.id, sourceVersion: journal.sourceVersion, targetVersion: journal.targetVersion
103
+ });
104
+ if (plan.descriptorSha256 !== hash(JSON.stringify(descriptor))) {
105
+ throw updateError('update_bootstrap_manifest_mismatch', 'Bootstrap recovery material does not match the signed release.');
106
+ }
107
+ let migration = { ...journal.bootstrapMigration, phase: plan.state === 'applied' ? 'applied' : 'prepared',
108
+ targetDigest: plan.targetDigest, sourceDigest: plan.sourceDigest,
109
+ deadline: journal.bootstrapMigration.deadline || Date.now() + BOOTSTRAP_DEADLINE_MS };
110
+ if (Date.now() >= migration.deadline) throw updateError('update_bootstrap_health_timeout', 'Bootstrap update did not finish within its health window.');
111
+ journal = { ...journal, bootstrapMigration: migration };
112
+ deps.writeJournal(journal);
113
+ // A new connection always establishes a new monitor handshake. PID reuse
114
+ // alone can never authorize writing the only browser background entry.
115
+ {
116
+ const token = crypto.randomUUID();
117
+ migration = { ...migration, watchdogToken: token, watchdogPid: null };
118
+ journal = { ...journal, bootstrapMigration: migration };
119
+ deps.writeJournal(journal);
120
+ const pid = await (options.startWatchdog || startWatchdog)(context, journal, token);
121
+ migration = { ...migration, watchdogPid: pid };
122
+ journal = { ...journal, bootstrapMigration: migration };
123
+ deps.writeJournal(journal);
124
+ }
125
+ Files.applyBootstrapMigration(plan, options.fileHooks);
126
+ const observed = Files.verifyBootstrapMigration(plan);
127
+ migration = { ...migration, phase: 'applied', targetDigest: observed.digest };
128
+ if (params.bootstrapRevision === descriptor.targetRevision) {
129
+ migration = { ...migration, phase: 'loaded', loadedAt: new Date().toISOString() };
130
+ deps.writeJournal({ ...journal, state: 'awaiting_health', bootstrapMigration: migration });
131
+ return { state: 'awaiting_health', transactionId: journal.id, targetVersion: journal.targetVersion,
132
+ bootstrapRevision: descriptor.targetRevision };
133
+ }
134
+ if (migration.reloadCount >= 2) throw updateError('update_bootstrap_reload_failed', 'The new Bootstrap did not load after extension restart.');
135
+ migration.reloadCount += 1;
136
+ deps.writeJournal({ ...journal, bootstrapMigration: migration });
137
+ return { state: 'reload_required', transactionId: journal.id, targetVersion: journal.targetVersion,
138
+ bootstrapRevision: descriptor.targetRevision };
139
+ }
140
+
141
+ function assertBootstrapConfirmation(context, journal, params) {
142
+ if (!journal.bootstrapMigration) {
143
+ if (migrationHint(context, journal)) throw updateError('update_bootstrap_confirmation_pending', 'Bootstrap migration is still pending.');
144
+ return;
145
+ }
146
+ if (journal.bootstrapMigration.phase !== 'loaded' || params.bootstrapRevision !== journal.bootstrapMigration.targetRevision) {
147
+ throw updateError('update_bootstrap_confirmation_pending', 'The loaded Bootstrap did not confirm the target revision.');
148
+ }
149
+ const plan = loadPlan(context, journal);
150
+ if (!plan || Files.verifyBootstrapMigration(plan).digest !== journal.bootstrapMigration.targetDigest) {
151
+ throw updateError('update_bootstrap_target_mismatch', 'The confirmed Bootstrap bytes no longer match the target.');
152
+ }
153
+ }
154
+
155
+ function restoreBootstrap(context, journal) {
156
+ const plan = loadPlan(context, journal);
157
+ if (!plan) {
158
+ if (journal.bootstrapMigration && journal.bootstrapMigration.phase !== 'pending') {
159
+ throw updateError('update_bootstrap_record_missing', 'Bootstrap recovery material is missing.');
160
+ }
161
+ if (journal.bootstrapMigration) return untouchedBootstrap(context, journal);
162
+ }
163
+ return plan ? Files.restoreBootstrapMigration(plan) : null;
164
+ }
165
+
166
+ function bootstrapEvidence(context, journal, expected) {
167
+ const plan = loadPlan(context, journal);
168
+ if (!plan && journal.bootstrapMigration) {
169
+ if (expected !== 'source' || !(journal.bootstrapMigration.phase === 'pending'
170
+ || journal.bootstrapMigration.phase === 'restored' && journal.bootstrapMigration.sourceUnchanged === true)) {
171
+ throw updateError('update_bootstrap_record_missing', 'Bootstrap recovery material is missing.');
172
+ }
173
+ return untouchedBootstrap(context, journal);
174
+ }
175
+ return plan ? Files.verifyBootstrapMigration(plan, { expected }) : null;
176
+ }
177
+
178
+ // Without a file plan this transaction never wrote Bootstrap, so recovery only
179
+ // leaves it in place. Published source bytes keep the baseline digest; any other
180
+ // installed bytes (an earlier release candidate or local build) are recorded as
181
+ // observed. Later reads compare the same digest, so changes still block.
182
+ function untouchedBootstrap(context, journal) {
183
+ let knownSource = true;
184
+ const entries = Object.entries(Contract.BOOTSTRAP_MIGRATION_BASELINE).sort((a, b) => a[0].localeCompare(b[0]))
185
+ .map(([relative, expected]) => {
186
+ const actual = readBootstrapFileHash(context, relative);
187
+ if (actual !== expected) knownSource = false;
188
+ return [relative, actual];
189
+ });
190
+ return { transactionId: journal.id, migrationId: 'bootstrap-v3.1-from-2.5.0',
191
+ state: 'restored', expected: 'source', digest: hash(JSON.stringify(entries)), unmodified: true, knownSource };
192
+ }
193
+
194
+ function readBootstrapFileHash(context, relative) {
195
+ const filename = path.join(context.extensionRoot, relative);
196
+ if (!fs.existsSync(filename)) return null;
197
+ if (!fs.lstatSync(filename).isFile()) {
198
+ throw updateError('update_bootstrap_source_mismatch', 'Bootstrap source bytes cannot be confirmed.');
199
+ }
200
+ return hash(fs.readFileSync(filename));
201
+ }
202
+
203
+ // Runs before staging and in the update check. A signed migration starts only
204
+ // from the exact source bytes it names; other installations need a full
205
+ // reinstall instead of a transaction that cannot finish. This reads only the
206
+ // descriptor's file list, so later migration contracts use the same check.
207
+ function assertMigrationSource(context, manifest, sourceVersion) {
208
+ const descriptor = manifest?.bootstrapMigration;
209
+ if (!descriptor || !Array.isArray(descriptor.sourceVersions) || !descriptor.sourceVersions.includes(sourceVersion)
210
+ || !Array.isArray(descriptor.files)) return;
211
+ for (const file of descriptor.files) {
212
+ const relative = String(file?.path || '');
213
+ if (!/^bootstrap\/[A-Za-z0-9._-]+$/.test(relative)) {
214
+ throw updateError('update_bootstrap_manifest_mismatch', 'Bootstrap migration file identity is invalid.');
215
+ }
216
+ if (readBootstrapFileHash(context, relative) !== (file.sourceSha256 ?? null)) {
217
+ throw updateError('update_bootstrap_source_mismatch',
218
+ 'The installed startup files do not match the published source of this update.');
219
+ }
220
+ }
221
+ }
222
+
223
+ function bootstrapExpired(journal, { recoveryRequested = false, context } = {}) {
224
+ const migration = journal?.bootstrapMigration;
225
+ if (!migration || !['bootstrap_pending', 'bootstrap_rolling_back', 'awaiting_health'].includes(journal.state)) return false;
226
+ // Automatic retries are bounded; an explicit update.rollback still runs.
227
+ if (journal.state === 'bootstrap_rolling_back') {
228
+ return Number(journal.rollbackFailure?.attempts || 0) < MAX_AUTOMATIC_ROLLBACK_ATTEMPTS;
229
+ }
230
+ return (migration.deadline && Date.now() >= migration.deadline)
231
+ || (recoveryRequested && migration.phase !== 'pending' && !watchdogAlive(migration, context, journal));
232
+ }
233
+
234
+ function watchdogAlive(migration, context, journal) {
235
+ if (!Number.isInteger(migration?.watchdogPid) || migration.watchdogPid < 1) return false;
236
+ const heartbeat = context && journal && readJson(path.join(backupRoot(context), journal.id, 'watchdog.json'));
237
+ if (!heartbeat || heartbeat.token !== migration.watchdogToken || heartbeat.pid !== migration.watchdogPid
238
+ || heartbeat.transactionId !== journal.id || Date.now() - heartbeat.at > 2500 || heartbeat.at > Date.now() + 1000) return false;
239
+ try { process.kill(migration.watchdogPid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
240
+ }
241
+
242
+ function startWatchdog(context, journal, token) {
243
+ return new Promise((resolve, reject) => {
244
+ const child = spawn(process.execPath, [path.join(__dirname, 'bootstrapUpdateWatchdog.js'),
245
+ context.nativeRoot, context.extensionRoot, journal.id, token], {
246
+ env: { ...context.env }, detached: true, windowsHide: true, stdio: ['ignore', 'ignore', 'ignore', 'ipc']
247
+ });
248
+ const timer = setTimeout(() => fail(new Error('Bootstrap recovery monitor did not become ready.')), 5000);
249
+ let settled = false;
250
+ function fail(error) {
251
+ if (settled) return;
252
+ settled = true; clearTimeout(timer);
253
+ try { child.kill(); } catch (_) { /* A failed child may already be gone. */ }
254
+ reject(Object.assign(error, { code: 'update_bootstrap_monitor_unavailable' }));
255
+ }
256
+ child.on('error', fail);
257
+ child.on('exit', () => { if (!settled) fail(new Error('Bootstrap recovery monitor exited before readiness.')); });
258
+ child.on('message', message => {
259
+ if (settled || message?.type !== 'ready' || message.token !== token || message.pid !== child.pid) return;
260
+ settled = true; clearTimeout(timer); child.disconnect(); child.unref(); resolve(child.pid);
261
+ });
262
+ });
263
+ }
264
+
265
+ function readJson(filename) { try { return JSON.parse(fs.readFileSync(filename, 'utf8')); } catch (_) { return null; } }
266
+
267
+ module.exports = { BOOTSTRAP_DEADLINE_MS, BOOTSTRAP_STATES, MAX_AUTOMATIC_ROLLBACK_ATTEMPTS, migrationHint,
268
+ bindBootstrapMigration, advanceBootstrapMigration, assertBootstrapConfirmation, assertMigrationSource,
269
+ restoreBootstrap, bootstrapEvidence, bootstrapExpired, watchdogAlive };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ // Independent of the browser worker being replaced. The Native update lock
4
+ // serializes timeout recovery with normal health confirmation.
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const [nativeRoot, extensionRoot, transactionId, token] = process.argv.slice(2);
8
+ const { recoverBootstrapTransaction } = require('./updateManager');
9
+ if (typeof recoverBootstrapTransaction !== 'function') throw new Error('Bootstrap recovery entry is unavailable.');
10
+ const env = { ...process.env, CODEX_OVERLEAF_MANAGED: '1',
11
+ CODEX_OVERLEAF_MANAGED_NATIVE_ROOT: nativeRoot, CODEX_OVERLEAF_MANAGED_EXTENSION_ROOT: extensionRoot };
12
+ let busy = false;
13
+ let failures = 0;
14
+ function heartbeat() {
15
+ const directory = path.join(nativeRoot, 'updates', 'bootstrap-migrations', transactionId);
16
+ const temporary = path.join(directory, 'watchdog-' + process.pid + '.tmp');
17
+ fs.writeFileSync(temporary, JSON.stringify({ transactionId, token, pid: process.pid, at: Date.now() }), { mode: 0o600 });
18
+ fs.renameSync(temporary, path.join(directory, 'watchdog.json'));
19
+ }
20
+ const timer = setInterval(async () => {
21
+ if (busy) return;
22
+ busy = true;
23
+ try {
24
+ const journal = JSON.parse(fs.readFileSync(path.join(nativeRoot, 'updates', 'transaction.json'), 'utf8'));
25
+ if (journal.id !== transactionId || journal.bootstrapMigration?.watchdogToken !== token
26
+ || ['committed', 'superseded'].includes(journal.state)
27
+ || journal.state === 'rolled_back' && !journal.sourceActivationPending) {
28
+ clearInterval(timer); return;
29
+ }
30
+ heartbeat();
31
+ if (journal.sourceActivationPending || journal.bootstrapMigration?.deadline && Date.now() >= journal.bootstrapMigration.deadline) {
32
+ await recoverBootstrapTransaction({ env }, transactionId, token);
33
+ clearInterval(timer);
34
+ }
35
+ } catch (_) {
36
+ // Retry boundedly if a filesystem/lock is temporarily unavailable. Recovery
37
+ // material remains durable for the next Native start or popup recovery probe.
38
+ if (++failures >= 20) clearInterval(timer);
39
+ } finally { busy = false; }
40
+ }, 500);
41
+ const initial = JSON.parse(fs.readFileSync(path.join(nativeRoot, 'updates', 'transaction.json'), 'utf8'));
42
+ if (initial.id !== transactionId || initial.bootstrapMigration?.watchdogToken !== token) throw new Error('Bootstrap recovery identity changed.');
43
+ heartbeat();
44
+ process.send?.({ type: 'ready', token, pid: process.pid });
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('node:crypto');
4
+ const { providerError } = require('./providerProfile');
4
5
 
5
6
  const {
6
7
  applyReasoningControl,
@@ -58,7 +59,15 @@ function appendResponsesInput(messages, input, { replayReasoning = false, toolKi
58
59
  }
59
60
  if (!Array.isArray(input)) return;
60
61
  let pendingToolCalls = [];
62
+ let pendingToolOutputContent = [];
61
63
  let pendingReasoning = '';
64
+ const flushToolOutputContent = () => {
65
+ if (!pendingToolOutputContent.length) return;
66
+ // Chat tool messages accept text only. Attach images after the complete
67
+ // tool-result group so a user message cannot split parallel tool results.
68
+ messages.push({ role: 'user', content: pendingToolOutputContent });
69
+ pendingToolOutputContent = [];
70
+ };
62
71
  const flushToolCalls = () => {
63
72
  if (!pendingToolCalls.length) return;
64
73
  const assistant = {
@@ -73,6 +82,9 @@ function appendResponsesInput(messages, input, { replayReasoning = false, toolKi
73
82
  };
74
83
  for (const item of input) {
75
84
  if (!item || typeof item !== 'object') continue;
85
+ const isToolOutput = item.type === 'function_call_output' || item.type === 'custom_tool_call_output'
86
+ || item.type === 'computer_call_output' || item.type === 'tool_search_output';
87
+ if (!isToolOutput && item.type !== 'reasoning') flushToolOutputContent();
76
88
  if (item.type === 'reasoning') {
77
89
  flushToolCalls();
78
90
  pendingReasoning = extractReasoningText(item);
@@ -83,12 +95,22 @@ function appendResponsesInput(messages, input, { replayReasoning = false, toolKi
83
95
  continue;
84
96
  }
85
97
  flushToolCalls();
86
- if (item.type === 'function_call_output' || item.type === 'custom_tool_call_output' || item.type === 'computer_call_output' || item.type === 'tool_search_output') {
98
+ if (isToolOutput) {
99
+ const callId = normalizeText(item.call_id) || normalizeText(item.id);
100
+ const imageContent = translateImageToolOutput(item.output);
87
101
  messages.push({
88
102
  role: 'tool',
89
- tool_call_id: normalizeText(item.call_id) || normalizeText(item.id),
90
- content: normalizeToolOutput(item.output)
103
+ tool_call_id: callId,
104
+ content: imageContent
105
+ ? 'The tool output, including images, is attached after the tool results.'
106
+ : normalizeToolOutput(item.output)
91
107
  });
108
+ if (imageContent) {
109
+ pendingToolOutputContent.push(
110
+ { type: 'text', text: `Output from tool call ${callId}:` },
111
+ ...imageContent
112
+ );
113
+ }
92
114
  continue;
93
115
  }
94
116
  if (item.type === 'message' || item.role) {
@@ -105,6 +127,38 @@ function appendResponsesInput(messages, input, { replayReasoning = false, toolKi
105
127
  }
106
128
  }
107
129
  flushToolCalls();
130
+ flushToolOutputContent();
131
+ }
132
+
133
+ function translateImageToolOutput(output) {
134
+ const parts = Array.isArray(output) ? output : [output];
135
+ if (!parts.some(isImagePart)) return null;
136
+ return parts.map(part => {
137
+ if (isImagePart(part)) {
138
+ const image = translateImagePart(part);
139
+ if (!image) {
140
+ throw providerError('provider_request_invalid', 'A tool image requires an image URL or data URL for Chat Completions.');
141
+ }
142
+ return image;
143
+ }
144
+ const text = ['input_text', 'output_text', 'text'].includes(part?.type)
145
+ ? normalizeText(part.text)
146
+ : normalizeToolOutput(part);
147
+ return { type: 'text', text };
148
+ });
149
+ }
150
+
151
+ function isImagePart(part) {
152
+ return part?.type === 'input_image' || part?.type === 'image_url' || part?.type === 'computer_screenshot';
153
+ }
154
+
155
+ function translateImagePart(part) {
156
+ const url = normalizeText(part.image_url?.url || part.image_url || part.url);
157
+ if (!url) return null;
158
+ const detail = normalizeText(part.image_url?.detail || part.detail);
159
+ const image = { type: 'image_url', image_url: { url } };
160
+ if (['auto', 'low', 'high'].includes(detail)) image.image_url.detail = detail;
161
+ return image;
108
162
  }
109
163
 
110
164
  function extractReasoningText(item) {
@@ -142,10 +196,10 @@ function translateMessageContent(content) {
142
196
  if (part.type === 'input_text' || part.type === 'output_text' || part.type === 'text') {
143
197
  parts.push({ type: 'text', text: normalizeText(part.text) });
144
198
  } else if (part.type === 'input_image' || part.type === 'image_url') {
145
- const url = normalizeText(part.image_url?.url || part.image_url || part.url);
146
- if (url) {
199
+ const image = translateImagePart(part);
200
+ if (image) {
147
201
  hasStructuredPart = true;
148
- parts.push({ type: 'image_url', image_url: { url } });
202
+ parts.push(image);
149
203
  }
150
204
  }
151
205
  }
@@ -2,9 +2,11 @@
2
2
 
3
3
  const crypto = require('node:crypto');
4
4
  const { requiresReasoningContentReplay } = require('./providerReasoning');
5
+ const { isContextLimitError } = require('./providerContextError');
5
6
  const MAX_STREAM_BUFFER_BYTES = 8 * 1024 * 1024;
6
7
 
7
8
  function convertChatResponse(chat = {}, context = {}) {
9
+ if (chat.error) throwUpstreamError(chat.error);
8
10
  const choice = Array.isArray(chat.choices) ? chat.choices[0] || {} : {};
9
11
  const message = choice.message || {};
10
12
  assertNoLeakedToolControlTokens(message);
@@ -131,7 +133,7 @@ function writeConvertedResponseAsSse(res, converted, context = {}, onComplete =
131
133
 
132
134
  function processChatChunk(state, res, chunk) {
133
135
  if (!chunk || typeof chunk !== 'object') return;
134
- if (chunk.error) throw new Error(normalizeText(chunk.error.message) || 'Upstream provider returned an error.');
136
+ if (chunk.error) throwUpstreamError(chunk.error);
135
137
  if (chunk.id && !state.upstreamId) {
136
138
  state.upstreamId = chunk.id;
137
139
  }
@@ -295,6 +297,7 @@ function completeMessage(state, res) {
295
297
  }
296
298
 
297
299
  function completeTool(state, res, tool) {
300
+ assertCompleteToolArguments(tool.arguments);
298
301
  if (!tool.opened) openTool(state, res, tool);
299
302
  tool.item.status = 'completed';
300
303
  if (tool.kind === 'custom') {
@@ -419,6 +422,7 @@ function normalizeToolCalls(values, toolKinds) {
419
422
  const name = normalizeText(value?.function?.name) || 'tool';
420
423
  const callId = normalizeText(value?.id) || randomId('call');
421
424
  const argumentsText = normalizeText(value?.function?.arguments) || '{}';
425
+ assertCompleteToolArguments(argumentsText);
422
426
  const spec = toolKinds?.get(name);
423
427
  const kind = (typeof spec === 'object' ? spec.kind : spec) === 'custom' ? 'custom' : 'function';
424
428
  const originalName = typeof spec === 'object' ? spec.name || name : name;
@@ -489,6 +493,13 @@ function mapChatFinishReason(value) {
489
493
  return { status: 'completed', incompleteReason: null };
490
494
  }
491
495
 
496
+ function throwUpstreamError(detail) {
497
+ throw responseError(
498
+ normalizeText(detail?.message) || normalizeText(detail) || 'Upstream provider returned an error.',
499
+ isContextLimitError(detail) ? 'context_length_exceeded' : 'provider_upstream_error'
500
+ );
501
+ }
502
+
492
503
  function responseError(message, code = 'provider_response_invalid') {
493
504
  const error = new Error(message);
494
505
  error.code = code;
@@ -533,6 +544,14 @@ function unwrapCustomArguments(value) {
533
544
  }
534
545
  }
535
546
 
547
+ function assertCompleteToolArguments(value) {
548
+ try {
549
+ JSON.parse(value || '{}');
550
+ } catch (_error) {
551
+ throw responseError('The provider returned incomplete or invalid JSON tool arguments.');
552
+ }
553
+ }
554
+
536
555
  function reserveItem(state, item) {
537
556
  const index = state.items.length;
538
557
  state.items.push(item);
@@ -11,7 +11,9 @@ const {
11
11
  const { requiresReasoningContentReplay } = require('./providerReasoning');
12
12
  const { classifyResponsesRoute } = require('./providerBridgeRoutes');
13
13
  const { providerError } = require('./providerProfile');
14
+ const { fetchProvider, readProviderText, readProviderJson } = require('./providerHttp');
14
15
  const { sanitizeProviderMessage } = require('./providerRedaction');
16
+ const { isContextLimitError } = require('./providerContextError');
15
17
  const { logDebug } = require('./debugLog');
16
18
  const {
17
19
  createProviderStreamLifecycle,
@@ -118,15 +120,19 @@ async function handleRequest({
118
120
  const startedAt = Date.now();
119
121
  const previousResponseId = String(requestBody.previous_response_id || '');
120
122
  const previous = history.get(previousResponseId);
121
- const fallbackAnchor = previous
123
+ const fallbackAllowed = !previousResponseId || previousResponseId === continuationState.responseId;
124
+ const fallbackAnchor = previous || !fallbackAllowed
122
125
  ? []
123
126
  : continuationState.anchorMessages;
127
+ const continuationAttempts = previous
128
+ ? Number(previous.continuationAttempts) || 0
129
+ : fallbackAllowed ? continuationState.attempts : 0;
124
130
  const translated = buildChatRequest({
125
131
  requestBody,
126
132
  launch,
127
133
  historyMessages: previous?.messages || fallbackAnchor
128
134
  });
129
- assertReasoningContinuationBudget(translated.messages, continuationState.attempts);
135
+ assertReasoningContinuationBudget(translated.messages, continuationAttempts);
130
136
  const idleTimeoutMs = resolveProviderIdleTimeoutMs(launch);
131
137
  const totalTimeoutMs = resolveProviderTotalTimeoutMs(launch, idleTimeoutMs);
132
138
  logDebug('provider.bridge.request', {
@@ -141,7 +147,7 @@ async function handleRequest({
141
147
  usedFallbackAnchor: fallbackAnchor.length > 0,
142
148
  inputItemCount: Array.isArray(requestBody.input) ? requestBody.input.length : requestBody.input ? 1 : 0,
143
149
  messageCount: translated.messages.length,
144
- continuationAttempts: continuationState.attempts,
150
+ continuationAttempts,
145
151
  idleTimeoutMs,
146
152
  totalTimeoutMs
147
153
  });
@@ -161,15 +167,15 @@ async function handleRequest({
161
167
  res.on('close', onClientClose);
162
168
  lifecycle.start();
163
169
  try {
164
- const upstream = await fetch(buildChatCompletionsUrl(launch.baseUrl, launch), {
170
+ const upstream = await fetchProvider(buildChatCompletionsUrl(launch.baseUrl, launch), {
165
171
  method: 'POST',
166
172
  headers: buildUpstreamHeaders(launch),
167
173
  body: JSON.stringify(translated.body),
168
174
  signal: controller.signal
169
- });
175
+ }, launch);
170
176
  lifecycle.touch();
171
177
  if (!upstream.ok) {
172
- await forwardUpstreamError(upstream, res, launch.apiKey);
178
+ await forwardUpstreamError(upstream, res, launch.apiKey, lifecycle.touch, requestBody.stream !== false);
173
179
  return;
174
180
  }
175
181
  const context = {
@@ -193,7 +199,7 @@ async function handleRequest({
193
199
  : 0;
194
200
  if (anchored) {
195
201
  continuationState.anchorMessages = continuation;
196
- continuationState.attempts += 1;
202
+ continuationState.attempts = continuationAttempts + 1;
197
203
  continuationState.responseId = converted.response.id;
198
204
  } else {
199
205
  continuationState.anchorMessages = [];
@@ -217,7 +223,7 @@ async function handleRequest({
217
223
  rememberHistory(history, converted.response.id, [
218
224
  ...translated.messages,
219
225
  ...continuation
220
- ]);
226
+ ], { continuationAttempts: anchored ? continuationAttempts + 1 : 0 });
221
227
  };
222
228
  if (requestBody.stream !== false) {
223
229
  const contentType = String(upstream.headers.get('content-type') || '').toLowerCase();
@@ -231,11 +237,11 @@ async function handleRequest({
231
237
  recoverInterrupted: lifecycle.recoverInterrupted
232
238
  });
233
239
  } else {
234
- const converted = convertChatResponse(await upstream.json(), context);
240
+ const converted = convertChatResponse(await readProviderJson(upstream, lifecycle.touch), context);
235
241
  writeConvertedResponseAsSse(res, converted, context, remember);
236
242
  }
237
243
  } else {
238
- const converted = convertChatResponse(await upstream.json(), context);
244
+ const converted = convertChatResponse(await readProviderJson(upstream, lifecycle.touch), context);
239
245
  remember(converted);
240
246
  sendJson(res, 200, converted.response);
241
247
  }
@@ -250,6 +256,10 @@ async function handleRequest({
250
256
  if (controller.signal.aborted) {
251
257
  throw providerError('provider_connection_timeout', 'The provider request was cancelled or timed out.');
252
258
  }
259
+ if (error?.code === 'context_length_exceeded' && !res.headersSent) {
260
+ sendContextLimitError(res, sanitizeProviderMessage(error.message, [launch.apiKey]), requestBody.stream !== false);
261
+ return;
262
+ }
253
263
  throw error;
254
264
  } finally {
255
265
  lifecycle.dispose();
@@ -376,7 +386,9 @@ function normalizeBridgeErrorCode(value) {
376
386
  'provider_agent_tools_incompatible',
377
387
  'provider_reasoning_continuation_exhausted',
378
388
  'provider_stream_tool_parse_failed',
379
- 'provider_protocol_incompatible'
389
+ 'provider_protocol_incompatible',
390
+ 'context_length_exceeded',
391
+ 'provider_upstream_error'
380
392
  ].includes(value) ? value : 'provider_bridge_failed';
381
393
  }
382
394
 
@@ -386,19 +398,40 @@ function bridgeErrorMessage(code) {
386
398
  : 'The local provider bridge failed.';
387
399
  }
388
400
 
389
- async function forwardUpstreamError(upstream, res, apiKey) {
390
- const text = (await upstream.text()).slice(0, MAX_ERROR_BYTES);
401
+ async function forwardUpstreamError(upstream, res, apiKey, onActivity, clientStream) {
402
+ const text = (await readProviderText(upstream, onActivity)).slice(0, MAX_ERROR_BYTES);
391
403
  let message = `Provider returned HTTP ${upstream.status}.`;
404
+ let error;
392
405
  try {
393
406
  const parsed = JSON.parse(text);
407
+ error = parsed?.error || parsed;
394
408
  message = parsed?.error?.message || parsed?.message || message;
395
409
  } catch (_error) {}
410
+ if ([400, 413, 422].includes(upstream.status) && isContextLimitError(error || text)) {
411
+ sendContextLimitError(res, sanitizeProviderMessage(error ? message : text, [apiKey]), clientStream);
412
+ return;
413
+ }
396
414
  sendJsonError(res, upstream.status, 'provider_upstream_error', sanitizeProviderMessage(message, [apiKey]));
397
415
  }
398
416
 
399
- function rememberHistory(history, responseId, messages) {
417
+ function sendContextLimitError(res, message, stream) {
418
+ const error = { type: 'invalid_request_error', code: 'context_length_exceeded', message };
419
+ if (!stream) {
420
+ sendJson(res, 400, { error });
421
+ return;
422
+ }
423
+ // Non-2xx JSON is a generic HTTP failure to the installed Codex client.
424
+ // Its typed context recovery consumes a Responses failure event instead.
425
+ res.writeHead(200, { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-store' });
426
+ res.end(`event: response.failed\ndata: ${JSON.stringify({
427
+ type: 'response.failed', sequence_number: 0,
428
+ response: { id: `resp_${crypto.randomUUID()}`, object: 'response', status: 'failed', error }
429
+ })}\n\n`);
430
+ }
431
+
432
+ function rememberHistory(history, responseId, messages, metadata = {}) {
400
433
  history.delete(responseId);
401
- history.set(responseId, { messages });
434
+ history.set(responseId, { messages, ...metadata });
402
435
  while (history.size > MAX_HISTORY_ENTRIES) {
403
436
  history.delete(history.keys().next().value);
404
437
  }