codex-overleaf-link 2.5.0 → 2.5.1

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 (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  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 +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  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 +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -4,7 +4,7 @@
4
4
  // Native Host restarts and only permits work after positive exit evidence.
5
5
  const fs = require('node:fs');
6
6
  const path = require('node:path');
7
- const { getProjectMirror } = require('./mirrorWorkspace');
7
+ const { getProjectMirror, getDefaultMirrorRoot } = require('./mirrorWorkspace');
8
8
  const pending = new Map();
9
9
 
10
10
  function location(projectId, rootDir) {
@@ -13,16 +13,23 @@ function location(projectId, rootDir) {
13
13
  }
14
14
 
15
15
  function load(file) {
16
- if (pending.has(file)) return pending.get(file);
17
16
  let records = [];
18
17
  try {
19
18
  if (fs.statSync(file).size > 65536) throw new Error('Oversized stop barrier');
20
19
  const value = JSON.parse(fs.readFileSync(file, 'utf8'));
21
20
  if (!Array.isArray(value) || value.length > 64) throw new Error('Invalid stop barrier');
22
- records = value;
21
+ records = value.map(record => record && typeof record === 'object' ? record : { unknown: true });
23
22
  } catch (error) {
24
23
  if (error.code !== 'ENOENT') records = [{ unknown: true }];
25
24
  }
25
+ // Other Hosts can publish a barrier while this Host is already alive. Keep
26
+ // in-memory callbacks and failed writes, but never cache an empty disk read.
27
+ const key = ({ isStopped, ...record }) => JSON.stringify(record);
28
+ const merged = new Map(records.map(record => [key(record), record]));
29
+ for (const record of pending.get(file) || []) {
30
+ if (typeof record.isStopped === 'function' || record.persistenceFailed) merged.set(key(record), record);
31
+ }
32
+ records = Array.from(merged.values());
26
33
  pending.set(file, records);
27
34
  return records;
28
35
  }
@@ -53,13 +60,17 @@ function stopped(record) {
53
60
  function record(projectId, rootDir, error) {
54
61
  const file = location(projectId, rootDir);
55
62
  const records = load(file);
56
- records.push({
63
+ const entry = {
57
64
  processId: error.processId, processGroup: error.processGroup === true,
58
65
  platform: process.platform, createdAt: new Date().toISOString(),
59
66
  isStopped: error.isStopped
60
- });
67
+ };
68
+ records.push(entry);
61
69
  try { persist(file, records); }
62
- catch (failure) { error.stopBarrierPersistenceError = failure.message; }
70
+ catch (failure) {
71
+ Object.defineProperty(entry, 'persistenceFailed', { value: true });
72
+ error.stopBarrierPersistenceError = failure.message;
73
+ }
63
74
  }
64
75
 
65
76
  function isBlocked(projectId, rootDir) {
@@ -76,5 +87,29 @@ function isBlocked(projectId, rootDir) {
76
87
  return remaining.length > 0;
77
88
  }
78
89
 
79
- module.exports = { record, isBlocked };
90
+ // Update safety observes the existing project quarantine. It neither searches
91
+ // unrelated OS processes nor mutates a project's recovery records.
92
+ function getBlockedProjectCount(rootDir, options = {}) {
93
+ const root = path.resolve(rootDir || getDefaultMirrorRoot(options));
94
+ const files = new Set();
95
+ let unknown = 0;
96
+ try {
97
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
98
+ if (entry.isDirectory()) files.add(path.join(root, entry.name, '.codex-stop-barrier.json'));
99
+ else if (entry.isSymbolicLink()) unknown += 1;
100
+ }
101
+ } catch (error) {
102
+ if (error.code !== 'ENOENT') unknown += 1;
103
+ }
104
+ // Include failed persistence owned by this Host, even if its directory was
105
+ // never created. A disk inventory alone cannot release that quarantine.
106
+ for (const file of pending.keys()) {
107
+ if (path.dirname(path.dirname(file)) === root) files.add(file);
108
+ }
109
+ for (const file of files) {
110
+ if (load(file).some(value => !stopped(value))) unknown += 1;
111
+ }
112
+ return unknown;
113
+ }
80
114
 
115
+ module.exports = { record, isBlocked, getBlockedProjectCount };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ // Canonical project names stay literal. Reject names the local filesystem would
4
+ // reinterpret instead of attempting to escape, trim or rename them.
5
+ function isRepresentableProjectPath(value, platform = process.platform) {
6
+ if (typeof value !== 'string' || !value) return false;
7
+ if (platform !== 'win32') return true;
8
+ return value.split('/').every(part => part && part !== '.' && part !== '..'
9
+ && part.length <= 255 && !/[<>:"\\|?*\u0000-\u001f]/.test(part) && !/[ .]$/.test(part)
10
+ && !/^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(part));
11
+ }
12
+
13
+ function assertProjectPathsForPlatform(paths, platform = process.platform) {
14
+ const identities = new Map();
15
+ for (const value of paths) {
16
+ let valid = isRepresentableProjectPath(value, platform);
17
+ if (valid && platform === 'win32') {
18
+ const parts = value.split('/');
19
+ for (let index = 1; index <= parts.length; index += 1) {
20
+ const literal = parts.slice(0, index).join('/'), key = literal.toLowerCase();
21
+ if (identities.has(key) && identities.get(key) !== literal) valid = false;
22
+ identities.set(key, literal);
23
+ }
24
+ }
25
+ if (!valid) throw Object.assign(new Error(`Project path cannot be represented faithfully on ${platform}: ${value}`),
26
+ { code: 'project_path_unrepresentable', path: value });
27
+ }
28
+ }
29
+
30
+ module.exports = { isRepresentableProjectPath, assertProjectPathsForPlatform };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const { providerError } = require('./providerProfile');
4
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
5
+ const BODY_HEADERS = ['content-encoding', 'content-language', 'content-location', 'content-type', 'content-length'];
6
+
7
+ function carriesEndpointCredentials(headers, launch) {
8
+ const namedHeader = String(launch.apiKeyHeaderName || '').toLowerCase();
9
+ return [...headers].some(([name, value]) => value && (
10
+ name === namedHeader
11
+ || /(?:^|[-_])(?:authorization|auth|api[-_]?key|token|secret|cookie|credential|password|signature)(?:$|[-_])/i.test(name)
12
+ || launch.apiKey && value.includes(launch.apiKey)
13
+ ));
14
+ }
15
+
16
+ // Fetch's method/body redirect rules remain intact. Credentials configured for
17
+ // this endpoint never authorize a request to a different origin.
18
+ async function fetchProvider(url, options, launch = {}) {
19
+ let current = new URL(url);
20
+ let method = String(options.method || 'GET').toUpperCase();
21
+ let body = options.body;
22
+ const headers = new Headers(options.headers);
23
+ const credentialed = carriesEndpointCredentials(headers, launch);
24
+ for (let redirects = 0; ; redirects += 1) {
25
+ const response = await fetch(current, { ...options, method, body, headers, redirect: 'manual' });
26
+ const location = response.headers.get('location');
27
+ if (!REDIRECT_STATUSES.has(response.status) || location === null) return response;
28
+ await response.body?.cancel().catch(() => {});
29
+ if (redirects >= 20) throw providerError('provider_upstream_error', 'The provider exceeded the redirect limit.');
30
+ let next;
31
+ try { next = new URL(location, current); }
32
+ catch (_) { throw providerError('provider_upstream_error', 'The provider returned an invalid redirect.'); }
33
+ if (!['http:', 'https:'].includes(next.protocol) || next.username || next.password) {
34
+ throw providerError('provider_upstream_error', 'The provider returned an unsupported redirect.');
35
+ }
36
+ if (credentialed && next.origin !== current.origin) {
37
+ throw providerError('provider_upstream_error', 'The provider redirected to a different origin; endpoint credentials were not forwarded.');
38
+ }
39
+ if ((response.status === 301 || response.status === 302) && method === 'POST'
40
+ || response.status === 303 && method !== 'GET' && method !== 'HEAD') {
41
+ method = 'GET';
42
+ body = undefined;
43
+ for (const name of BODY_HEADERS) headers.delete(name);
44
+ }
45
+ current = next;
46
+ }
47
+ }
48
+
49
+ // Preserve ordinary Response.text()/json() capacity and UTF-8 semantics while
50
+ // reporting each body chunk to the existing idle lifecycle. Compact/SSE limits
51
+ // remain in their own readers; this does not introduce a new response cap.
52
+ async function readProviderText(response, onActivity = () => {}) {
53
+ if (!response.body) return '';
54
+ const reader = response.body.getReader();
55
+ const decoder = new TextDecoder();
56
+ const chunks = [];
57
+ let complete = false;
58
+ try {
59
+ while (true) {
60
+ const { done, value } = await reader.read();
61
+ if (done) break;
62
+ onActivity();
63
+ chunks.push(decoder.decode(value, { stream: true }));
64
+ }
65
+ complete = true;
66
+ chunks.push(decoder.decode());
67
+ return chunks.join('');
68
+ } finally {
69
+ if (!complete) await reader.cancel().catch(() => {});
70
+ reader.releaseLock();
71
+ }
72
+ }
73
+
74
+ async function readProviderJson(response, onActivity) {
75
+ const text = await readProviderText(response, onActivity);
76
+ try { return JSON.parse(text); }
77
+ catch (_) { throw providerError('provider_response_invalid', 'The provider returned an invalid JSON response.'); }
78
+ }
79
+
80
+ module.exports = { fetchProvider, readProviderText, readProviderJson };
@@ -65,7 +65,8 @@ function normalizeProviderDraft(value = {}) {
65
65
  anthropicThinkingMode: normalizeAnthropicThinkingMode(value.anthropicThinkingMode),
66
66
  anthropicPromptCaching: value.anthropicPromptCaching === true,
67
67
  impersonateClaudeCode: value.impersonateClaudeCode === true,
68
- maxOutputTokens: normalizeMaxOutputTokens(value.maxOutputTokens)
68
+ maxOutputTokens: normalizeMaxOutputTokens(value.maxOutputTokens),
69
+ outputBudgetVersion: 1
69
70
  };
70
71
  }
71
72
 
@@ -245,6 +246,7 @@ function sanitizeProfile(profile, hasSecret) {
245
246
  anthropicPromptCaching: profile.anthropicPromptCaching === true,
246
247
  impersonateClaudeCode: profile.impersonateClaudeCode === true,
247
248
  maxOutputTokens: normalizeMaxOutputTokens(profile.maxOutputTokens),
249
+ outputBudgetVersion: 1,
248
250
  hasSecret: Boolean(hasSecret),
249
251
  secretUpdatedAt: profile.secretUpdatedAt || 0,
250
252
  endpointDisclosureHost: profile.endpointDisclosureHost || '',
@@ -419,7 +421,7 @@ function normalizeUpstreamResponseMode(value, allowAuto) {
419
421
 
420
422
  function normalizeMaxOutputTokens(value) {
421
423
  const number = Number(value);
422
- if (!Number.isFinite(number) || number === 8192 || number === 32768) return 65536;
424
+ if (!Number.isFinite(number)) return 65536;
423
425
  return Math.min(65536, Math.max(256, Math.floor(number)));
424
426
  }
425
427
 
@@ -13,7 +13,6 @@ const REASONING_ADAPTERS = new Set([
13
13
  ]);
14
14
  const REASONING_CAPABILITIES = new Set(['auto', 'none', 'toggle', 'effort']);
15
15
  const REASONING_EFFORTS = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
16
- const DEEPSEEK_MAX_REASONING_OUTPUT_TOKENS = 65536;
17
16
 
18
17
  function normalizeReasoningAdapter(value) {
19
18
  const normalized = String(value || '').trim().toLowerCase();
@@ -165,12 +164,8 @@ function resolveReasoningMaxOutputTokens(value, responsesRequest = {}, launch =
165
164
  if (!Number.isFinite(number)) {
166
165
  return value;
167
166
  }
168
- const normalized = Math.max(1, Math.floor(number));
169
- const effort = resolveRequestedReasoningEffort(responsesRequest, launch);
170
- const adapter = resolveReasoningAdapter(launch, modelId || launch.modelId);
171
- return adapter === 'deepseek' && ['xhigh', 'max'].includes(effort)
172
- ? Math.max(normalized, DEEPSEEK_MAX_REASONING_OUTPUT_TOKENS)
173
- : normalized;
167
+ // Reasoning effort cannot silently increase the configured output maximum.
168
+ return Math.max(1, Math.floor(number));
174
169
  }
175
170
 
176
171
  function requiresReasoningContentReplay(launch = {}, modelId = '') {
@@ -92,6 +92,7 @@ async function testProvider(params, env, emit = () => {}, externalSignal = null)
92
92
  }, totalBudgetMs);
93
93
  const onExternalAbort = () => controller.abort();
94
94
  externalSignal?.addEventListener?.('abort', onExternalAbort, { once: true });
95
+ if (externalSignal?.aborted) onExternalAbort();
95
96
  activeTests.get(operationId)?.abort?.();
96
97
  activeTests.set(operationId, controller);
97
98
  const protocols = draft.wireApiPreference === 'auto'
@@ -4,6 +4,7 @@ const crypto = require('node:crypto');
4
4
  const fs = require('node:fs');
5
5
  const os = require('node:os');
6
6
  const path = require('node:path');
7
+ const { isDeepStrictEqual } = require('node:util');
7
8
  const {
8
9
  computeConnectionFingerprint,
9
10
  computeDraftFingerprint,
@@ -16,6 +17,9 @@ const {
16
17
  } = require('./providerProfile');
17
18
 
18
19
  const SCHEMA_VERSION = 1;
20
+ const STORE_FILE_MAX_BYTES = 1024 * 1024;
21
+ // A compact journal contains two bounded Store files plus revision/schema keys.
22
+ const STORE_TRANSACTION_MAX_BYTES = 2 * STORE_FILE_MAX_BYTES + 1024;
19
23
  const STORE_LOCK_TIMEOUT_MS = 2500;
20
24
  const STORE_LOCK_STALE_MS = 30000;
21
25
  const STORE_LOCK_RETRY_MS = 25;
@@ -357,6 +361,7 @@ function loadProviderStateUnlocked(env = process.env) {
357
361
  : [];
358
362
  return {
359
363
  paths,
364
+ stored: { public: publicStore, secrets: secretStore },
360
365
  public: {
361
366
  schemaVersion: SCHEMA_VERSION,
362
367
  storeRevision: normalizeRevision(publicStore.storeRevision),
@@ -373,23 +378,35 @@ function loadProviderStateUnlocked(env = process.env) {
373
378
 
374
379
  function commitProviderState(previous, next) {
375
380
  const revision = previous.public.storeRevision + 1;
376
- const nextPublic = { ...next.public, schemaVersion: SCHEMA_VERSION, storeRevision: revision };
377
- const nextSecrets = { ...next.secrets, schemaVersion: SCHEMA_VERSION, storeRevision: revision };
381
+ const previousPublic = previous.stored.public;
382
+ const previousSecrets = previous.stored.secrets;
383
+ const nextPublic = { ...mergeStoredChanges(previousPublic, previous.public, next.public), schemaVersion: SCHEMA_VERSION, storeRevision: revision };
384
+ const nextSecrets = { ...mergeStoredChanges(previousSecrets, previous.secrets, next.secrets), schemaVersion: SCHEMA_VERSION, storeRevision: revision };
385
+ const compactPublic = Buffer.byteLength(`${JSON.stringify(previousPublic, null, 2)}\n`) > STORE_FILE_MAX_BYTES;
386
+ const compactSecrets = Buffer.byteLength(`${JSON.stringify(previousSecrets, null, 2)}\n`) > STORE_FILE_MAX_BYTES;
378
387
  const journal = {
379
388
  schemaVersion: SCHEMA_VERSION,
380
389
  nextRevision: revision,
381
- previousPublic: previous.public,
382
- previousSecrets: previous.secrets
390
+ previousPublic,
391
+ previousSecrets,
392
+ compactPublic,
393
+ compactSecrets
383
394
  };
395
+ // Validate every write, including rollback copies, before replacing any file.
396
+ serializeStoreJson(previous.paths.publicFile, nextPublic, compactPublic);
397
+ serializeStoreJson(previous.paths.secretFile, nextSecrets, compactSecrets);
398
+ serializeStoreJson(previous.paths.publicFile, previousPublic, compactPublic);
399
+ serializeStoreJson(previous.paths.secretFile, previousSecrets, compactSecrets);
400
+ serializeStoreJson(previous.paths.transactionFile, journal);
384
401
  atomicWriteJson(previous.paths.transactionFile, journal);
385
402
  try {
386
- atomicWriteJson(previous.paths.secretFile, nextSecrets);
387
- atomicWriteJson(previous.paths.publicFile, nextPublic);
403
+ atomicWriteJson(previous.paths.secretFile, nextSecrets, compactSecrets);
404
+ atomicWriteJson(previous.paths.publicFile, nextPublic, compactPublic);
388
405
  fs.rmSync(previous.paths.transactionFile, { force: true });
389
406
  } catch (error) {
390
407
  try {
391
- atomicWriteJson(previous.paths.secretFile, previous.secrets);
392
- atomicWriteJson(previous.paths.publicFile, previous.public);
408
+ atomicWriteJson(previous.paths.secretFile, previousSecrets, compactSecrets);
409
+ atomicWriteJson(previous.paths.publicFile, previousPublic, compactPublic);
393
410
  fs.rmSync(previous.paths.transactionFile, { force: true });
394
411
  } catch (_restoreError) {
395
412
  // The journal remains the recovery source on the next Native Host start.
@@ -398,6 +415,26 @@ function commitProviderState(previous, next) {
398
415
  }
399
416
  }
400
417
 
418
+ function mergeStoredChanges(stored, before, after) {
419
+ if (isDeepStrictEqual(before, after)) return stored;
420
+ if (!stored || !before || !after || typeof stored !== 'object' || typeof before !== 'object' || typeof after !== 'object') return after;
421
+ if (Array.isArray(before) || Array.isArray(after)) {
422
+ // Profiles and models carry stable IDs; changed membership must not persist
423
+ // normalization defaults into unrelated records at the storage boundary.
424
+ if (!Array.isArray(stored) || !Array.isArray(before) || !Array.isArray(after)
425
+ || [...before, ...after].some(item => !item?.id)) return after;
426
+ const oldById = new Map(before.map(item => [item.id, item]));
427
+ const storedById = new Map(stored.map(item => [typeof item === 'string' ? item : item?.id, item]));
428
+ return after.map(item => mergeStoredChanges(storedById.get(item.id), oldById.get(item.id), item));
429
+ }
430
+ const result = { ...stored };
431
+ for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
432
+ if (!Object.prototype.hasOwnProperty.call(after, key)) delete result[key];
433
+ else if (!isDeepStrictEqual(before[key], after[key])) result[key] = mergeStoredChanges(stored[key], before[key], after[key]);
434
+ }
435
+ return result;
436
+ }
437
+
401
438
  function recoverInterruptedTransaction(paths) {
402
439
  if (!fs.existsSync(paths.transactionFile)) {
403
440
  return;
@@ -412,8 +449,10 @@ function recoverInterruptedTransaction(paths) {
412
449
  fs.rmSync(paths.transactionFile, { force: true });
413
450
  return;
414
451
  }
415
- atomicWriteJson(paths.secretFile, journal.previousSecrets);
416
- atomicWriteJson(paths.publicFile, journal.previousPublic);
452
+ serializeStoreJson(paths.secretFile, journal.previousSecrets, journal.compactSecrets === true);
453
+ serializeStoreJson(paths.publicFile, journal.previousPublic, journal.compactPublic === true);
454
+ atomicWriteJson(paths.secretFile, journal.previousSecrets, journal.compactSecrets === true);
455
+ atomicWriteJson(paths.publicFile, journal.previousPublic, journal.compactPublic === true);
417
456
  fs.rmSync(paths.transactionFile, { force: true });
418
457
  }
419
458
 
@@ -502,11 +541,16 @@ function normalizeStoredProfile(value) {
502
541
  if (!value || typeof value !== 'object' || !normalizeId(value.id)) {
503
542
  throw providerError('provider_store_corrupt', 'Provider store contains an invalid profile.');
504
543
  }
505
- const normalized = normalizeProviderDraft(value);
544
+ const legacyOutputBudget = value.outputBudgetVersion !== 1
545
+ && [8192, 32768].includes(Number(value.maxOutputTokens));
546
+ const normalized = normalizeProviderDraft(legacyOutputBudget ? { ...value, maxOutputTokens: 65536 } : value);
506
547
  return {
507
548
  id: normalizeId(value.id),
508
549
  revision: Math.max(1, normalizeRevision(value.revision)),
509
550
  ...normalized,
551
+ // Keep the old provenance until an explicit upsert persists the new value.
552
+ // Unrelated mutations must not mark an unmigrated raw record as current.
553
+ outputBudgetVersion: value.outputBudgetVersion === 1 ? 1 : 0,
510
554
  resolvedWireApi: isResolvedWireApi(value.resolvedWireApi) ? value.resolvedWireApi : '',
511
555
  endpointDisclosureHost: String(value.endpointDisclosureHost || ''),
512
556
  endpointDisclosureBaseUrl: String(value.endpointDisclosureBaseUrl || ''),
@@ -634,11 +678,12 @@ function ensurePrivateDirectory(directory) {
634
678
  }
635
679
  }
636
680
 
637
- function atomicWriteJson(target, value) {
681
+ function atomicWriteJson(target, value, compact = false) {
682
+ const serialized = serializeStoreJson(target, value, compact);
638
683
  const temp = `${target}.tmp-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
639
684
  const fd = fs.openSync(temp, 'w', 0o600);
640
685
  try {
641
- fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
686
+ fs.writeFileSync(fd, serialized, 'utf8');
642
687
  fs.fsyncSync(fd);
643
688
  } finally {
644
689
  fs.closeSync(fd);
@@ -649,13 +694,26 @@ function atomicWriteJson(target, value) {
649
694
  fs.renameSync(temp, target);
650
695
  }
651
696
 
697
+ function getStoreFileMaxBytes(target) {
698
+ return path.basename(target) === 'provider-transaction.json' ? STORE_TRANSACTION_MAX_BYTES : STORE_FILE_MAX_BYTES;
699
+ }
700
+
701
+ function serializeStoreJson(target, value, compact = false) {
702
+ const serialized = `${JSON.stringify(value, null, compact || path.basename(target) === 'provider-transaction.json' ? undefined : 2)}\n`;
703
+ const maxBytes = getStoreFileMaxBytes(target);
704
+ if (Buffer.byteLength(serialized, 'utf8') > maxBytes) {
705
+ throw providerError('provider_store_too_large', `Provider settings exceed the ${maxBytes}-byte limit for ${path.basename(target)}. Reduce the configuration and retry.`);
706
+ }
707
+ return serialized;
708
+ }
709
+
652
710
  function readJson(target, fallback) {
653
711
  if (!fs.existsSync(target)) {
654
712
  return fallback;
655
713
  }
656
714
  try {
657
715
  const stat = fs.statSync(target);
658
- if (!stat.isFile() || stat.size > 1024 * 1024) {
716
+ if (!stat.isFile() || stat.size > getStoreFileMaxBytes(target)) {
659
717
  throw new Error('invalid provider store file');
660
718
  }
661
719
  return JSON.parse(fs.readFileSync(target, 'utf8'));