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
@@ -132,6 +132,18 @@
132
132
  };
133
133
  }
134
134
 
135
+ // Commit the explicit preflight decision to both live and durable run consumers.
136
+ // The originally captured (possibly queued) snapshot stays immutable.
137
+ function applyUntrackedDecision(view, record, choice) {
138
+ if (!['continue', 'disable'].includes(choice) || !view?.recordId || record?.id !== view.recordId) {
139
+ throw Codec.snapshotError('invalid_reviewing_decision', 'An explicit decision for the current run is required.');
140
+ }
141
+ const snapshot = create({ ...snapshotOf(view), requireReviewing: false });
142
+ view.executionSnapshot = cloneSnapshot(snapshot);
143
+ Object.assign(record, toRunCompatibilityTuple(snapshot));
144
+ return snapshot;
145
+ }
146
+
135
147
  function projectForDisplay(value = {}) {
136
148
  const snapshot = snapshotOf(value);
137
149
  return {
@@ -174,6 +186,7 @@
174
186
  SCHEMA_VERSION: Codec.SCHEMA_VERSION,
175
187
  TUPLE_FIELDS: Codec.TUPLE_FIELDS,
176
188
  applyToState,
189
+ applyUntrackedDecision,
177
190
  capture,
178
191
  captureRawQueueTuple,
179
192
  cloneSnapshot,
@@ -11,6 +11,7 @@
11
11
  const MAX_TEXT_CHARS = 12000;
12
12
  const MAX_QUEUE_BYTES = 128 * 1024;
13
13
  const MAX_PERSISTED_QUEUE_BYTES = 512 * 1024;
14
+ const MAX_CUSTOM_INSTRUCTIONS_CHARS = 12000;
14
15
  const ACTIVE_STATUSES = new Set(['claimed', 'executing', 'steering']);
15
16
  const VALID_STATUSES = new Set(['queued', 'paused', ...ACTIVE_STATUSES]);
16
17
 
@@ -41,7 +42,7 @@
41
42
  claimToken: status === 'claimed' || status === 'executing' ? String(raw.claimToken || '') : '',
42
43
  pauseReason: status === 'paused' ? String(raw.pauseReason || '') : '',
43
44
  executionSnapshot: RunExecutionSnapshot.cloneSnapshot(executionSnapshot),
44
- payload: RunExecutionSnapshot.toQueuePayload(executionSnapshot)
45
+ payload: buildPayload(executionSnapshot, raw.payload?.submissionContext)
45
46
  });
46
47
  if (normalized.length >= MAX_ITEMS) {
47
48
  break;
@@ -79,7 +80,7 @@
79
80
  claimToken: '',
80
81
  pauseReason: input.status === 'paused' ? String(input.pauseReason || '') : '',
81
82
  executionSnapshot: RunExecutionSnapshot.cloneSnapshot(executionSnapshot),
82
- payload: RunExecutionSnapshot.toQueuePayload(executionSnapshot)
83
+ payload: buildPayload(executionSnapshot, input.payload?.submissionContext)
83
84
  };
84
85
  const next = [...queue, item];
85
86
  if (estimateLegacyBytes(next) > MAX_QUEUE_BYTES || estimateBytes(next) > MAX_PERSISTED_QUEUE_BYTES) {
@@ -137,7 +138,7 @@
137
138
  item = {
138
139
  ...item,
139
140
  executionSnapshot: RunExecutionSnapshot.cloneSnapshot(snapshot),
140
- payload: RunExecutionSnapshot.toQueuePayload(snapshot)
141
+ payload: buildPayload(snapshot, item.payload?.submissionContext)
141
142
  };
142
143
  } catch (error) {
143
144
  return {
@@ -199,6 +200,46 @@
199
200
  );
200
201
  }
201
202
 
