codex-overleaf-link 1.3.7 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -21
- package/extension/src/shared/agentTranscript.js +273 -10
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/failureReasons.js +578 -0
- package/extension/src/shared/i18n.js +139 -2
- package/extension/src/shared/pathRedaction.js +143 -0
- package/extension/src/shared/sessionState.js +113 -4
- package/extension/src/shared/storageDb.js +217 -4
- package/native-host/src/codexPromptAssembly.js +10 -2
- package/native-host/src/codexSessionRunner.js +33 -1
- package/package.json +1 -1
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
(function initFailureReasons(root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) {
|
|
3
|
+
module.exports = factory();
|
|
4
|
+
} else {
|
|
5
|
+
root.CodexOverleafFailureReasons = factory();
|
|
6
|
+
}
|
|
7
|
+
})(typeof globalThis !== 'undefined' ? globalThis : window, function failureReasonsFactory() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* All stages a FailureReason can attach to. Source: design spec §8.
|
|
12
|
+
* @type {Set<string>}
|
|
13
|
+
*/
|
|
14
|
+
const FAILURE_STAGES = new Set([
|
|
15
|
+
'context', 'codex', 'navigation', 'preflight', 'write', 'verify',
|
|
16
|
+
'reviewing', 'undo', 'accept', 'native', 'storage', 'unknown'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Severity vocabulary. Source: design spec §7.
|
|
21
|
+
* @type {Set<string>}
|
|
22
|
+
*/
|
|
23
|
+
const FAILURE_SEVERITIES = new Set(['info', 'warning', 'error', 'blocked']);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Terminal-state vocabulary. Source: design spec §7.
|
|
27
|
+
* @type {Set<string>}
|
|
28
|
+
*/
|
|
29
|
+
const TERMINAL_STATES = new Set(['failed', 'blocked', 'degraded', 'needs_review']);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Allowed severity × terminalState pairs per design spec §7.
|
|
33
|
+
* @type {Record<string, Set<string>>}
|
|
34
|
+
*/
|
|
35
|
+
const ALLOWED_SEVERITY_PER_TERMINAL = {
|
|
36
|
+
blocked: new Set(['blocked']),
|
|
37
|
+
failed: new Set(['error', 'blocked']),
|
|
38
|
+
needs_review: new Set(['warning', 'error']),
|
|
39
|
+
degraded: new Set(['info', 'warning'])
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Code catalog — keyed by code. Pulled verbatim from §9 of the spec.
|
|
44
|
+
* Each entry: { stage, severity, defaultRetryable, fallbackUserMessage, fallbackNextAction }.
|
|
45
|
+
* Used by `normalizeFailureReason` to fill in defaults for legacy results.
|
|
46
|
+
* @type {Record<string, {stage: string, severity: string, defaultRetryable: boolean, fallbackUserMessage: string, fallbackNextAction: string}>}
|
|
47
|
+
*/
|
|
48
|
+
const FAILURE_CODE_CATALOG = {
|
|
49
|
+
// 9.0 Context
|
|
50
|
+
project_snapshot_unavailable: {
|
|
51
|
+
stage: 'context', severity: 'error', defaultRetryable: true,
|
|
52
|
+
fallbackUserMessage: 'Codex could not read the Overleaf project snapshot.',
|
|
53
|
+
fallbackNextAction: 'Refresh Overleaf, then rerun the task.'
|
|
54
|
+
},
|
|
55
|
+
selected_context_unresolved: {
|
|
56
|
+
stage: 'context', severity: 'warning', defaultRetryable: true,
|
|
57
|
+
fallbackUserMessage: 'Codex could not resolve the requested selection or context.',
|
|
58
|
+
fallbackNextAction: 'Select the target again or specify the file/section explicitly.'
|
|
59
|
+
},
|
|
60
|
+
base_file_missing_at_start: {
|
|
61
|
+
stage: 'context', severity: 'error', defaultRetryable: true,
|
|
62
|
+
fallbackUserMessage: 'A file needed for this task was missing during the initial project read.',
|
|
63
|
+
fallbackNextAction: 'Refresh project context and rerun.'
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// 9.1 Navigation
|
|
67
|
+
target_file_missing_path: {
|
|
68
|
+
stage: 'navigation', severity: 'error', defaultRetryable: true,
|
|
69
|
+
fallbackUserMessage: 'The change did not include a target file path.',
|
|
70
|
+
fallbackNextAction: 'Retry after regenerating the change; report a bug if repeated.'
|
|
71
|
+
},
|
|
72
|
+
target_file_not_found: {
|
|
73
|
+
stage: 'navigation', severity: 'blocked', defaultRetryable: true,
|
|
74
|
+
fallbackUserMessage: 'Codex could not find the target file in this Overleaf project.',
|
|
75
|
+
fallbackNextAction: 'Check the file name/path in Overleaf and retry.'
|
|
76
|
+
},
|
|
77
|
+
target_file_open_failed: {
|
|
78
|
+
stage: 'navigation', severity: 'blocked', defaultRetryable: true,
|
|
79
|
+
fallbackUserMessage: 'Codex could not open the target file in Overleaf.',
|
|
80
|
+
fallbackNextAction: 'Expand the folder or manually open the file, then retry.'
|
|
81
|
+
},
|
|
82
|
+
target_file_not_active: {
|
|
83
|
+
stage: 'navigation', severity: 'blocked', defaultRetryable: true,
|
|
84
|
+
fallbackUserMessage: 'Codex tried to write the target file, but another file was active at write time.',
|
|
85
|
+
fallbackNextAction: 'Open the target file in Overleaf, then retry this run.'
|
|
86
|
+
},
|
|
87
|
+
target_editor_not_ready: {
|
|
88
|
+
stage: 'navigation', severity: 'blocked', defaultRetryable: true,
|
|
89
|
+
fallbackUserMessage: 'Target file is active but the editor was not ready before timeout.',
|
|
90
|
+
fallbackNextAction: 'Wait for Overleaf to finish loading, then retry.'
|
|
91
|
+
},
|
|
92
|
+
target_file_focus_lost: {
|
|
93
|
+
stage: 'navigation', severity: 'blocked', defaultRetryable: true,
|
|
94
|
+
fallbackUserMessage: 'Target file became active, then focus switched before the write landed.',
|
|
95
|
+
fallbackNextAction: 'Avoid switching files during writeback and retry.'
|
|
96
|
+
},
|
|
97
|
+
jump_target_not_resolved: {
|
|
98
|
+
stage: 'navigation', severity: 'warning', defaultRetryable: false,
|
|
99
|
+
fallbackUserMessage: 'A line/position link could not map to an editor range.',
|
|
100
|
+
fallbackNextAction: 'Open the file manually and inspect the referenced line.'
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// 9.2 Preflight
|
|
104
|
+
missing_base_file: {
|
|
105
|
+
stage: 'preflight', severity: 'blocked', defaultRetryable: true,
|
|
106
|
+
fallbackUserMessage: 'A write safety check needed a base file that is missing from the run snapshot.',
|
|
107
|
+
fallbackNextAction: 'Refresh project context and rerun.'
|
|
108
|
+
},
|
|
109
|
+
stale_source_changed: {
|
|
110
|
+
stage: 'preflight', severity: 'blocked', defaultRetryable: true,
|
|
111
|
+
fallbackUserMessage: 'The file changed while Codex was working.',
|
|
112
|
+
fallbackNextAction: 'Review the current file, then rerun the task.'
|
|
113
|
+
},
|
|
114
|
+
patch_anchor_not_found: {
|
|
115
|
+
stage: 'preflight', severity: 'blocked', defaultRetryable: true,
|
|
116
|
+
fallbackUserMessage: 'The edit anchor no longer matches current Overleaf content.',
|
|
117
|
+
fallbackNextAction: 'Rerun the task against the current document.'
|
|
118
|
+
},
|
|
119
|
+
patch_anchor_ambiguous: {
|
|
120
|
+
stage: 'preflight', severity: 'blocked', defaultRetryable: true,
|
|
121
|
+
fallbackUserMessage: 'The edit anchor matches multiple locations.',
|
|
122
|
+
fallbackNextAction: 'Narrow the request or select a smaller region.'
|
|
123
|
+
},
|
|
124
|
+
delete_confirmation_missing: {
|
|
125
|
+
stage: 'preflight', severity: 'blocked', defaultRetryable: true,
|
|
126
|
+
fallbackUserMessage: 'A destructive delete was not confirmed.',
|
|
127
|
+
fallbackNextAction: 'Confirm the delete explicitly if intended.'
|
|
128
|
+
},
|
|
129
|
+
binary_write_blocked: {
|
|
130
|
+
stage: 'preflight', severity: 'blocked', defaultRetryable: false,
|
|
131
|
+
fallbackUserMessage: 'Codex tried to write unsupported binary content.',
|
|
132
|
+
fallbackNextAction: 'Upload the file manually or attach a supported text file.'
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// 9.3 Write/verify
|
|
136
|
+
write_operation_failed: {
|
|
137
|
+
stage: 'write', severity: 'error', defaultRetryable: true,
|
|
138
|
+
fallbackUserMessage: 'Editor write call failed.',
|
|
139
|
+
fallbackNextAction: 'Retry after the editor is stable.'
|
|
140
|
+
},
|
|
141
|
+
write_timeout: {
|
|
142
|
+
stage: 'write', severity: 'error', defaultRetryable: true,
|
|
143
|
+
fallbackUserMessage: 'Editor did not accept the write before timeout.',
|
|
144
|
+
fallbackNextAction: 'Refresh Overleaf and retry.'
|
|
145
|
+
},
|
|
146
|
+
write_observed_mismatch: {
|
|
147
|
+
stage: 'verify', severity: 'error', defaultRetryable: false,
|
|
148
|
+
fallbackUserMessage: 'Codex attempted to write, but the content read back from Overleaf did not match the approved change.',
|
|
149
|
+
fallbackNextAction: 'Open Technical Details and compare expected vs observed.'
|
|
150
|
+
},
|
|
151
|
+
write_post_read_failed: {
|
|
152
|
+
stage: 'verify', severity: 'error', defaultRetryable: true,
|
|
153
|
+
fallbackUserMessage: 'Codex could not read the file after writing.',
|
|
154
|
+
fallbackNextAction: 'Check Overleaf manually, then refresh and retry.'
|
|
155
|
+
},
|
|
156
|
+
file_tree_verification_failed: {
|
|
157
|
+
stage: 'verify', severity: 'error', defaultRetryable: true,
|
|
158
|
+
fallbackUserMessage: 'Create/delete/rename/move did not appear in the file tree as expected.',
|
|
159
|
+
fallbackNextAction: 'Inspect the file tree and retry if needed.'
|
|
160
|
+
},
|
|
161
|
+
partial_write_needs_review: {
|
|
162
|
+
stage: 'verify', severity: 'warning', defaultRetryable: false,
|
|
163
|
+
fallbackUserMessage: 'Some operations wrote, some were skipped.',
|
|
164
|
+
fallbackNextAction: 'Review written files; use Undo written parts if needed.'
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// 9.0+ — write-safety primitive (welcome-panel + writeback project-ID guard v1.3.8 add-on)
|
|
168
|
+
aborted_project_changed: {
|
|
169
|
+
stage: 'write', severity: 'blocked', defaultRetryable: true,
|
|
170
|
+
fallbackUserMessage: 'Codex stopped a write because Overleaf switched to a different project mid-run.',
|
|
171
|
+
fallbackNextAction: 'Reopen the original project and rerun the task if you still want this change.'
|
|
172
|
+
},
|
|
173
|
+
editor_project_id_unavailable: {
|
|
174
|
+
stage: 'write', severity: 'blocked', defaultRetryable: true,
|
|
175
|
+
fallbackUserMessage: 'Codex could not confirm which Overleaf project the editor is currently showing, so it did not write.',
|
|
176
|
+
fallbackNextAction: 'Refresh the Overleaf tab and retry; if it persists, reload the extension.'
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// 9.4 Reviewing
|
|
180
|
+
reviewing_state_unknown: {
|
|
181
|
+
stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
|
|
182
|
+
fallbackUserMessage: 'Codex could not determine whether Reviewing/Track Changes is enabled.',
|
|
183
|
+
fallbackNextAction: 'Check Overleaf mode manually before retrying.'
|
|
184
|
+
},
|
|
185
|
+
reviewing_enable_failed: {
|
|
186
|
+
stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
|
|
187
|
+
fallbackUserMessage: 'Codex could not enable Reviewing when it needed to.',
|
|
188
|
+
fallbackNextAction: 'Enable Reviewing manually or retry.'
|
|
189
|
+
},
|
|
190
|
+
reviewing_disable_failed: {
|
|
191
|
+
stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
|
|
192
|
+
fallbackUserMessage: 'Codex could not switch to Editing / no-trace mode.',
|
|
193
|
+
fallbackNextAction: 'Switch to Editing manually, wait briefly, then retry.'
|
|
194
|
+
},
|
|
195
|
+
editing_not_confirmed: {
|
|
196
|
+
stage: 'reviewing', severity: 'blocked', defaultRetryable: true,
|
|
197
|
+
fallbackUserMessage: 'Editing mode could not be proven stable.',
|
|
198
|
+
fallbackNextAction: 'Do not write; check the mode selector and retry.'
|
|
199
|
+
},
|
|
200
|
+
tracked_changes_created_unexpectedly: {
|
|
201
|
+
stage: 'reviewing', severity: 'warning', defaultRetryable: true,
|
|
202
|
+
fallbackUserMessage: 'A replay intended to be untracked created Track Changes.',
|
|
203
|
+
fallbackNextAction: 'Inspect Overleaf Reviewing and decide whether to accept or reject.'
|
|
204
|
+
},
|
|
205
|
+
tracked_changes_remain: {
|
|
206
|
+
stage: 'reviewing', severity: 'warning', defaultRetryable: true,
|
|
207
|
+
fallbackUserMessage: 'Accept/reject operation finished but tracked changes remain.',
|
|
208
|
+
fallbackNextAction: 'Open Overleaf Reviewing and inspect remaining changes.'
|
|
209
|
+
},
|
|
210
|
+
tracked_change_nodes_not_identified: {
|
|
211
|
+
stage: 'reviewing', severity: 'warning', defaultRetryable: false,
|
|
212
|
+
fallbackUserMessage: 'Codex wrote while Reviewing was enabled but cannot map generated tracked-change nodes.',
|
|
213
|
+
fallbackNextAction: 'Use Overleaf review tools manually; automatic tracked undo is disabled.'
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// 9.5 Undo
|
|
217
|
+
undo_preflight_content_drift: {
|
|
218
|
+
stage: 'undo', severity: 'blocked', defaultRetryable: false,
|
|
219
|
+
fallbackUserMessage: "Current content no longer matches this run's written content.",
|
|
220
|
+
fallbackNextAction: 'Review manual edits; Codex did not undo to avoid removing them.'
|
|
221
|
+
},
|
|
222
|
+
undo_operation_failed: {
|
|
223
|
+
stage: 'undo', severity: 'error', defaultRetryable: true,
|
|
224
|
+
fallbackUserMessage: 'Overleaf undo or reject operation failed.',
|
|
225
|
+
fallbackNextAction: 'Use Overleaf undo/review tools manually.'
|
|
226
|
+
},
|
|
227
|
+
undo_not_verified: {
|
|
228
|
+
stage: 'undo', severity: 'warning', defaultRetryable: true,
|
|
229
|
+
fallbackUserMessage: 'Undo ran, but Codex could not prove the file returned to pre-run content.',
|
|
230
|
+
fallbackNextAction: 'Inspect the file manually before continuing.'
|
|
231
|
+
},
|
|
232
|
+
undo_partial: {
|
|
233
|
+
stage: 'undo', severity: 'warning', defaultRetryable: true,
|
|
234
|
+
fallbackUserMessage: 'Some run writes were undone, some were skipped.',
|
|
235
|
+
fallbackNextAction: 'Review remaining files and retry undo if safe.'
|
|
236
|
+
},
|
|
237
|
+
undo_reviewing_restore_unverified: {
|
|
238
|
+
stage: 'undo', severity: 'info', defaultRetryable: true,
|
|
239
|
+
fallbackUserMessage: 'Undo completed but previous Reviewing state was not verified afterward.',
|
|
240
|
+
fallbackNextAction: 'Check Overleaf Reviewing/Editing mode manually.'
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// 9.6 Accept
|
|
244
|
+
accept_preflight_content_drift: {
|
|
245
|
+
stage: 'accept', severity: 'blocked', defaultRetryable: true,
|
|
246
|
+
fallbackUserMessage: "Content drifted from this run's writeback before accept.",
|
|
247
|
+
fallbackNextAction: 'Retry Accept changes after reviewing current content.'
|
|
248
|
+
},
|
|
249
|
+
accept_force_editing_failed: {
|
|
250
|
+
stage: 'accept', severity: 'blocked', defaultRetryable: true,
|
|
251
|
+
fallbackUserMessage: 'Codex could not enter stable Editing mode for untracked replay.',
|
|
252
|
+
fallbackNextAction: 'Switch to Editing manually and retry.'
|
|
253
|
+
},
|
|
254
|
+
accept_replay_failed: {
|
|
255
|
+
stage: 'accept', severity: 'error', defaultRetryable: true,
|
|
256
|
+
fallbackUserMessage: 'Replaying accepted content failed.',
|
|
257
|
+
fallbackNextAction: 'Inspect the file and retry.'
|
|
258
|
+
},
|
|
259
|
+
accept_replay_created_tracked_changes: {
|
|
260
|
+
stage: 'accept', severity: 'warning', defaultRetryable: true,
|
|
261
|
+
fallbackUserMessage: 'Replay created tracked changes, so accept cannot be proven clean.',
|
|
262
|
+
fallbackNextAction: 'Review remaining tracked changes manually.'
|
|
263
|
+
},
|
|
264
|
+
accept_not_verified: {
|
|
265
|
+
stage: 'accept', severity: 'warning', defaultRetryable: true,
|
|
266
|
+
fallbackUserMessage: 'Accept appeared to run but Codex could not prove final content/state.',
|
|
267
|
+
fallbackNextAction: 'Inspect Overleaf Reviewing before continuing.'
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
// 9.7 Native + Codex
|
|
271
|
+
native_bridge_unavailable: {
|
|
272
|
+
stage: 'native', severity: 'blocked', defaultRetryable: true,
|
|
273
|
+
fallbackUserMessage: 'Extension cannot connect to the Codex native host.',
|
|
274
|
+
fallbackNextAction: 'Run install-native or reload the extension.'
|
|
275
|
+
},
|
|
276
|
+
native_request_failed: {
|
|
277
|
+
stage: 'native', severity: 'error', defaultRetryable: true,
|
|
278
|
+
fallbackUserMessage: 'Native host request failed.',
|
|
279
|
+
fallbackNextAction: 'Open diagnostics and retry.'
|
|
280
|
+
},
|
|
281
|
+
native_protocol_incompatible: {
|
|
282
|
+
stage: 'native', severity: 'blocked', defaultRetryable: true,
|
|
283
|
+
fallbackUserMessage: 'Native host version or capabilities do not match the extension.',
|
|
284
|
+
fallbackNextAction: 'Update the native host.'
|
|
285
|
+
},
|
|
286
|
+
codex_no_usable_result: {
|
|
287
|
+
stage: 'codex', severity: 'error', defaultRetryable: true,
|
|
288
|
+
fallbackUserMessage: 'Local Codex returned no usable final report or operations.',
|
|
289
|
+
fallbackNextAction: 'Open Technical Details and resolve the local Codex error.'
|
|
290
|
+
},
|
|
291
|
+
codex_project_locked: {
|
|
292
|
+
stage: 'codex', severity: 'blocked', defaultRetryable: true,
|
|
293
|
+
fallbackUserMessage: 'Another Codex task is already running for this Overleaf project.',
|
|
294
|
+
fallbackNextAction: 'Wait for the active task to finish, or cancel it before retrying.'
|
|
295
|
+
},
|
|
296
|
+
codex_result_parse_failed: {
|
|
297
|
+
stage: 'codex', severity: 'error', defaultRetryable: true,
|
|
298
|
+
fallbackUserMessage: 'Codex output could not be parsed into operations.',
|
|
299
|
+
fallbackNextAction: 'Retry with a simpler request; report if repeated.'
|
|
300
|
+
},
|
|
301
|
+
codex_run_cancelled: {
|
|
302
|
+
stage: 'codex', severity: 'info', defaultRetryable: true,
|
|
303
|
+
fallbackUserMessage: 'The local run was cancelled.',
|
|
304
|
+
fallbackNextAction: 'Start a new run.'
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
// 9.8 Storage
|
|
308
|
+
storage_quota_exceeded: {
|
|
309
|
+
stage: 'storage', severity: 'warning', defaultRetryable: true,
|
|
310
|
+
fallbackUserMessage: 'Browser storage quota was exceeded.',
|
|
311
|
+
fallbackNextAction: 'Clear old run history or reduce attachments.'
|
|
312
|
+
},
|
|
313
|
+
run_state_persist_failed: {
|
|
314
|
+
stage: 'storage', severity: 'warning', defaultRetryable: true,
|
|
315
|
+
fallbackUserMessage: 'Current run state could not be saved.',
|
|
316
|
+
fallbackNextAction: "Continue carefully; refresh may lose this run's recovery state."
|
|
317
|
+
},
|
|
318
|
+
receipt_persist_failed: {
|
|
319
|
+
stage: 'storage', severity: 'warning', defaultRetryable: true,
|
|
320
|
+
fallbackUserMessage: 'Writeback receipt could not be saved.',
|
|
321
|
+
fallbackNextAction: 'Export/copy Technical Details before refreshing.'
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
// Defensive fallback
|
|
325
|
+
unknown_legacy_failure: {
|
|
326
|
+
stage: 'unknown', severity: 'error', defaultRetryable: false,
|
|
327
|
+
fallbackUserMessage: 'Codex could not classify this failure.',
|
|
328
|
+
fallbackNextAction: 'Open Technical Details for the raw report.'
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Legacy code → canonical code mapping per design spec §14.
|
|
334
|
+
* Function-valued entries inspect the legacy result for evidence flags before deciding.
|
|
335
|
+
* @type {Record<string, string | ((result: any) => string)>}
|
|
336
|
+
*/
|
|
337
|
+
const LEGACY_CODE_MAP = {
|
|
338
|
+
path_not_found: 'target_file_not_found',
|
|
339
|
+
file_open_failed: 'target_file_open_failed',
|
|
340
|
+
editor_not_ready: 'target_editor_not_ready',
|
|
341
|
+
stale_snapshot: 'stale_source_changed',
|
|
342
|
+
missing_base_file: 'missing_base_file',
|
|
343
|
+
reviewing_enable_failed: 'reviewing_enable_failed',
|
|
344
|
+
reviewing_disable_failed: 'reviewing_disable_failed',
|
|
345
|
+
editing_not_confirmed: 'editing_not_confirmed',
|
|
346
|
+
file_tree_verification_failed: 'file_tree_verification_failed',
|
|
347
|
+
native_connection_failed: 'native_bridge_unavailable',
|
|
348
|
+
reviewing_not_enabled: function reviewingNotEnabledMapper(result) {
|
|
349
|
+
const attempted = result && result.evidence && result.evidence.toggleAttempted === true;
|
|
350
|
+
return attempted ? 'reviewing_enable_failed' : 'reviewing_state_unknown';
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Validate a candidate FailureReason record against the design-spec §7 schema.
|
|
356
|
+
* Never throws. Returns `{ ok: true }` on success or `{ ok: false, reason: <code> }` on failure.
|
|
357
|
+
* @param {unknown} obj
|
|
358
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
359
|
+
*/
|
|
360
|
+
function validateFailureReason(obj) {
|
|
361
|
+
if (!obj || typeof obj !== 'object') {
|
|
362
|
+
return { ok: false, reason: 'not_object' };
|
|
363
|
+
}
|
|
364
|
+
if (typeof obj.code !== 'string' || !obj.code) {
|
|
365
|
+
return { ok: false, reason: 'missing_code' };
|
|
366
|
+
}
|
|
367
|
+
if (!FAILURE_STAGES.has(obj.stage)) {
|
|
368
|
+
return { ok: false, reason: 'invalid_stage' };
|
|
369
|
+
}
|
|
370
|
+
if (!FAILURE_SEVERITIES.has(obj.severity)) {
|
|
371
|
+
return { ok: false, reason: 'invalid_severity' };
|
|
372
|
+
}
|
|
373
|
+
if (typeof obj.userMessage !== 'string' || !obj.userMessage) {
|
|
374
|
+
return { ok: false, reason: 'missing_userMessage' };
|
|
375
|
+
}
|
|
376
|
+
if (typeof obj.retryable !== 'boolean') {
|
|
377
|
+
return { ok: false, reason: 'missing_retryable' };
|
|
378
|
+
}
|
|
379
|
+
if (obj.retryable === true && (typeof obj.nextAction !== 'string' || !obj.nextAction)) {
|
|
380
|
+
return { ok: false, reason: 'missing_nextAction_for_retryable' };
|
|
381
|
+
}
|
|
382
|
+
if (obj.terminalState !== undefined) {
|
|
383
|
+
if (!TERMINAL_STATES.has(obj.terminalState)) {
|
|
384
|
+
return { ok: false, reason: 'invalid_terminalState' };
|
|
385
|
+
}
|
|
386
|
+
const allowed = ALLOWED_SEVERITY_PER_TERMINAL[obj.terminalState];
|
|
387
|
+
if (!allowed || !allowed.has(obj.severity)) {
|
|
388
|
+
return { ok: false, reason: 'disallowed_severity_terminalState_pair' };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { ok: true };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Normalize a legacy `result` object (possibly null / malformed) into a valid FailureReason.
|
|
396
|
+
* Never throws. Prefers an explicit `result.failure` when valid; otherwise maps legacy
|
|
397
|
+
* `result.code` via `LEGACY_CODE_MAP` and falls back to `unknown_legacy_failure`.
|
|
398
|
+
*
|
|
399
|
+
* @param {unknown} result - The legacy result object from a writeback/page-bridge call.
|
|
400
|
+
* @param {{ path?: string, type?: string }} [operation] - Operation context to augment the failure with.
|
|
401
|
+
* @param {Record<string, unknown>} [context] - Reserved for future contextual hints (unused today).
|
|
402
|
+
* @returns {{ code: string, stage: string, severity: string, userMessage: string, retryable: boolean, nextAction?: string, file?: string, operationType?: string, evidence?: Record<string, unknown>, technicalMessage?: string }}
|
|
403
|
+
*/
|
|
404
|
+
function normalizeFailureReason(result, operation, context) {
|
|
405
|
+
void context;
|
|
406
|
+
const op = operation || {};
|
|
407
|
+
|
|
408
|
+
// Defensive repair for null / non-object / missing code.
|
|
409
|
+
if (!result || typeof result !== 'object') {
|
|
410
|
+
return buildFallbackFailureReason('unknown_legacy_failure', op);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Prefer explicit, valid failure record.
|
|
414
|
+
if (result.failure) {
|
|
415
|
+
const checked = validateFailureReason(result.failure);
|
|
416
|
+
if (checked.ok) {
|
|
417
|
+
return augmentWithOperation(result.failure, op);
|
|
418
|
+
}
|
|
419
|
+
// Invalid explicit failure — fall through to legacy normalization.
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const legacyCode = typeof result.code === 'string' ? result.code : '';
|
|
423
|
+
let canonicalCode = '';
|
|
424
|
+
const mapEntry = LEGACY_CODE_MAP[legacyCode];
|
|
425
|
+
if (mapEntry instanceof Function) {
|
|
426
|
+
canonicalCode = mapEntry(result);
|
|
427
|
+
} else if (typeof mapEntry === 'string') {
|
|
428
|
+
canonicalCode = mapEntry;
|
|
429
|
+
} else if (legacyCode && FAILURE_CODE_CATALOG[legacyCode]) {
|
|
430
|
+
// Already a canonical code.
|
|
431
|
+
canonicalCode = legacyCode;
|
|
432
|
+
} else {
|
|
433
|
+
canonicalCode = 'unknown_legacy_failure';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const catalog = FAILURE_CODE_CATALOG[canonicalCode] || FAILURE_CODE_CATALOG.unknown_legacy_failure;
|
|
437
|
+
const legacyReason = typeof result.reason === 'string' ? result.reason : undefined;
|
|
438
|
+
const failure = {
|
|
439
|
+
code: canonicalCode,
|
|
440
|
+
stage: catalog.stage,
|
|
441
|
+
severity: catalog.severity,
|
|
442
|
+
userMessage: legacyReason || catalog.fallbackUserMessage,
|
|
443
|
+
retryable: catalog.defaultRetryable,
|
|
444
|
+
nextAction: catalog.fallbackNextAction,
|
|
445
|
+
evidence: {}
|
|
446
|
+
};
|
|
447
|
+
if (legacyReason) {
|
|
448
|
+
failure.technicalMessage = legacyReason;
|
|
449
|
+
}
|
|
450
|
+
if (result.evidence && typeof result.evidence === 'object') {
|
|
451
|
+
Object.assign(failure.evidence, result.evidence);
|
|
452
|
+
}
|
|
453
|
+
if (legacyCode) {
|
|
454
|
+
failure.evidence.originalCode = legacyCode;
|
|
455
|
+
}
|
|
456
|
+
if (legacyReason) {
|
|
457
|
+
failure.evidence.originalReason = legacyReason;
|
|
458
|
+
}
|
|
459
|
+
// Strip undefined evidence keys.
|
|
460
|
+
for (const k of Object.keys(failure.evidence)) {
|
|
461
|
+
if (failure.evidence[k] === undefined) delete failure.evidence[k];
|
|
462
|
+
}
|
|
463
|
+
return augmentWithOperation(failure, op);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function augmentWithOperation(failure, op) {
|
|
467
|
+
const out = Object.assign({}, failure);
|
|
468
|
+
if (op && op.path && !out.file) out.file = op.path;
|
|
469
|
+
if (op && op.type && !out.operationType) out.operationType = op.type;
|
|
470
|
+
return out;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function buildFallbackFailureReason(code, op) {
|
|
474
|
+
const catalog = FAILURE_CODE_CATALOG[code] || FAILURE_CODE_CATALOG.unknown_legacy_failure;
|
|
475
|
+
return augmentWithOperation({
|
|
476
|
+
code: catalog === FAILURE_CODE_CATALOG[code] ? code : 'unknown_legacy_failure',
|
|
477
|
+
stage: catalog.stage,
|
|
478
|
+
severity: catalog.severity,
|
|
479
|
+
userMessage: catalog.fallbackUserMessage,
|
|
480
|
+
retryable: catalog.defaultRetryable,
|
|
481
|
+
nextAction: catalog.fallbackNextAction,
|
|
482
|
+
evidence: {}
|
|
483
|
+
}, op || {});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Severity ranking used by `selectPrimaryFailure`. Lower index = higher priority.
|
|
488
|
+
* Source: design spec §12.
|
|
489
|
+
*/
|
|
490
|
+
const SEVERITY_ORDER = { blocked: 0, error: 1, warning: 2, info: 3 };
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Stage tie-breaker order used by `selectPrimaryFailure` when two failures
|
|
494
|
+
* share severity. Source: design spec §12 — infrastructure first, then
|
|
495
|
+
* user-controlled blockers, then pipeline order, then auxiliary stages.
|
|
496
|
+
* @type {string[]}
|
|
497
|
+
*/
|
|
498
|
+
const STAGE_TIE_BREAKER_ORDER = [
|
|
499
|
+
'native', 'navigation', 'preflight', 'write', 'verify',
|
|
500
|
+
'reviewing', 'accept', 'undo', 'storage', 'codex', 'context', 'unknown'
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Pick the run-level primary failure from a list of per-operation FailureReason
|
|
505
|
+
* records. Orders first by severity (`blocked > error > warning > info`),
|
|
506
|
+
* then by stage tie-breaker per §12.
|
|
507
|
+
* @param {Array<{ stage?: string, severity?: string }>} failures
|
|
508
|
+
* @returns {object | null}
|
|
509
|
+
*/
|
|
510
|
+
function selectPrimaryFailure(failures) {
|
|
511
|
+
if (!Array.isArray(failures) || failures.length === 0) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
const ranked = failures.slice().sort(function compareFailures(a, b) {
|
|
515
|
+
const sevA = SEVERITY_ORDER[a && a.severity] !== undefined ? SEVERITY_ORDER[a.severity] : 99;
|
|
516
|
+
const sevB = SEVERITY_ORDER[b && b.severity] !== undefined ? SEVERITY_ORDER[b.severity] : 99;
|
|
517
|
+
if (sevA !== sevB) return sevA - sevB;
|
|
518
|
+
const stageA = STAGE_TIE_BREAKER_ORDER.indexOf((a && a.stage) || '');
|
|
519
|
+
const stageB = STAGE_TIE_BREAKER_ORDER.indexOf((b && b.stage) || '');
|
|
520
|
+
const normalizedA = stageA < 0 ? STAGE_TIE_BREAKER_ORDER.length : stageA;
|
|
521
|
+
const normalizedB = stageB < 0 ? STAGE_TIE_BREAKER_ORDER.length : stageB;
|
|
522
|
+
return normalizedA - normalizedB;
|
|
523
|
+
});
|
|
524
|
+
return ranked[0];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Render the localized user message + next action for a FailureReason. Looks up
|
|
529
|
+
* `failureReason_<code>_user` and `failureReason_<code>_next` through the supplied
|
|
530
|
+
* `i18nLookup(key)` accessor; falls back to the failure's own `userMessage` /
|
|
531
|
+
* `nextAction` (which already come from the §9 catalog) when the lookup misses.
|
|
532
|
+
* Interpolates `{file}`, `{activeFile}`, `{operationType}` from the failure record.
|
|
533
|
+
*
|
|
534
|
+
* @param {{ code: string, userMessage?: string, nextAction?: string, file?: string, activeFile?: string, operationType?: string }} failure
|
|
535
|
+
* @param {string} locale - Caller passes the active locale; reserved for future locale-aware fallback logic.
|
|
536
|
+
* @param {(key: string) => (string | undefined)} i18nLookup
|
|
537
|
+
* @returns {{ userMessage: string, nextAction: string | undefined }}
|
|
538
|
+
*/
|
|
539
|
+
function localizeFailureReason(failure, locale, i18nLookup) {
|
|
540
|
+
void locale;
|
|
541
|
+
const lookup = i18nLookup instanceof Function ? i18nLookup : function noopLookup() { return undefined; };
|
|
542
|
+
const code = failure && typeof failure.code === 'string' ? failure.code : '';
|
|
543
|
+
const userTemplate = code ? lookup('failureReason_' + code + '_user') : undefined;
|
|
544
|
+
const nextTemplate = code ? lookup('failureReason_' + code + '_next') : undefined;
|
|
545
|
+
const userMessage = userTemplate
|
|
546
|
+
? interpolateFailureTemplate(userTemplate, failure)
|
|
547
|
+
: (failure && failure.userMessage) || '';
|
|
548
|
+
const nextAction = nextTemplate
|
|
549
|
+
? interpolateFailureTemplate(nextTemplate, failure)
|
|
550
|
+
: (failure && failure.nextAction) || undefined;
|
|
551
|
+
return { userMessage, nextAction };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function interpolateFailureTemplate(template, failure) {
|
|
555
|
+
const file = failure && failure.file ? String(failure.file) : '';
|
|
556
|
+
const activeFile = failure && failure.activeFile ? String(failure.activeFile) : '';
|
|
557
|
+
const operationType = failure && failure.operationType ? String(failure.operationType) : '';
|
|
558
|
+
return String(template)
|
|
559
|
+
.replace(/\{file\}/g, file)
|
|
560
|
+
.replace(/\{activeFile\}/g, activeFile)
|
|
561
|
+
.replace(/\{operationType\}/g, operationType);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
FAILURE_STAGES,
|
|
566
|
+
FAILURE_SEVERITIES,
|
|
567
|
+
TERMINAL_STATES,
|
|
568
|
+
ALLOWED_SEVERITY_PER_TERMINAL,
|
|
569
|
+
FAILURE_CODE_CATALOG,
|
|
570
|
+
LEGACY_CODE_MAP,
|
|
571
|
+
SEVERITY_ORDER,
|
|
572
|
+
STAGE_TIE_BREAKER_ORDER,
|
|
573
|
+
validateFailureReason,
|
|
574
|
+
normalizeFailureReason,
|
|
575
|
+
selectPrimaryFailure,
|
|
576
|
+
localizeFailureReason
|
|
577
|
+
};
|
|
578
|
+
});
|