203
+ // Queue inputs persist only these submitted extras. Attachments stay unsupported,
204
+ // and older items must not acquire a later draft's one-turn context on promotion.
205
+ function normalizeSubmissionContext(value = {}, helpers = {}) {
206
+ const input = value && typeof value === 'object' ? value : {};
207
+ const normalizeText = (value, maxChars) => {
208
+ const text = typeof value === 'string' ? value : '';
209
+ return String(helpers.normalizeDisplay ? helpers.normalizeDisplay(text, maxChars) : text).slice(0, maxChars);
210
+ };
211
+ const safeId = value => typeof value === 'string'
212
+ && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(value) && !value.includes('..')
213
+ && !['__proto__', 'prototype', 'constructor'].includes(value);
214
+ const rawSkill = input.skillInvocation;
215
+ const installer = rawSkill?.id === 'skill-installer' && (rawSkill.scope == null || rawSkill.scope === 'builtin');
216
+ const skillInvocation = safeId(rawSkill?.id)
217
+ && (installer || ['codex-overleaf', 'codex-local'].includes(rawSkill.scope))
218
+ ? { id: rawSkill.id, title: normalizeText(rawSkill.title || rawSkill.id, 80),
219
+ ...(installer ? {} : { scope: rawSkill.scope }) }
220
+ : null;
221
+ return {
222
+ customInstructions: normalizeText(input.customInstructions, MAX_CUSTOM_INSTRUCTIONS_CHARS),
223
+ skillLoadingSettings: {
224
+ loadCodexLocalSkills: input.skillLoadingSettings?.loadCodexLocalSkills === true,
225
+ loadCodexOverleafSkills: input.skillLoadingSettings?.loadCodexOverleafSkills === true
226
+ },
227
+ skillInvocation,
228
+ ...(Array.isArray(input.enabledCodexLocalSkillIds) ? {
229
+ enabledCodexLocalSkillIds: [...new Set(input.enabledCodexLocalSkillIds.filter(safeId))]
230
+ } : {}),
231
+ enabledCodexOverleafSkillIds: [...new Set((Array.isArray(input.enabledCodexOverleafSkillIds)
232
+ ? input.enabledCodexOverleafSkillIds : []).filter(safeId))]
233
+ };
234
+ }
235
+
236
+ function buildPayload(snapshot, submissionContext, helpers) {
237
+ const payload = RunExecutionSnapshot.toQueuePayload(snapshot);
238
+ // Keep legacy absence compact; safe defaults are applied only at execution.
239
+ if (submissionContext == null) return payload;
240
+ return { ...payload, submissionContext: normalizeSubmissionContext(submissionContext, helpers) };
241
+ }
242
+
202
243
  function normalizeTimestamp(value) {
203
244
  return typeof value === 'string' && value ? value : new Date().toISOString();
204
245
  }
@@ -237,7 +278,8 @@
237
278
  speedTier: item.payload.speedTier,
238
279
  autoRecompile: item.payload.autoRecompile,
239
280
  requireReviewing: item.payload.requireReviewing,
240
- focusFiles: item.payload.focusFiles
281
+ focusFiles: item.payload.focusFiles,
282
+ submissionContext: item.payload.submissionContext
241
283
  }
242
284
  : {}
243
285
  };
@@ -269,7 +311,7 @@
269
311
  claimToken: normalizeField(item?.claimToken, 160),
270
312
  pauseReason: normalizeField(item?.pauseReason, 160),
271
313
  executionSnapshot: compactSnapshot,
272
- payload: RunExecutionSnapshot.toQueuePayload(compactSnapshot)
314
+ payload: buildPayload(compactSnapshot, payload.submissionContext, { normalizeDisplay })
273
315
  };
274
316
  })
275
317
  .filter(item => item.id && item.text);
@@ -292,6 +334,7 @@
292
334
  markExecuting,
293
335
  markSteering,
294
336
  normalizeQueue,
337
+ normalizeSubmissionContext,
295
338
  pauseAll,
296
339
  remove,
297
340
  resumeAll,
@@ -6,20 +6,29 @@
6
6
  const MAX_TEXT = 20000;
7
7
  function normalize(value) {
8
8
  if (!value || typeof value !== 'object') return null;
9
- const path = typeof value.path === 'string' ? value.path.replace(/\\/g, '/') : '';
10
- if (!path || path.startsWith('/') || /^[A-Za-z]:/.test(path)
9
+ const path = typeof value.path === 'string' ? value.path : '';
10
+ if (!path || path.length > 2048 || path.trim() !== path || path.includes('\\')
11
+ || /[\u0000-\u001f\u007f]/.test(path) || path.startsWith('/') || /^[A-Za-z]:/.test(path)
11
12
  || path.split('/').some(part => !part || part === '.' || part === '..')) return null;
12
13
  if (typeof value.projectId !== 'string' || !value.projectId
13
14
  || typeof value.text !== 'string' || !value.text.length || value.text.length > MAX_TEXT
15
+ || /[\uD800-\uDFFF]/u.test(value.text)
14
16
  || !Number.isSafeInteger(value.from) || !Number.isSafeInteger(value.to)
15
17
  || value.from < 0 || value.to <= value.from || value.to - value.from !== value.text.length
16
18
  || !/^[a-f0-9]{64}$/i.test(value.documentHash || '')) return null;
19
+ const proof = value.rebaseEvidence;
20
+ const rebaseEvidence = proof?.v === 1 && Number.isSafeInteger(proof.documentLength)
21
+ && proof.documentLength >= value.to && /^[a-f0-9]{64}$/i.test(proof.prefixHash || '')
22
+ && /^[a-f0-9]{64}$/i.test(proof.suffixHash || '')
23
+ ? Object.freeze({ v: 1, documentLength: proof.documentLength,
24
+ prefixHash: proof.prefixHash.toLowerCase(), suffixHash: proof.suffixHash.toLowerCase() }) : null;
17
25
  return Object.freeze({
18
- projectId: value.projectId.slice(0, 160), path: path.slice(0, 320),
26
+ projectId: value.projectId.slice(0, 160), path,
19
27
  from: value.from, to: value.to, text: value.text,
20
28
  lineStart: Number.isSafeInteger(value.lineStart) && value.lineStart > 0 ? value.lineStart : 1,
21
29
  lineEnd: Number.isSafeInteger(value.lineEnd) && value.lineEnd > 0 ? value.lineEnd : 1,
22
30
  documentHash: value.documentHash.toLowerCase(),
31
+ ...(rebaseEvidence ? { rebaseEvidence } : {}),
23
32
  mode: value.mode === 'edit' ? 'edit' : 'reference',
24
33
  capturedAt: typeof value.capturedAt === 'string' ? value.capturedAt.slice(0, 64) : ''
25
34
  });
@@ -10,12 +10,15 @@
10
10
  require('./sessionTitle'),
11
11
  require('./runActivityModel'),
12
12
  require('./subagentActivityModel'),
13
- require('./writebackIntent')
13
+ require('./writebackIntent'),
14
+ require('./directoryCleanupProof'),
15
+ require('./undoOperations'),
16
+ require('./contextUsage')
14
17
  );
15
18
  } else {
16
19
  root.CodexOverleafModuleRegistry.define(
17
20
  'SessionState',
18
- ['I18n', 'SettlementFacts', 'RunInputQueue', 'SelectionContext', 'LineReferences', 'PathRedaction', 'SessionTitle', 'RunActivityModel', 'SubagentActivityModel', 'WritebackIntent'],
21
+ ['I18n', 'SettlementFacts', 'RunInputQueue', 'SelectionContext', 'LineReferences', 'PathRedaction', 'SessionTitle', 'RunActivityModel', 'SubagentActivityModel', 'WritebackIntent', 'DirectoryCleanupProof', 'UndoOperations', 'ContextUsage'],
19
22
  factory
20
23
  );
21
24
  }
@@ -29,7 +32,10 @@
29
32
  SessionTitle,
30
33
  RunActivityModel,
31
34
  SubagentActivityModel,
32
- WritebackIntent
35
+ WritebackIntent,
36
+ DirectoryCleanupProof,
37
+ UndoOperations,
38
+ ContextUsage
33
39
  ) {
34
40
  'use strict';
35
41
 
@@ -46,6 +52,7 @@
46
52
  loadCodexLocalSkills: true,
47
53
  loadCodexOverleafSkills: true,
48
54
  codexOverleafSkillEnabled: {},
55
+ codexLocalSkillEnabled: {},
49
56
  theme: 'dark',
50
57
  panelWidth: 380,
51
58
  task: '',
@@ -120,7 +127,6 @@
120
127
  statusTextChars: 400,
121
128
  attachmentPreviewChars: 160 * 1024
122
129
  };
123
- const REDACTED_SECRET = '[REDACTED_SECRET]';
124
130
  const SECRET_REDACTION_PATTERNS = [
125
131
  /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
126
132
  /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
@@ -153,6 +159,7 @@
153
159
  state.loadCodexLocalSkills = state.loadCodexLocalSkills !== false;
154
160
  state.loadCodexOverleafSkills = state.loadCodexOverleafSkills !== false;
155
161
  state.codexOverleafSkillEnabled = normalizeCodexOverleafSkillEnabled(state.codexOverleafSkillEnabled);
162
+ state.codexLocalSkillEnabled = normalizeCodexLocalSkillEnabled(state.codexLocalSkillEnabled);
156
163
  state.theme = ['dark', 'light', 'auto'].includes(state.theme) ? state.theme : 'dark';
157
164
  state.panelWidth = normalizePanelWidth(state.panelWidth);
158
165
  state.task = typeof state.task === 'string' ? state.task : '';
@@ -213,6 +220,8 @@
213
220
  const history = normalizeHistoryEntries(session.history);
214
221
  const normalizedRuns = normalizeRuns(session.runs, options);
215
222
  const runs = normalizedRuns.length ? normalizedRuns : recoverRunsFromHistory(session, history);
223
+ for (const run of runs) pruneRunRecoveryScope(run, session);
224
+
216
225
  const rawTitle = typeof session.title === 'string' ? session.title.trim() : '';
217
226
  const derivedTitle = deriveSessionTitle(runs, session.task);
218
227
  const titleSource = normalizeTitleSource(session.titleSource, rawTitle, derivedTitle);
@@ -243,6 +252,7 @@
243
252
  selectionContext: SelectionContext.normalize(session.selectionContext),
244
253
  projectReferenceFiles: normalizeProjectReferenceFiles(session.projectReferenceFiles),
245
254
  codexThreadId: typeof session.codexThreadId === 'string' ? session.codexThreadId : '',
255
+ contextUsage: ContextUsage.normalize(session.contextUsage),
246
256
  forkedFromSessionId: typeof session.forkedFromSessionId === 'string' ? session.forkedFromSessionId : '',
247
257
  forkedFromRunId: typeof session.forkedFromRunId === 'string' ? session.forkedFromRunId : '',
248
258
  pendingInputs: normalizePendingInputs(
@@ -323,6 +333,7 @@
323
333
  focusFiles: normalizeFocusFiles(active.focusFiles),
324
334
  projectReferenceFiles: normalizeProjectReferenceFiles(active.projectReferenceFiles),
325
335
  codexThreadId: active.codexThreadId || '',
336
+ contextUsage: ContextUsage.normalize(active.contextUsage),
326
337
  providerId: state.providerId || 'builtin'
327
338
  };
328
339
  state.runs = Array.isArray(active.runs) ? active.runs : [];
@@ -367,6 +378,7 @@
367
378
  selectionContext: SelectionContext.normalize(overrides.selectionContext),
368
379
  projectReferenceFiles: normalizeProjectReferenceFiles(overrides.projectReferenceFiles),
369
380
  codexThreadId: typeof overrides.codexThreadId === 'string' ? overrides.codexThreadId : '',
381
+ contextUsage: ContextUsage.normalize(overrides.contextUsage),
370
382
  forkedFromSessionId: typeof overrides.forkedFromSessionId === 'string' ? overrides.forkedFromSessionId : '',
371
383
  forkedFromRunId: typeof overrides.forkedFromRunId === 'string' ? overrides.forkedFromRunId : '',
372
384
  pendingInputs: normalizePendingInputs(overrides.pendingInputs)
@@ -432,6 +444,34 @@
432
444
  });
433
445
  }
434
446
 
447
+ // Composer input changes only the draft on an already normalized state.
448
+ // Keep live run/event objects intact so typing cannot detach their writers.
449
+ function updateActiveSessionDraft(state, task) {
450
+ const active = getActiveSession(state);
451
+ if (!active) {
452
+ return mirrorActiveSession({ ...state });
453
+ }
454
+ const draft = sanitizeAssistantVisibleText(task);
455
+ if (draft === active.task && draft === state.task) {
456
+ return state;
457
+ }
458
+ const rawTitle = typeof active.title === 'string' ? active.title.trim() : '';
459
+ const derivedTitle = deriveSessionTitle(active.runs, task);
460
+ const titleSource = normalizeTitleSource(active.titleSource, rawTitle, derivedTitle);
461
+ const next = {
462
+ ...active,
463
+ task: draft,
464
+ updatedAt: new Date().toISOString(),
465
+ title: normalizeSessionTitle(rawTitle, derivedTitle, titleSource),
466
+ titleSource
467
+ };
468
+ return {
469
+ ...state,
470
+ sessions: state.sessions.map(session => session === active ? next : session),
471
+ task: next.task
472
+ };
473
+ }
474
+
435
475
  function setActiveSession(state, sessionId) {
436
476
  if (!(state.sessions || []).some(session => session.id === sessionId)) {
437
477
  return mirrorActiveSession(state);
@@ -642,7 +682,33 @@
642
682
  finishedAt: Number.isFinite(Date.parse(run.finishedAt)) ? run.finishedAt : event.timestamp };
643
683
  }
644
684
 
685
+ function pruneRunRecoveryScope(run, session = {}) {
686
+ const wrongAccount = proof => Object.prototype.hasOwnProperty.call(session, 'accountScopeId')
687
+ && proof.accountScopeId !== session.accountScopeId;
688
+ const wrongProject = value => Boolean(session.projectId && value !== session.projectId);
689
+ if (run.pendingDirectoryCleanup && (run.pendingDirectoryCleanup.sessionId !== session.id
690
+ || wrongAccount(run.pendingDirectoryCleanup) || wrongProject(run.pendingDirectoryCleanup.projectId))) delete run.pendingDirectoryCleanup;
691
+ applyTrackedChangeStatus(run, run.trackedChangeStatus);
692
+ }
693
+
694
+ function normalizeRecoveryRetention(value) {
695
+ if (value?.state !== 'omitted') return undefined;
696
+ return { state: 'omitted', reason: ['run-budget', 'session-budget', 'fallback', 'invalid'].includes(value.reason)
697
+ ? value.reason : 'invalid', bytes: Number.isSafeInteger(value.bytes) && value.bytes >= 0 ? value.bytes : 0,
698
+ ...(value.directoryCleanup === true ? { directoryCleanup: true, ...(value.coreRetained === true ? { coreRetained: true } : {}) } : {}) };
699
+ }
700
+
701
+ // Normalization makes UI copies of the same record. This identity is page-local:
702
+ // it never travels through JSON/storage and is never shared by a replacement run.
703
+ const runIdentities = new WeakMap();
704
+ function sameRunIdentity(left, right) {
705
+ return Boolean(left && right && (left === right
706
+ || (runIdentities.has(left) && runIdentities.get(left) === runIdentities.get(right))));
707
+ }
708
+
645
709
  function normalizeRun(run, options = {}) {
710
+ const sourceRun = run;
711
+
646
712
  if (options.restoreRunningRuns === true || options.recoverSettledRuns === true) run = recoverSettledRunningRun(run);
647
713
  run = recoverRecordedRunFailure(run, options.locale);
648
714
  const shouldStopRestoredRun = options.restoreRunningRuns === true && run.status === 'running';
@@ -658,6 +724,12 @@
658
724
  status: 'failed',
659
725
  kind: 'report',
660
726
  detail: i18n.t(locale, 'restoredRunStoppedDetail'),
727
+ detailStructured: {
728
+ conclusion: i18n.t(locale, 'restoredRunStoppedDetail'),
729
+ conclusionSource: { origin: 'product', key: 'reload_interrupted' },
730
+ // Ask forbids writeback; an interrupted Auto run has no zero-write proof.
731
+ meta: run.mode === 'ask' ? [{ key: 'writeResult', value: 'wrote 0 items, skipped 0 items' }] : []
732
+ },
661
733
  failure: { code: 'native_request_failed', source: 'panel_reload' },
662
734
  timestamp: new Date().toISOString()
663
735
  });
@@ -687,9 +759,14 @@
687
759
  undoBaseFiles: normalizeRunFiles(run.undoBaseFiles),
688
760
  undoTrackedChanges: normalizeRunTrackedChanges(run.undoTrackedChanges),
689
761
  undoExpectedFiles: normalizeRunFiles(run.undoExpectedFiles),
762
+ ...(normalizeRecoveryRetention(run.recoveryRetention) && !run.forkSnapshot
763
+ ? { recoveryRetention: normalizeRecoveryRetention(run.recoveryRetention) } : {}),
690
764
  undoStatus: sanitizeAssistantVisibleText(run.undoStatus),
765
+ ...(run.undoOwnerProtocol === 1 ? { undoOwnerProtocol: 1 } : {}),
691
766
  nativeRequestId: sanitizeAssistantVisibleText(run.nativeRequestId),
767
+ ...(run.operation === 'compact' ? { operation: 'compact' } : {}),
692
768
  codexThreadId: sanitizeAssistantVisibleText(run.codexThreadId), codexTurnId: sanitizeAssistantVisibleText(run.codexTurnId),
769
+ contextUsage: run.forkSnapshot === true ? null : ContextUsage.normalize(run.contextUsage),
693
770
  forkSnapshot: run.forkSnapshot === true,
694
771
  forkSourceRunId: sanitizeAssistantVisibleText(run.forkSourceRunId),
695
772
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
@@ -712,9 +789,15 @@
712
789
  if (Array.isArray(run.trackedChangeCaptures) && run.trackedChangeCaptures.length && !run.forkSnapshot) {
713
790
  normalized.trackedChangeCaptures = sanitizeAssistantVisibleValue(run.trackedChangeCaptures);
714
791
  }
792
+ const directories = DirectoryCleanupProof.normalize(run.pendingDirectoryCleanup);
793
+ if (directories && directories.runId === normalized.id && directories.projectId === normalized.runProjectId
794
+ && !run.forkSnapshot) normalized.pendingDirectoryCleanup = directories;
715
795
  applyTrackedChangeStatus(normalized, run.trackedChangeStatus);
716
796
  Object.assign(normalized, pickWritebackRecovery(run));
717
797
 
798
+ const identity = runIdentities.get(sourceRun) || {};
799
+ runIdentities.set(sourceRun, identity);
800
+ runIdentities.set(normalized, identity);
718
801
  return normalized;
719
802
  }
720
803
 
@@ -724,6 +807,7 @@
724
807
  // output yields the same result.
725
808
  function applyTrackedChangeStatus(run, rawStatus) {
726
809
  const hasRefs = run.undoTrackedChanges.length > 0;
810
+ const hasFileUndo = (run.undoOperations || []).some(UndoOperations.isFileUndoOperation);
727
811
  const hasRawStatus = rawStatus !== undefined && rawStatus !== null && rawStatus !== '';
728
812
 
729
813
  // Step 1 — value recovery. Keep a stable value; recover any other present
@@ -740,12 +824,11 @@
740
824
  }
741
825
 
742
826
  // Step 1 — reload reconciliation. A non-terminal status (`pending`) with no
743
- // refs is the post-reload state of an un-acted tracked-change run: the heavy refs are
744
- // never persisted, so without them there is nothing to act on. Drop it so
745
- // the run returns to the legacy-undo world. A terminal status with no refs
827
+ // refs can result from an old record or a storage-budget omission. Drop
828
+ // pending and let actual legacy Undo material decide the action. A terminal status with no refs
746
829
  // is kept — step 3 already empties terminal payloads and the label stays
747
830
  // meaningful.
748
- if (!hasRefs && status === 'pending') {
831
+ if (!hasRefs && !hasFileUndo && status === 'pending') {
749
832
  status = undefined;
750
833
  }
751
834
 
@@ -762,7 +845,7 @@
762
845
 
763
846
  if (status !== undefined) {
764
847
  run.trackedChangeStatus = status;
765
- }
848
+ } else delete run.trackedChangeStatus;
766
849
  }
767
850
 
768
851
  // Welcome-panel + write-guard: the run lifecycle now
@@ -1034,6 +1117,17 @@
1034
1117
  return result;
1035
1118
  }
1036
1119
 
1120
+ function normalizeCodexLocalSkillEnabled(value) {
1121
+ const result = {};
1122
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return result;
1123
+ for (const [id, enabled] of Object.entries(value)) {
1124
+ if (/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(id) && !id.includes('..')
1125
+ && !['__proto__', 'prototype', 'constructor'].includes(id) && typeof enabled === 'boolean') result[id] = enabled;
1126
+ }
1127
+ if (Object.keys(result).length > 200) throw new Error('Too many skill preferences.');
1128
+ return result;
1129
+ }
1130
+
1037
1131
  function normalizeProjectPrefKey(value) {
1038
1132
  const key = typeof value === 'string' ? value.trim() : '';
1039
1133
  if (!key) {
@@ -1105,6 +1199,7 @@
1105
1199
  loadCodexLocalSkills: source.loadCodexLocalSkills !== false,
1106
1200
  loadCodexOverleafSkills: source.loadCodexOverleafSkills !== false,
1107
1201
  codexOverleafSkillEnabled: normalizeCodexOverleafSkillEnabled(source.codexOverleafSkillEnabled),
1202
+ codexLocalSkillEnabled: normalizeCodexLocalSkillEnabled(source.codexLocalSkillEnabled),
1108
1203
  theme: ['dark', 'light', 'auto'].includes(source.theme) ? source.theme : 'dark',
1109
1204
  panelWidth: normalizePanelWidth(source.panelWidth),
1110
1205
  task: summarizeTextForStorage(active?.task || source.task || '', 'task'),
@@ -1190,6 +1285,7 @@
1190
1285
  selectionContext: SelectionContext.normalize(session.selectionContext),
1191
1286
  projectReferenceFiles: normalizeProjectReferenceFiles(session.projectReferenceFiles),
1192
1287
  codexThreadId: typeof session.codexThreadId === 'string' ? session.codexThreadId : '',
1288
+ contextUsage: ContextUsage.normalize(session.contextUsage),
1193
1289
  forkedFromSessionId: normalizeTextField(session.forkedFromSessionId, 160),
1194
1290
  forkedFromRunId: normalizeTextField(session.forkedFromRunId, 160),
1195
1291
  pendingInputs: normalizePendingInputs(session.pendingInputs)
@@ -1278,19 +1374,32 @@
1278
1374
  events: compactRunEvents(run.events, limits),
1279
1375
  ...(run.subagents?.length ? { subagents: SubagentActivityModel.normalize(run.subagents, sanitizeAssistantVisibleText) } : {}),
1280
1376
  attachments: compactRunAttachmentsForStorage(run.attachments, limits),
1281
- appliedOperations: [],
1377
+ appliedOperations: UndoOperations.compactUnavailableUndoOperations(run),
1282
1378
  undoOperations: undoPayload.undoOperations,
1283
1379
  undoBaseFiles: undoPayload.undoBaseFiles,
1284
1380
  undoTrackedChanges: undoPayload.undoTrackedChanges,
1285
1381
  undoExpectedFiles: undoPayload.undoExpectedFiles,
1382
+ ...(!run.forkSnapshot && (run.pendingDirectoryCleanup || run.recoveryRetention?.directoryCleanup === true || (run.undoStatus !== 'applied' && !['accepted', 'rejected'].includes(run.trackedChangeStatus)))
1383
+ && (run.pendingDirectoryCleanup || run.recoveryRetention?.state === 'omitted' || ['appliedOperations', 'undoOperations', 'undoTrackedChanges', 'undoExpectedFiles']
1384
+ .some(key => Array.isArray(run[key]) && run[key].length))
1385
+ ? { recoveryRetention: normalizeRecoveryRetention(run.recoveryRetention)
1386
+ || { state: 'omitted', reason: 'fallback', bytes: 0, ...(run.pendingDirectoryCleanup ? { directoryCleanup: true } : {}) } } : {}),
1286
1387
  undoStatus: summarizeTextForStorage(run.undoStatus, 'undo status'),
1388
+ ...(run.undoOwnerProtocol === 1 ? { undoOwnerProtocol: 1 } : {}),
1287
1389
  nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
1390
+ ...(run.operation === 'compact' ? { operation: 'compact' } : {}),
1288
1391
  codexThreadId: normalizeTextField(run.codexThreadId, 160), codexTurnId: normalizeTextField(run.codexTurnId, 160),
1392
+ contextUsage: run.forkSnapshot === true ? null : ContextUsage.normalize(run.contextUsage),
1289
1393
  forkSnapshot: run.forkSnapshot === true,
1290
1394
  forkSourceRunId: normalizeTextField(run.forkSourceRunId, 160),
1291
1395
  nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
1292
1396
  queueItemId: normalizeTextField(run.queueItemId, 160)
1293
1397
  };
1398
+ if (compact.recoveryRetention?.coreRetained === true && run.undoStatus !== 'applied'
1399
+ && !TERMINAL_TRACKED_CHANGE_STATUS.has(run.trackedChangeStatus)
1400
+ && ['appliedOperations', 'undoOperations', 'undoTrackedChanges', 'undoExpectedFiles'].some(key => run[key]?.length)) {
1401
+ delete compact.recoveryRetention.coreRetained;
1402
+ }
1294
1403
  if (run.executionSnapshot && typeof run.executionSnapshot === 'object') {
1295
1404
  compact.executionSnapshot = sanitizeAssistantVisibleValue(run.executionSnapshot);
1296
1405
  }
@@ -1577,21 +1686,22 @@
1577
1686
  if (typeof value !== 'string' || !value) {
1578
1687
  return '';
1579
1688
  }
1580
- const secretRedacted = redactSecretLikeText(value);
1581
- if (!mightContainLocalReferenceText(secretRedacted)) {
1582
- return secretRedacted;
1689
+ // Conversation text is user/model content, not a credential field. Keep
1690
+ // examples and quoted values intact; local-reference display rules remain.
1691
+ if (!mightContainLocalReferenceText(value)) {
1692
+ return value;
1583
1693
  }
1584
1694
  if (LineReferences?.sanitizeLocalReferences) {
1585
1695
  try {
1586
- return LineReferences.sanitizeLocalReferences(secretRedacted, {
1696
+ return LineReferences.sanitizeLocalReferences(value, {
1587
1697
  projectFiles: [],
1588
1698
  context: 'persist'
1589
1699
  });
1590
1700
  } catch (_error) {
1591
- return fallbackSanitizeLocalReferences(secretRedacted);
1701
+ return fallbackSanitizeLocalReferences(value);
1592
1702
  }
1593
1703
  }
1594
- return fallbackSanitizeLocalReferences(secretRedacted);
1704
+ return fallbackSanitizeLocalReferences(value);
1595
1705
  }
1596
1706
 
1597
1707
  function mightContainLocalReferenceText(value) {
@@ -1619,18 +1729,6 @@
1619
1729
  });
1620
1730
  }
1621
1731
 
1622
- function redactSecretLikeText(value) {
1623
- if (typeof value !== 'string') {
1624
- return '';
1625
- }
1626
- let redacted = value;
1627
- for (const pattern of SECRET_REDACTION_PATTERNS) {
1628
- pattern.lastIndex = 0;
1629
- redacted = redacted.replace(pattern, REDACTED_SECRET);
1630
- }
1631
- return redacted;
1632
- }
1633
-
1634
1732
  function containsSecretLikeText(value) {
1635
1733
  if (typeof value === 'string') {
1636
1734
  return SECRET_REDACTION_PATTERNS.some(pattern => {
@@ -1703,6 +1801,9 @@
1703
1801
  }
1704
1802
 
1705
1803
  return {
1804
+ sameRunIdentity,
1805
+ normalizeRecoveryRetention,
1806
+ pruneRunRecoveryScope,
1706
1807
  pickWritebackRecovery,
1707
1808
  DEFAULT_PANEL_STATE,
1708
1809
  createSession,
@@ -1716,11 +1817,14 @@
1716
1817
  selectVisibleSessionsForList,
1717
1818
  setActiveSession,
1718
1819
  updateActiveSession,
1820
+ updateActiveSessionDraft,
1719
1821
  normalizeRuns,
1720
1822
  normalizeRunStatus,
1721
1823
  recoverRecordedRunFailure,
1722
1824
  recoverSettledRunningRun,
1825
+ normalizeCodexLocalSkillEnabled,
1723
1826
  normalizeProjectReferenceFiles,
1827
+ normalizeComposerAttachmentsForPersistence,
1724
1828
  normalizeSelectionContext: SelectionContext.normalize,
1725
1829
  prepareStateForStorage,
1726
1830
  estimateJsonBytes,
@@ -25,6 +25,19 @@
25
25
  }
26
26
 
27
27
  function fromPrompt(value, { sanitize = text => text, maxChars = 24 } = {}) {
28
+ let title = projectPrompt(value, { sanitize, maxChars });
29
+ // Display titles pass through normalization again on unrelated callbacks.
30
+ // Finish removing nested formatting/prefixes now, before acknowledging a title.
31
+ // The bound guards against an input that never settles.
32
+ for (let pass = 0; pass < 8; pass += 1) {
33
+ const next = projectPrompt(title, { sanitize, maxChars });
34
+ if (next === title) return title;
35
+ title = next;
36
+ }
37
+ return title;
38
+ }
39
+
40
+ function projectPrompt(value, { sanitize, maxChars }) {
28
41
  if (typeof value !== 'string' || !value.trim()) return '';
29
42
  const source = String(sanitize(value.slice(0, INPUT_LIMIT)) || '');
30
43
  let fence = null;
@@ -75,7 +88,7 @@
75
88
  .replace(/\[(?:REDACTED_SECRET|local path(?::\d+)?)\]/gi, ' ')
76
89
  .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
77
90
  .replace(/\s+/g, ' ').trim()
78
- .replace(/^(?:\u8bf7(?:\u4f60)?\u5e2e\u6211|\u8bf7\u4f60|\u5e2e\u6211|\u9ebb\u70e6\u4f60(?:\u5e2e\u6211)?|\u9ebb\u70e6\u5e2e\u6211|please\s+)\s*/i, '');
91
+ .replace(/^(?:(?:\u8bf7(?:\u4f60)?\u5e2e\u6211|\u8bf7\u4f60|\u5e2e\u6211|\u9ebb\u70e6\u4f60(?:\u5e2e\u6211)?|\u9ebb\u70e6\u5e2e\u6211|please\s+)\s*)+/i, '');
79
92
  }
80
93
 
81
94
  function isScaffolding(value) {
@@ -157,6 +157,9 @@
157
157
  canAccept,
158
158
  canUndo,
159
159
  canRetry: Boolean(primaryFailure?.retryable),
160
+ requiresWriteReview: Boolean(facts && (facts.documentEffect === 'possibly-changed'
161
+ || facts.evidence?.settled === 'needs-review'
162
+ || facts.failures.some(failure => DefaultFailureReasons.isUnconfirmedWriteFailure?.(failure)))),
160
163
  terminalState: failureTerminalState || '',
161
164
  actions: { accept: canAccept, undo: canUndo }
162
165
  };
@@ -209,6 +212,10 @@
209
212
  file: safePath(item.file),
210
213
  operationType: text(item.operationType).slice(0, 80),
211
214
  changedDocument: item.changedDocument === true,
215
+ ...(item.mayHaveMutated === true ? { mayHaveMutated: true } : {}),
216
+ ...(item.evidence?.originalCode === 'native_untracked_undo_pending_changes'
217
+ && item.changedDocument !== true && item.mayHaveMutated !== true && item.evidence.writeStarted !== true
218
+ ? { evidence: { originalCode: 'native_untracked_undo_pending_changes' } } : {}),
212
219
  terminalState: normalizeTerminalState(item.terminalState)
213
220
  }));
214
221
  }
@@ -287,8 +294,9 @@
287
294
  }
288
295
 
289
296
  function selectPrimaryFailure(failures) {
290
- return DefaultFailureReasons?.selectPrimaryFailure instanceof Function
291
- ? DefaultFailureReasons.selectPrimaryFailure(failures)
297
+ const select = DefaultFailureReasons?.selectPrimaryWritebackFailure || DefaultFailureReasons?.selectPrimaryFailure;
298
+ return select instanceof Function
299
+ ? select(failures)
292
300
  : failures[0] || null;
293
301
  }
294
302