codex-overleaf-link 1.3.8 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <h1>Codex Overleaf Link</h1>
4
4
  <p><strong>Empower Overleaf with Codex.</strong></p>
5
5
  <p>
6
- <img src="https://img.shields.io/badge/version-1.3.8-blue" alt="version">
6
+ <img src="https://img.shields.io/badge/version-1.4.0-blue" alt="version">
7
7
  <img src="https://img.shields.io/badge/platform-macOS%20%2F%20Windows%20%2F%20Linux-lightgrey" alt="platform">
8
8
  <img src="https://img.shields.io/badge/chrome-MV3-green" alt="chrome manifest v3">
9
9
  <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="node version">
@@ -38,14 +38,14 @@ One command installs the native host **and** sets up the extension: the script r
38
38
  macOS / Linux:
39
39
 
40
40
  ```bash
41
- CODEX_OVERLEAF_REF=v1.3.8 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.3.8/install.sh)"
41
+ CODEX_OVERLEAF_REF=v1.4.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.4.0/install.sh)"
42
42
  ```
43
43
 
44
44
  Windows PowerShell:
45
45
 
46
46
  ```powershell
47
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.3.8/install.ps1 -OutFile install.ps1
48
- $env:CODEX_OVERLEAF_REF='v1.3.8'
47
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.4.0/install.ps1 -OutFile install.ps1
48
+ $env:CODEX_OVERLEAF_REF='v1.4.0'
49
49
  powershell -ExecutionPolicy Bypass -File install.ps1
50
50
  ```
51
51
 
@@ -56,10 +56,10 @@ Then, in the `chrome://extensions` tab the script opened: enable **Developer mod
56
56
  `npm exec` installs and updates the **native host only** — it does not include the Chrome extension. Use it if you prefer a pinned npm package to a source checkout.
57
57
 
58
58
  ```bash
59
- npm exec --yes codex-overleaf-link@1.3.8 -- install-native
59
+ npm exec --yes codex-overleaf-link@1.4.0 -- install-native
60
60
  ```
61
61
 
62
- Then add the extension yourself: download `codex-overleaf-link-extension-v1.3.8.zip` from the [v1.3.8 GitHub Release](https://github.com/Ghqqqq/codex-overleaf-link/releases/tag/v1.3.8), unzip it to a stable folder, and in `chrome://extensions` enable **Developer mode**, click **Load unpacked**, and select that folder.
62
+ Then add the extension yourself: download `codex-overleaf-link-extension-v1.4.0.zip` from the [v1.4.0 GitHub Release](https://github.com/Ghqqqq/codex-overleaf-link/releases/tag/v1.4.0), unzip it to a stable folder, and in `chrome://extensions` enable **Developer mode**, click **Load unpacked**, and select that folder.
63
63
 
64
64
  ### Open Overleaf
65
65
 
@@ -86,9 +86,9 @@ npm installs, updates, uninstalls, and diagnoses the native host only. npm does
86
86
 
87
87
  | Action | Command |
88
88
  |--------|---------|
89
- | Install / update | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native` |
90
- | Diagnose | `npm exec --yes codex-overleaf-link@1.3.8 -- doctor` |
91
- | Uninstall | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native` |
89
+ | Install / update | `npm exec --yes codex-overleaf-link@1.4.0 -- install-native` |
90
+ | Diagnose | `npm exec --yes codex-overleaf-link@1.4.0 -- doctor` |
91
+ | Uninstall | `npm exec --yes codex-overleaf-link@1.4.0 -- uninstall-native` |
92
92
 
93
93
  Use `--extension-id <chrome-extension-id>` only for a custom/dev unpacked extension id that differs from the official bundled id.
94
94
 
@@ -98,13 +98,13 @@ To update, re-run any of the [native host installers](#install) — they install
98
98
 
99
99
  ## GitHub Release Artifacts
100
100
 
101
- The v1.3.8 GitHub Release contains:
101
+ The v1.4.0 GitHub Release contains:
102
102
 
103
- - `codex-overleaf-link-extension-v1.3.8.zip`: loadable Chrome extension package for manual unpacked installation.
104
- - `codex-overleaf-native-host-v1.3.8.tar.gz`: native host runtime files used by the installer and release verification.
105
- - `codex-overleaf-link-1.3.8.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
106
- - `install.sh`: release-pinned macOS / Linux installer that defaults to `v1.3.8` when run directly from the release artifact.
107
- - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.3.8` when run directly from the release artifact.
103
+ - `codex-overleaf-link-extension-v1.4.0.zip`: loadable Chrome extension package for manual unpacked installation.
104
+ - `codex-overleaf-native-host-v1.4.0.tar.gz`: native host runtime files used by the installer and release verification.
105
+ - `codex-overleaf-link-1.4.0.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
106
+ - `install.sh`: release-pinned macOS / Linux installer that defaults to `v1.4.0` when run directly from the release artifact.
107
+ - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.4.0` when run directly from the release artifact.
108
108
  - `uninstall-native-host.mjs`: native host uninstaller that removes the Chrome Native Messaging manifest, bridge executable, and runtime copy.
109
109
  - `nativeHostPlatform.js`, `manifest.js`, `runtimeInstaller.js`: helper files required by the loose uninstaller asset.
110
110
  - `SHA256SUMS` and `release-manifest.json`: checksum and artifact metadata for release verification.
@@ -115,7 +115,7 @@ The v1.3.8 GitHub Release contains:
115
115
  Remove the native host (use `--browser chromium` on Linux Chromium):
116
116
 
117
117
  ```bash
118
- npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native
118
+ npm exec --yes codex-overleaf-link@1.4.0 -- uninstall-native
119
119
  ```
120
120
 
121
121
  The same command works on Windows PowerShell. If you installed from a manual checkout or source installer, you can also run `npm run uninstall:native` inside the repo, use `node ~/.codex-overleaf/source/scripts/uninstall-native-host.mjs` on macOS / Linux, or use `node $env:LOCALAPPDATA\CodexOverleaf\source\scripts\uninstall-native-host.mjs` on Windows PowerShell.
@@ -150,13 +150,13 @@ Then remove the extension from `chrome://extensions`. To delete local data: on m
150
150
  Linux Chromium install or update:
151
151
 
152
152
  ```bash
153
- CODEX_OVERLEAF_REF=v1.3.8 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.3.8/install.sh)" -- --browser chromium
153
+ CODEX_OVERLEAF_REF=v1.4.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.4.0/install.sh)" -- --browser chromium
154
154
  ```
155
155
 
156
156
  Linux Chromium uninstall:
157
157
 
158
158
  ```bash
159
- npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native --browser chromium
159
+ npm exec --yes codex-overleaf-link@1.4.0 -- uninstall-native --browser chromium
160
160
  ```
161
161
 
162
162
  ## Features
@@ -289,7 +289,7 @@ Composer attachments are turn-scoped Codex context. Limits are 8 attachments per
289
289
  Re-run any [native host installer](#install), reload the extension in `chrome://extensions`, then refresh the Overleaf tab. This also fixes extension/native version mismatch and native protocol mismatch.
290
290
 
291
291
  ```bash
292
- npm exec --yes codex-overleaf-link@1.3.8 -- install-native
292
+ npm exec --yes codex-overleaf-link@1.4.0 -- install-native
293
293
  ```
294
294
 
295
295
  **The Windows popup or panel shows a Bash recovery command**
@@ -338,8 +338,8 @@ Use this matrix for release-candidate signoff and compatibility reports. Record
338
338
  | Browser/channel/version | Google Chrome channel and version. | Google Chrome channel and version. | Google Chrome channel and version. | Chromium channel/package and version. |
339
339
  | Install mode | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout; native host installed with `--browser chromium`. |
340
340
  | Extension id | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. |
341
- | Installer/update command | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- install-native --browser chromium` |
342
- | Uninstall command | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.3.8 -- uninstall-native --browser chromium` |
341
+ | Installer/update command | `npm exec --yes codex-overleaf-link@1.4.0 -- install-native` | `npm exec --yes codex-overleaf-link@1.4.0 -- install-native` | `npm exec --yes codex-overleaf-link@1.4.0 -- install-native` | `npm exec --yes codex-overleaf-link@1.4.0 -- install-native --browser chromium` |
342
+ | Uninstall command | `npm exec --yes codex-overleaf-link@1.4.0 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.4.0 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.4.0 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.4.0 -- uninstall-native --browser chromium` |
343
343
  | Manifest/registry path | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.codex.overleaf.json` | `HKCU\Software\Google\Chrome\NativeMessagingHosts\com.codex.overleaf` -> `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime\com.codex.overleaf.json` | `~/.config/google-chrome/NativeMessagingHosts/com.codex.overleaf.json` | `~/.config/chromium/NativeMessagingHosts/com.codex.overleaf.json` |
344
344
  | Bridge/runtime/source path | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `%LOCALAPPDATA%\CodexOverleaf\codex-overleaf-bridge.cmd`; runtime `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime`; source `%LOCALAPPDATA%\CodexOverleaf\source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. |
345
345
  | Node/Git/Codex/TeX | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. |
@@ -502,40 +502,53 @@
502
502
  function translateRawError(message, context = {}) {
503
503
  const locale = normalizeLocale(context);
504
504
  const text = String(message || '');
505
+ // Each branch returns the user-visible {conclusion, nextStep} pair and,
506
+ // when the error maps to a structured FailureReason code in the v1.3.8
507
+ // catalog, the `failureCode`. Callers that emit a content-side failure
508
+ // event can attach the catalog code so the run record carries structured
509
+ // failure data alongside the human-readable text. The string text is kept
510
+ // verbatim because it has mode-aware nuance (e.g. "No files were written"
511
+ // vs "This run did not start") the catalog's fallback strings don't capture.
505
512
  if (/Mode must be "confirm" or "auto"/i.test(text)) {
506
513
  return {
507
514
  conclusion: textFor(locale, '这轮没有写入:当前是“只问不改”,但这个任务需要写入权限。', 'No files were written: this task needs write access, but the current mode is Ask.'),
508
- nextStep: textFor(locale, '请切换到“建议修改”或“自动写入”后重新运行。', 'Switch to Suggest or Auto and run the task again.')
515
+ nextStep: textFor(locale, '请切换到“建议修改”或“自动写入”后重新运行。', 'Switch to Suggest or Auto and run the task again.'),
516
+ failureCode: null // no direct catalog match — preflight / governance
509
517
  };
510
518
  }
511
519
  if (/Agent returned invalid JSON/i.test(text)) {
512
520
  return {
513
521
  conclusion: textFor(locale, '这轮没有写入:Codex 已结束,但本地桥接器没有读到可用结果。', 'No files were written: Codex finished, but the local bridge did not receive a usable result.'),
514
- nextStep: textFor(locale, '请重新运行一次;如果再次失败,请打开技术详情查看本地 Codex 输出。', 'Run it again. If it fails again, open Technical Details to inspect local Codex output.')
522
+ nextStep: textFor(locale, '请重新运行一次;如果再次失败,请打开技术详情查看本地 Codex 输出。', 'Run it again. If it fails again, open Technical Details to inspect local Codex output.'),
523
+ failureCode: 'codex_result_parse_failed'
515
524
  };
516
525
  }
517
526
  if (/Could not parse Codex output/i.test(text)) {
518
527
  return {
519
528
  conclusion: textFor(locale, '这轮没有写入:Codex 返回的结果格式不完整。', 'No files were written: Codex returned an incomplete result format.'),
520
- nextStep: textFor(locale, '请重新运行一次;如果再次失败,请打开技术详情查看原始输出。', 'Run it again. If it fails again, open Technical Details to inspect the raw output.')
529
+ nextStep: textFor(locale, '请重新运行一次;如果再次失败,请打开技术详情查看原始输出。', 'Run it again. If it fails again, open Technical Details to inspect the raw output.'),
530
+ failureCode: 'codex_result_parse_failed'
521
531
  };
522
532
  }
523
533
  if (/timed out/i.test(text)) {
524
534
  return {
525
535
  conclusion: textFor(locale, '这轮没有写入:本地 Codex 长时间没有完成。', 'No files were written: local Codex took too long to finish.'),
526
- nextStep: textFor(locale, '请检查本机 Codex 是否仍在运行;如果没有进展,可以中断后缩小 @context 再重试。', 'Check whether local Codex is still running. If there is no progress, cancel and retry with smaller @context.')
536
+ nextStep: textFor(locale, '请检查本机 Codex 是否仍在运行;如果没有进展,可以中断后缩小 @context 再重试。', 'Check whether local Codex is still running. If there is no progress, cancel and retry with smaller @context.'),
537
+ failureCode: 'codex_timeout'
527
538
  };
528
539
  }
529
540
  if (/output limit exceeded/i.test(text)) {
530
541
  return {
531
542
  conclusion: textFor(locale, '这轮没有写入:本地 Codex 输出过长,桥接器停止读取。', 'No files were written: local Codex output was too large, so the bridge stopped reading.'),
532
- nextStep: textFor(locale, '请缩小 @context 后重试,或在技术详情中查看输出限制。', 'Retry with smaller @context, or open Technical Details to inspect the output limit.')
543
+ nextStep: textFor(locale, '请缩小 @context 后重试,或在技术详情中查看输出限制。', 'Retry with smaller @context, or open Technical Details to inspect the output limit.'),
544
+ failureCode: 'codex_output_limit'
533
545
  };
534
546
  }
535
547
  if (/codex_not_found|Codex CLI was not found|ENOENT/i.test(text)) {
536
548
  return {
537
549
  conclusion: textFor(locale, '这轮没有启动:本机没有找到 Codex CLI。', 'This run did not start: Codex CLI was not found locally.'),
538
- nextStep: textFor(locale, '请确认终端里可以运行 `codex`,然后重新安装 native host 或刷新扩展后重试。', 'Confirm `codex` works in Terminal, then reinstall the native host or reload the extension.')
550
+ nextStep: textFor(locale, '请确认终端里可以运行 `codex`,然后重新安装 native host 或刷新扩展后重试。', 'Confirm `codex` works in Terminal, then reinstall the native host or reload the extension.'),
551
+ failureCode: 'codex_not_found'
539
552
  };
540
553
  }
541
554
  if (/project_locked|currently in use by codex\.run/i.test(text)) {
@@ -545,31 +558,36 @@
545
558
  'This run did not start: another Codex task is already running for this Overleaf project.'),
546
559
  nextStep: textFor(locale,
547
560
  '请等待当前任务完成,或先取消当前任务后再重试。',
548
- 'Wait for the current task to finish, or cancel it before retrying.')
561
+ 'Wait for the current task to finish, or cancel it before retrying.'),
562
+ failureCode: 'codex_project_locked'
549
563
  };
550
564
  }
551
565
  if (/unsupported[_ ]parameter/i.test(text) && /reasoning\.summary|summary/i.test(text)) {
552
566
  return {
553
567
  conclusion: textFor(locale, '这轮没有继续:当前 Codex 模型不支持插件请求的推理摘要参数。', 'This run did not continue: the selected Codex model does not support the requested reasoning summary parameter.'),
554
- nextStep: textFor(locale, '请刷新扩展并重新运行;插件会按模型能力自动去掉不兼容参数。', 'Reload the extension and run again; the plugin will omit incompatible parameters based on model capability.')
568
+ nextStep: textFor(locale, '请刷新扩展并重新运行;插件会按模型能力自动去掉不兼容参数。', 'Reload the extension and run again; the plugin will omit incompatible parameters based on model capability.'),
569
+ failureCode: 'native_protocol_incompatible'
555
570
  };
556
571
  }
557
572
  if (/quota|kQuotaBytes|QUOTA_BYTES/i.test(text)) {
558
573
  return {
559
574
  conclusion: textFor(locale, 'Codex 结果已经生成,但本地会话记录超出 Chrome 存储配额。', 'Codex produced a result, but local session history exceeded Chrome storage quota.'),
560
- nextStep: textFor(locale, '请删除一些旧 session,或刷新扩展后重试;这不是论文分析本身失败。', 'Delete older sessions or reload the extension and retry. The paper analysis itself did not fail.')
575
+ nextStep: textFor(locale, '请删除一些旧 session,或刷新扩展后重试;这不是论文分析本身失败。', 'Delete older sessions or reload the extension and retry. The paper analysis itself did not fail.'),
576
+ failureCode: 'storage_quota_exceeded'
561
577
  };
562
578
  }
563
579
  if (/checkpoint/i.test(text)) {
564
580
  return {
565
581
  conclusion: textFor(locale, '这轮没有自动写入:Codex 没有拿到可恢复版本。', 'This run did not auto-write: Codex did not get a recoverable version.'),
566
- nextStep: textFor(locale, '请切换到“建议修改”,或确认 Overleaf Reviewing 已开启后再用“自动写入”。', 'Switch to Suggest, or confirm Overleaf Reviewing is enabled before using Auto.')
582
+ nextStep: textFor(locale, '请切换到“建议修改”,或确认 Overleaf Reviewing 已开启后再用“自动写入”。', 'Switch to Suggest, or confirm Overleaf Reviewing is enabled before using Auto.'),
583
+ failureCode: null // no current catalog code; preflight reviewing issue
567
584
  };
568
585
  }
569
586
  if (/changed while Codex was working|任务执行期间被你或协作者改过/i.test(text)) {
570
587
  return {
571
588
  conclusion: textFor(locale, '这轮没有覆盖文件:任务执行期间文件被你或协作者改过。', 'No file was overwritten: a file changed while Codex was working.'),
572
- nextStep: textFor(locale, '请先确认 Overleaf 当前内容,再重新运行任务。', 'Review the current Overleaf content, then run the task again.')
589
+ nextStep: textFor(locale, '请先确认 Overleaf 当前内容,再重新运行任务。', 'Review the current Overleaf content, then run the task again.'),
590
+ failureCode: 'stale_source_changed'
573
591
  };
574
592
  }
575
593
 
@@ -585,14 +603,16 @@
585
603
  'Codex returned a result, but local post-processing of this run failed.'),
586
604
  nextStep: textFor(locale,
587
605
  '请打开技术详情查看错误。Codex 的回答仍保留在会话中。',
588
- 'Open Technical Details to inspect the error. Codex\'s answer is preserved in the conversation.')
606
+ 'Open Technical Details to inspect the error. Codex\'s answer is preserved in the conversation.'),
607
+ failureCode: null // post-processing path; the real error already attached its own structured failure
589
608
  };
590
609
  }
591
610
  return {
592
611
  conclusion: context.mode === 'ask'
593
612
  ? textFor(locale, '这轮只问不改没有完成:本地 Codex 没有正常完成,因此没有生成最终说明。', 'This Ask run did not complete: local Codex did not finish normally, so no final answer was generated.')
594
613
  : textFor(locale, '这轮任务失败:本地 Codex 没有返回可用结果,未确认任何写入。', 'This task failed: local Codex returned no usable result, so no writes were confirmed.'),
595
- nextStep: textFor(locale, '请查看技术详情,处理本地 Codex 错误后重试。', 'Open Technical Details, resolve the local Codex error, and retry.')
614
+ nextStep: textFor(locale, '请查看技术详情,处理本地 Codex 错误后重试。', 'Open Technical Details, resolve the local Codex error, and retry.'),
615
+ failureCode: 'codex_no_usable_result'
596
616
  };
597
617
  }
598
618
 
@@ -12,7 +12,7 @@
12
12
  const MIN_NATIVE_VERSION = '1.0.0';
13
13
  const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
14
14
  const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
15
- const BUILD_TARGET_VERSION = '1.3.8';
15
+ const BUILD_TARGET_VERSION = '1.4.0';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -164,7 +164,7 @@
164
164
  fallbackNextAction: 'Review written files; use Undo written parts if needed.'
165
165
  },
166
166
 
167
- // 9.0+ — write-safety primitive (welcome-panel + writeback project-ID guard v1.3.8 add-on)
167
+ // 9.0+ — write-safety primitive (welcome-panel + writeback project-ID guard)
168
168
  aborted_project_changed: {
169
169
  stage: 'write', severity: 'blocked', defaultRetryable: true,
170
170
  fallbackUserMessage: 'Codex stopped a write because Overleaf switched to a different project mid-run.',
@@ -303,6 +303,21 @@
303
303
  fallbackUserMessage: 'The local run was cancelled.',
304
304
  fallbackNextAction: 'Start a new run.'
305
305
  },
306
+ codex_timeout: {
307
+ stage: 'codex', severity: 'error', defaultRetryable: true,
308
+ fallbackUserMessage: 'Local Codex took too long to finish.',
309
+ fallbackNextAction: 'Check whether local Codex is still running; cancel and retry with smaller @context.'
310
+ },
311
+ codex_output_limit: {
312
+ stage: 'codex', severity: 'error', defaultRetryable: true,
313
+ fallbackUserMessage: 'Local Codex output was too large; the bridge stopped reading.',
314
+ fallbackNextAction: 'Retry with smaller @context, or open Technical Details to inspect the output limit.'
315
+ },
316
+ codex_not_found: {
317
+ stage: 'codex', severity: 'blocked', defaultRetryable: true,
318
+ fallbackUserMessage: 'Codex CLI was not found on this machine.',
319
+ fallbackNextAction: 'Confirm `codex` works in Terminal, then reinstall the native host or reload the extension.'
320
+ },
306
321
 
307
322
  // 9.8 Storage
308
323
  storage_quota_exceeded: {
@@ -188,9 +188,13 @@
188
188
  settingsScopeGlobalSubtitle: 'Skill loading settings that apply across all Overleaf projects.',
189
189
  governanceRulesTitle: 'Governance Rules',
190
190
  governanceReadonlyPatterns: 'Read-only patterns',
191
+ governanceReadonlyHelp: 'Files Codex must never modify — one glob per line.',
191
192
  governanceWritablePatterns: 'Writable patterns',
193
+ governanceWritableHelp: 'Files Codex may edit — one glob per line.',
192
194
  sensitiveCheckEnabled: 'Check for sensitive content before Codex runs',
195
+ sensitiveCheckHelp: 'Scan project files for secrets and personal data before each run.',
193
196
  sensitiveConfirmAllowed: 'Allow explicit confirmation when sensitive findings exist',
197
+ sensitiveConfirmHelp: 'Lets you review and proceed when the scan flags something.',
194
198
  sensitiveConfirmTitle: 'Sensitive Content Found',
195
199
  sensitiveConfirmMessage: 'Codex found possible sensitive content. Raw detected secrets are not shown here. Continue only if this project context may be sent to Codex.',
196
200
  sensitiveConfirmRun: 'Run anyway',
@@ -198,6 +202,7 @@
198
202
  codexOverleafSkillsEmpty: 'No Codex Overleaf skills installed.',
199
203
  codexOverleafSkillsDisabled: 'Codex Overleaf skills are disabled for runs.',
200
204
  loadCodexLocalSkills: 'Load local Codex skills',
205
+ loadCodexLocalSkillsHelp: 'Pull skills from your local Codex installation.',
201
206
  loadCodexOverleafSkills: 'Load Codex Overleaf skills',
202
207
  localSkillRemove: 'Remove',
203
208
  localSkillRemoveConfirm: 'Confirm',
@@ -221,6 +226,7 @@
221
226
  binaryAssetConfirm: 'Write assets',
222
227
  binaryAssetCancel: 'Skip assets',
223
228
  personalizationConfig: 'Personalization',
229
+ personalizationHelp: 'Style, terminology, and LaTeX conventions Codex should follow in this project.',
224
230
  settingsSaved: 'Saved',
225
231
  settingsSaving: 'Saving…',
226
232
  technicalDetails: 'Technical Details',
@@ -344,13 +350,19 @@
344
350
  failureReason_codex_no_usable_result_next: 'Open Technical Details and resolve the local Codex error.',
345
351
  failureReason_codex_project_locked_user: 'Another Codex task is already running for this Overleaf project.',
346
352
  failureReason_codex_project_locked_next: 'Wait for the active task to finish, or cancel it before retrying.',
353
+ failureReason_codex_timeout_user: 'Local Codex took too long to finish.',
354
+ failureReason_codex_timeout_next: 'Check whether local Codex is still running. If there is no progress, cancel and retry with smaller @context.',
355
+ failureReason_codex_output_limit_user: 'Local Codex output was too large, so the bridge stopped reading.',
356
+ failureReason_codex_output_limit_next: 'Retry with smaller @context, or open Technical Details to inspect the output limit.',
357
+ failureReason_codex_not_found_user: 'Codex CLI was not found on this machine.',
358
+ failureReason_codex_not_found_next: 'Confirm `codex` works in Terminal, then reinstall the native host or reload the extension.',
347
359
  failureReason_storage_quota_exceeded_user: 'Browser storage quota was exceeded.',
348
360
  failureReason_storage_quota_exceeded_next: 'Clear old run history or reduce attachments.',
349
361
  failureReason_aborted_project_changed_user: 'Codex stopped a write because Overleaf switched to a different project mid-run.',
350
362
  failureReason_aborted_project_changed_next: 'Reopen the original project and rerun the task if you still want this change.',
351
363
  failureReason_editor_project_id_unavailable_user: 'Codex could not confirm which Overleaf project the editor is showing, so it did not write.',
352
364
  failureReason_editor_project_id_unavailable_next: 'Refresh the Overleaf tab and retry; if it persists, reload the extension.',
353
- // Welcome-panel + write-guard v1.3.8 add-on (Task 5): Recent-projects
365
+ // Welcome-panel + write-guard: Recent-projects
354
366
  // variant copy. Spec §5.3–§5.5, §5.8, §5.10 are the source of truth.
355
367
  recentProjects_welcome: 'Codex Overleaf Link',
356
368
  recentProjects_welcome_subtitle: 'Open a project from the Overleaf list on the left to start.',
@@ -546,9 +558,13 @@
546
558
  settingsScopeGlobalSubtitle: '适用于所有 Overleaf 项目的技能加载设置。',
547
559
  governanceRulesTitle: '治理规则',
548
560
  governanceReadonlyPatterns: '只读路径规则',
561
+ governanceReadonlyHelp: 'Codex 绝不可修改的文件,每行一个 glob 规则。',
549
562
  governanceWritablePatterns: '可写路径规则',
563
+ governanceWritableHelp: 'Codex 允许编辑的文件,每行一个 glob 规则。',
550
564
  sensitiveCheckEnabled: 'Codex 运行前检查敏感内容',
565
+ sensitiveCheckHelp: '每次运行前扫描项目文件中的密钥和个人信息。',
551
566
  sensitiveConfirmAllowed: '发现敏感内容时允许显式确认后继续',
567
+ sensitiveConfirmHelp: '当扫描发现可疑内容时,允许你审阅后再继续。',
552
568
  sensitiveConfirmTitle: '发现敏感内容',
553
569
  sensitiveConfirmMessage: 'Codex 发现了可能的敏感内容。这里不会显示原始密钥或秘密值。只有在确认可以把这些项目上下文发送给 Codex 时才继续。',
554
570
  sensitiveConfirmRun: '仍然运行',
@@ -556,6 +572,7 @@
556
572
  codexOverleafSkillsEmpty: '没有安装 Codex Overleaf 专属技能。',
557
573
  codexOverleafSkillsDisabled: '运行时已禁用 Codex Overleaf 专属技能。',
558
574
  loadCodexLocalSkills: '加载 Codex 本地技能',
575
+ loadCodexLocalSkillsHelp: '从本地 Codex 安装中拉取技能。',
559
576
  loadCodexOverleafSkills: '加载 Codex Overleaf 专属技能',
560
577
  localSkillRemove: '删除',
561
578
  localSkillRemoveConfirm: '确认',
@@ -579,6 +596,7 @@
579
596
  binaryAssetConfirm: '写入资源',
580
597
  binaryAssetCancel: '跳过资源',
581
598
  personalizationConfig: '个性化配置',
599
+ personalizationHelp: 'Codex 在本项目中应遵循的风格、术语和 LaTeX 约定。',
582
600
  settingsSaved: '已保存',
583
601
  settingsSaving: '保存中…',
584
602
  technicalDetails: '技术细节',
@@ -701,13 +719,19 @@
701
719
  failureReason_codex_no_usable_result_next: '请打开“技术细节”排查本地 Codex 错误。',
702
720
  failureReason_codex_project_locked_user: '同一个 Overleaf 项目里已经有一轮 Codex 任务正在运行。',
703
721
  failureReason_codex_project_locked_next: '请等待当前任务完成,或先取消当前任务后再重试。',
722
+ failureReason_codex_timeout_user: '本地 Codex 长时间没有完成。',
723
+ failureReason_codex_timeout_next: '请检查本机 Codex 是否仍在运行;如果没有进展,可以中断后缩小 @context 再重试。',
724
+ failureReason_codex_output_limit_user: '本地 Codex 输出过长,桥接器停止读取。',
725
+ failureReason_codex_output_limit_next: '请缩小 @context 后重试,或在技术详情中查看输出限制。',
726
+ failureReason_codex_not_found_user: '本机没有找到 Codex CLI。',
727
+ failureReason_codex_not_found_next: '请确认终端里可以运行 `codex`,然后重新安装 native host 或刷新扩展后重试。',
704
728
  failureReason_storage_quota_exceeded_user: '浏览器本地存储配额已超出。',
705
729
  failureReason_storage_quota_exceeded_next: '请清理旧的运行历史,或减少附件大小。',
706
730
  failureReason_aborted_project_changed_user: 'Codex 已停止一次写入,因为 Overleaf 在写入过程中切换到了另一个项目。',
707
731
  failureReason_aborted_project_changed_next: '如仍需此次改动,请回到原项目后重新运行任务。',
708
732
  failureReason_editor_project_id_unavailable_user: 'Codex 无法确认当前 Overleaf 编辑器对应的项目,因此没有写入。',
709
733
  failureReason_editor_project_id_unavailable_next: '请刷新 Overleaf 页面后重试;如果仍然失败,请重新加载扩展。',
710
- // Welcome-panel + write-guard v1.3.8 add-on (Task 5): Recent-projects
734
+ // Welcome-panel + write-guard: Recent-projects
711
735
  // variant copy. 见 spec §5.3–§5.5、§5.8、§5.10。
712
736
  recentProjects_welcome: 'Codex Overleaf Link',
713
737
  recentProjects_welcome_subtitle: '从左侧 Overleaf 项目列表中打开任意项目即可开始。',
@@ -8,7 +8,7 @@
8
8
  'use strict';
9
9
 
10
10
  // -------------------------------------------------------------------------
11
- // Welcome-panel + write-guard v1.3.8 add-on (Fix C / spec §5.6.2).
11
+ // Welcome-panel + write-guard:
12
12
  //
13
13
  // Canonical local-path sanitizer shared between `computeSafeTaskSummary`
14
14
  // (sessionState) and the storage-side audit redaction helpers
@@ -95,19 +95,6 @@
95
95
  'gi'
96
96
  );
97
97
 
98
- // Quick predicate — used by callers that only want to do the (more
99
- // expensive) substitution pass when there's actually something to strip.
100
- const MIGHT_CONTAIN_LOCAL_PATH_PATTERN = new RegExp(
101
- [
102
- 'file:\\/{2,3}',
103
- '(?:\\\\\\\\|\\/\\/)[A-Za-z0-9._-]+[\\\\\\/]',
104
- '[A-Za-z]:[\\\\\\/]',
105
- '\\/(?:' + UNIX_TOPLEVELS.join('|') + ')\\/',
106
- '\\.codex-overleaf[\\\\\\/]projects[\\\\\\/]'
107
- ].join('|'),
108
- 'i'
109
- );
110
-
111
98
  // Replace every absolute-local-path token in `value` with `placeholder`.
112
99
  // Default placeholder is `<local-path>` to match the prior summary-side
113
100
  // sanitizer. The storage-side audit redactor uses a richer `[local path]`
@@ -124,20 +111,7 @@
124
111
  return value.replace(ABSOLUTE_PATH_PATTERN, token);
125
112
  }
126
113
 
127
- function mightContainLocalPath(value) {
128
- if (typeof value !== 'string' || !value) {
129
- return false;
130
- }
131
- return MIGHT_CONTAIN_LOCAL_PATH_PATTERN.test(value);
132
- }
133
-
134
114
  return {
135
- redactLocalPaths,
136
- mightContainLocalPath,
137
- // Exposed so the storage-side audit redactor (which formats per-token
138
- // placeholders with line-suffix preservation) can iterate the same set
139
- // without duplicating the regex source.
140
- ABSOLUTE_PATH_PATTERN,
141
- UNIX_TOPLEVELS
115
+ redactLocalPaths
142
116
  };
143
117
  });
@@ -12,6 +12,12 @@
12
12
  { id: 'private-key', pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/gi },
13
13
  { id: 'bearer-token', pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/gi },
14
14
  { id: 'api-token', pattern: /\b(?:(?:ghp|github_pat|xox[baprs])_[A-Za-z0-9_=-]{16,}|sk-[A-Za-z0-9_-]{16,})\b/gi },
15
+ { id: 'aws-access-key', pattern: /\bAKIA[0-9A-Z]{16}\b/g },
16
+ { id: 'google-api-key', pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
17
+ { id: 'huggingface-token', pattern: /\bhf_[A-Za-z0-9]{20,}\b/g },
18
+ { id: 'gitlab-token', pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
19
+ { id: 'stripe-live-secret', pattern: /\bsk_live_[A-Za-z0-9]{16,}\b/g },
20
+ { id: 'jwt-token', pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
15
21
  {
16
22
  id: 'secret-assignment',
17
23
  pattern: /\b(?:api[_-]?key|token|secret|password|passwd)\b\s*[:=]\s*["']?[^"'\s,;]{4,}/gi
@@ -106,11 +112,8 @@
106
112
  }
107
113
 
108
114
  return {
109
- DETECTORS,
110
115
  scanSensitiveText,
111
116
  scanSensitiveProjectFiles,
112
- scanSensitiveInputs,
113
- buildRedactedPreview,
114
- redactPreviewLine
117
+ scanSensitiveInputs
115
118
  };
116
119
  });
@@ -602,7 +602,7 @@
602
602
  }
603
603
  }
604
604
 
605
- // Welcome-panel + write-guard v1.3.8 add-on (Task 2): the run lifecycle now
605
+ // Welcome-panel + write-guard: the run lifecycle now
606
606
  // settles on three additional values when the user navigates away mid-run
607
607
  // and the original run completes (or aborts) in the background. They land on
608
608
  // the ORIGINAL project's session record, not the active one. The catalog is
@@ -635,16 +635,29 @@
635
635
  function normalizeRunEvents(events) {
636
636
  return (Array.isArray(events) ? events : [])
637
637
  .filter(event => event && typeof event.title === 'string')
638
- .map(event => ({
639
- title: sanitizeAssistantVisibleText(event.title) || 'Event',
640
- status: typeof event.status === 'string' ? event.status : 'info',
641
- detail: sanitizeAssistantVisibleValue(event.detail),
642
- timestamp: typeof event.timestamp === 'string' ? event.timestamp : '',
643
- kind: typeof event.kind === 'string' ? event.kind : 'activity',
644
- technicalDetail: sanitizeAssistantVisibleValue(event.technicalDetail),
645
- streamKey: sanitizeAssistantVisibleText(event.streamKey),
646
- streamRole: sanitizeAssistantVisibleText(event.streamRole)
647
- }))
638
+ .map(event => {
639
+ const normalized = {
640
+ title: sanitizeAssistantVisibleText(event.title) || 'Event',
641
+ status: typeof event.status === 'string' ? event.status : 'info',
642
+ detail: sanitizeAssistantVisibleValue(event.detail),
643
+ timestamp: typeof event.timestamp === 'string' ? event.timestamp : '',
644
+ kind: typeof event.kind === 'string' ? event.kind : 'activity',
645
+ technicalDetail: sanitizeAssistantVisibleValue(event.technicalDetail),
646
+ streamKey: sanitizeAssistantVisibleText(event.streamKey),
647
+ streamRole: sanitizeAssistantVisibleText(event.streamRole)
648
+ };
649
+ // Preserve the structured completion-report payload and the structured
650
+ // failure across reload. Without these the report re-renders via the
651
+ // flat legacy path (Write result / Undo / Next NOT demoted into the
652
+ // muted meta block) and the recovery action button disappears.
653
+ if (event.detailStructured) {
654
+ normalized.detailStructured = sanitizeAssistantVisibleValue(event.detailStructured);
655
+ }
656
+ if (event.failure) {
657
+ normalized.failure = sanitizeAssistantVisibleValue(event.failure);
658
+ }
659
+ return normalized;
660
+ })
648
661
  .slice(-MAX_RUN_EVENTS);
649
662
  }
650
663
 
@@ -906,7 +919,7 @@
906
919
  requireReviewing: session.requireReviewing !== false,
907
920
  focusFiles: normalizeFocusFiles(session.focusFiles)
908
921
  };
909
- // Welcome-panel + write-guard v1.3.8 add-on (Task 3): preserve the four
922
+ // Welcome-panel + write-guard: preserve the four
910
923
  // Recent-projects fields through compaction so they round-trip when state
911
924
  // is reloaded from chrome.storage.local. The active record builder in
912
925
  // `buildSessionRecord` is the canonical writer; this branch preserves an
@@ -1010,6 +1023,14 @@
1010
1023
  if (detail !== undefined) {
1011
1024
  compact.detail = detail;
1012
1025
  }
1026
+ // Keep the structured report payload + structured failure so the
1027
+ // demoted meta block and the recovery action survive a reload.
1028
+ if (event.detailStructured) {
1029
+ compact.detailStructured = sanitizeAssistantVisibleValue(event.detailStructured);
1030
+ }
1031
+ if (event.failure) {
1032
+ compact.failure = sanitizeAssistantVisibleValue(event.failure);
1033
+ }
1013
1034
  return compact;
1014
1035
  });
1015
1036
  }
@@ -1129,7 +1150,7 @@
1129
1150
  return Math.round(Math.min(760, Math.max(340, width)));
1130
1151
  }
1131
1152
 
1132
- // Welcome-panel + write-guard v1.3.8 add-on (Task 3): the Recent-projects
1153
+ // Welcome-panel + write-guard: the Recent-projects
1133
1154
  // dashboard variant renders one sanitized line per project. `computeSafeTaskSummary`
1134
1155
  // is the privacy floor for that line. It is written on every `saveState` and
1135
1156
  // stored on the session record (`session.safeTaskSummary`), so the dashboard
@@ -1148,7 +1169,7 @@
1148
1169
  if (typeof task !== 'string' || !task) return '';
1149
1170
  let s = task;
1150
1171
  // Strip absolute local paths via the canonical shared helper
1151
- // (spec §5.6.2 / Fix C). The helper covers Unix (/Users, /home,
1172
+ // (spec §5.6.2). The helper covers Unix (/Users, /home,
1152
1173
  // /private/var, /tmp, /var/folders, /Volumes, /etc, /opt, /usr, ...),
1153
1174
  // Windows drive letters with both `\\` and `/`, UNC `\\server\share`,
1154
1175
  // and `file:///` URLs. Adding a new path shape is a one-line change
@@ -155,8 +155,6 @@
155
155
  updateExpectedFileContent,
156
156
  removeExpectedFile,
157
157
  moveExpectedFile,
158
- checkPatchRangeFreshness,
159
- normalizeText,
160
158
  normalizePath
161
159
  };
162
160
  });
@@ -239,7 +239,7 @@
239
239
  var now = new Date().toISOString();
240
240
  var titleSource = input.titleSource === 'manual' ? 'manual' : 'auto';
241
241
  var updatedAt = typeof input.updatedAt === 'string' ? input.updatedAt : now;
242
- // Welcome-panel + write-guard v1.3.8 add-on (Task 3): persist the four
242
+ // Welcome-panel + write-guard: persist the four
243
243
  // Recent-projects fields on every session record so the cross-project
244
244
  // query (`listRecentProjectsAcrossAccount`) can filter / sort / render
245
245
  // without touching the raw `task` text.
@@ -680,10 +680,50 @@
680
680
  if (detail !== undefined) {
681
681
  compact.detail = detail;
682
682
  }
683
+ // Preserve the structured completion-report payload + structured
684
+ // failure with their object shape intact (the generic detail
685
+ // compactor would redact unknown objects to a {redacted,hash}
686
+ // summary). Without this the report re-renders flat on reload —
687
+ // Write result / Undo / Next no longer demote into the muted meta
688
+ // block — and the recovery action button disappears.
689
+ if (event.detailStructured) {
690
+ compact.detailStructured = compactStructuredEventValueForStorage(event.detailStructured, 0);
691
+ }
692
+ if (event.failure) {
693
+ compact.failure = compactStructuredEventValueForStorage(event.failure, 0);
694
+ }
683
695
  return compact;
684
696
  });
685
697
  }
686
698
 
699
+ // Deep-copies a known-safe structured event value (the completion-report
700
+ // {conclusion, body, meta[]} payload, or a FailureReason object) preserving
701
+ // its shape while redacting + truncating every string field. Bounded depth +
702
+ // breadth so a pathological value can't blow up the stored record.
703
+ function compactStructuredEventValueForStorage(value, depth) {
704
+ var d = depth || 0;
705
+ if (typeof value === 'string') {
706
+ return normalizeDisplayTextForStorage(value, SESSION_STORAGE_LIMITS.reportDetailChars);
707
+ }
708
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') {
709
+ return value;
710
+ }
711
+ if (d > 6 || typeof value !== 'object') {
712
+ return null;
713
+ }
714
+ if (Array.isArray(value)) {
715
+ return value.slice(0, 32).map(function (item) {
716
+ return compactStructuredEventValueForStorage(item, d + 1);
717
+ });
718
+ }
719
+ var out = {};
720
+ var keys = Object.keys(value).slice(0, 32);
721
+ for (var i = 0; i < keys.length; i++) {
722
+ out[keys[i]] = compactStructuredEventValueForStorage(value[keys[i]], d + 1);
723
+ }
724
+ return out;
725
+ }
726
+
687
727
  function getEventDetailLimit(event) {
688
728
  return event && event.kind === 'report'
689
729
  ? SESSION_STORAGE_LIMITS.reportDetailChars
@@ -845,7 +885,7 @@
845
885
  }
846
886
 
847
887
  function normalizeRunStatus(status) {
848
- // Welcome-panel + write-guard v1.3.8 add-on (Task 2/3): the run-status
888
+ // Welcome-panel + write-guard: the run-status
849
889
  // enum gained three post-navigation values. The storage normalizer must
850
890
  // accept them so a settled run round-trips intact through `buildSessionRecord`.
851
891
  // Unknown legacy values fall through to `completed` (the historical default).
@@ -1065,7 +1105,7 @@
1065
1105
  return result;
1066
1106
  }
1067
1107
 
1068
- // Welcome-panel + write-guard v1.3.8 add-on (Task 3): the Recent-projects
1108
+ // Welcome-panel + write-guard: the Recent-projects
1069
1109
  // dashboard variant calls `listRecentProjectsAcrossAccount` to get the
1070
1110
  // sorted, deduped, capped list of projects in the current account scope.
1071
1111
  //
@@ -279,13 +279,13 @@ function formatFocusFiles(files) {
279
279
  }
280
280
 
281
281
  function formatCompileLogContext(context = {}) {
282
- const log = String(context.compileLog || '').trim();
282
+ const log = redactCompileLogText(String(context.compileLog || '').trim());
283
283
  if (!log) {
284
284
  return '- none provided.';
285
285
  }
286
286
 
287
- const errors = normalizeCompileMessages(context.compileErrors);
288
- const warnings = normalizeCompileMessages(context.compileWarnings);
287
+ const errors = normalizeCompileMessages(context.compileErrors).map(redactCompileLogText);
288
+ const warnings = normalizeCompileMessages(context.compileWarnings).map(redactCompileLogText);
289
289
  const fresh = context.compileLogFresh === false
290
290
  ? 'possibly stale'
291
291
  : 'fresh';
@@ -305,6 +305,33 @@ function formatCompileLogContext(context = {}) {
305
305
  ].filter(Boolean).join('\n');
306
306
  }
307
307
 
308
+ function redactCompileLogText(value) {
309
+ let text = String(value || '');
310
+ const replacements = [
311
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/g, 'Bearer [REDACTED]'],
312
+ [/\b(?:ghp|github_pat|xox[baprs]|glpat)-?[_A-Za-z0-9=-]{16,}\b/g, '[REDACTED_TOKEN]'],
313
+ [/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g, '[REDACTED_TOKEN]'],
314
+ [/\bsk-[A-Za-z0-9_-]{16,}\b/g, '[REDACTED_TOKEN]'],
315
+ [/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED_AWS_KEY]'],
316
+ [/\bAIza[0-9A-Za-z_-]{35}\b/g, '[REDACTED_GOOGLE_KEY]'],
317
+ [/\bhf_[A-Za-z0-9]{20,}\b/g, '[REDACTED_HF_TOKEN]'],
318
+ [/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, '[REDACTED_JWT]'],
319
+ [/file:\/\/\/[^\s'"<>]+/g, '<local-path>'],
320
+ [/(^|[\s({["'=])\/Users\/[^:\s'"<>)]*/g, '$1<local-path>'],
321
+ [/(^|[\s({["'=])\/private\/var\/[^:\s'"<>)]*/g, '$1<local-path>'],
322
+ [/(^|[\s({["'=])\/var\/folders\/[^:\s'"<>)]*/g, '$1<local-path>'],
323
+ [/(^|[\s({["'=])\/tmp\/[^:\s'"<>)]*/g, '$1<local-path>'],
324
+ [/(^|[\s({["'=])\/Library\/TeX\/[^:\s'"<>)]*/g, '$1<TEXLIVE_PATH>'],
325
+ [/(^|[\s({["'=])\/usr\/local\/texlive\/[^:\s'"<>)]*/g, '$1<TEXLIVE_PATH>'],
326
+ [/[A-Za-z]:\\Users\\[^:\s'"<>)]*/g, '<local-path>'],
327
+ [/[A-Za-z]:\\(?:Program Files|texlive)\\[^:\s'"<>)]*/gi, '<local-path>']
328
+ ];
329
+ for (const [pattern, replacement] of replacements) {
330
+ text = text.replace(pattern, replacement);
331
+ }
332
+ return text;
333
+ }
334
+
308
335
  function formatCustomInstructionsContext(customInstructions = '') {
309
336
  const instructions = truncateText(
310
337
  String(customInstructions || '').trim(),
@@ -677,9 +677,23 @@ function runCodexAppServerSession(input) {
677
677
  const assistantMessages = new Map();
678
678
  const assistantMessageOrder = [];
679
679
  let settled = false;
680
+ // Two-layer timeout strategy:
681
+ // 1. Optional absolute deadline (CODEX_OVERLEAF_CODEX_TIMEOUT_MS) —
682
+ // legacy override; off by default. When set, the whole run must
683
+ // finish within that envelope.
684
+ // 2. Idle watchdog — fires after a stretch of silence from the
685
+ // app-server (no stdout / no messages). Default 10 minutes; the
686
+ // runtime resets it on every incoming line and on every outgoing
687
+ // request. This catches the failure mode where Codex sends
688
+ // turn/started, then hangs without ever emitting completed/error
689
+ // and the project lock would otherwise be held forever.
680
690
  const timeout = createOptionalTimeout(childEnv.CODEX_OVERLEAF_CODEX_TIMEOUT_MS, timeoutMs => {
681
691
  fail(new Error(`Codex app-server did not complete within configured timeout (${timeoutMs}ms)`));
682
692
  });
693
+ const idleTimeoutMs = parseOptionalPositiveInteger(childEnv.CODEX_OVERLEAF_CODEX_IDLE_TIMEOUT_MS) || 600000;
694
+ const idleWatchdog = createCodexIdleWatchdog(idleTimeoutMs, ms => {
695
+ fail(new Error(`Codex app-server produced no events for ${ms}ms (idle watchdog); the run was aborted to release the project lock.`));
696
+ });
683
697
  const onAbort = () => {
684
698
  fail(getAbortReason(input.signal));
685
699
  };
@@ -688,6 +702,7 @@ function runCodexAppServerSession(input) {
688
702
  child.stdout.setEncoding('utf8');
689
703
  child.stderr.setEncoding('utf8');
690
704
  child.stdout.on('data', chunk => {
705
+ idleWatchdog.reset();
691
706
  stdoutBuffer += chunk;
692
707
  const lines = stdoutBuffer.split(/\r?\n/);
693
708
  stdoutBuffer = lines.pop() || '';
@@ -924,6 +939,7 @@ function runCodexAppServerSession(input) {
924
939
 
925
940
  function cleanup() {
926
941
  timeout.cancel();
942
+ idleWatchdog.cancel();
927
943
  input.signal?.removeEventListener('abort', onAbort);
928
944
  }
929
945
  });
@@ -1141,6 +1157,30 @@ function createOptionalTimeout(value, onTimeout) {
1141
1157
  };
1142
1158
  }
1143
1159
 
1160
+ // Idle-style watchdog: fires only after the app-server has been silent for
1161
+ // `idleMs`. Callers must invoke .reset() on every signal of liveness
1162
+ // (incoming line, outgoing request). cancel() stops the timer on settle.
1163
+ // Returns no-op when idleMs is non-positive (defensive guard against bad env
1164
+ // var values).
1165
+ function createCodexIdleWatchdog(idleMs, onIdle) {
1166
+ if (!(idleMs > 0)) {
1167
+ return {
1168
+ reset() {},
1169
+ cancel() {}
1170
+ };
1171
+ }
1172
+ let timer = setTimeout(() => onIdle(idleMs), idleMs);
1173
+ return {
1174
+ reset() {
1175
+ clearTimeout(timer);
1176
+ timer = setTimeout(() => onIdle(idleMs), idleMs);
1177
+ },
1178
+ cancel() {
1179
+ clearTimeout(timer);
1180
+ }
1181
+ };
1182
+ }
1183
+
1144
1184
  function parseOptionalPositiveInteger(value) {
1145
1185
  if (value === undefined || value === null || value === '') {
1146
1186
  return 0;
@@ -84,11 +84,77 @@ process.on('unhandledRejection', reason => {
84
84
  });
85
85
 
86
86
  function writeResponse(response) {
87
- const frame = encodeMessage(response);
87
+ let frame;
88
+ try {
89
+ frame = encodeMessage(response);
90
+ } catch (error) {
91
+ const fallback = buildOversizeResponseFallback(response, error);
92
+ try {
93
+ frame = encodeMessage(fallback);
94
+ } catch (fallbackError) {
95
+ frame = encodeMessage({
96
+ id: response?.id,
97
+ ok: false,
98
+ error: {
99
+ code: 'native_response_too_large',
100
+ message: truncateForNativeFrame(fallbackError.message || error.message || 'Native response exceeded the browser frame limit.', 800)
101
+ }
102
+ });
103
+ }
104
+ }
88
105
  logDebug('stdout.write', { bytes: frame.length, ok: response?.ok, code: response?.error?.code });
89
106
  process.stdout.write(frame);
90
107
  }
91
108
 
109
+ function buildOversizeResponseFallback(response, error) {
110
+ if (response?.event) {
111
+ const event = response.event || {};
112
+ return {
113
+ id: response.id,
114
+ ok: true,
115
+ event: {
116
+ type: event.type || 'native.event.truncated',
117
+ title: truncateForNativeFrame(event.title || 'Native event was truncated', 500),
118
+ status: event.status || 'warning',
119
+ detail: {
120
+ code: 'native_event_truncated',
121
+ reason: truncateForNativeFrame(error?.message || 'Native event exceeded the browser frame limit.', 800),
122
+ originalType: truncateForNativeFrame(event.type || '', 160),
123
+ originalTitle: truncateForNativeFrame(event.title || '', 500),
124
+ originalDetailBytes: stringByteLength(safeJsonStringify(event.detail))
125
+ },
126
+ timestamp: event.timestamp || new Date().toISOString()
127
+ }
128
+ };
129
+ }
130
+
131
+ return {
132
+ id: response?.id,
133
+ ok: false,
134
+ error: {
135
+ code: 'native_response_too_large',
136
+ message: truncateForNativeFrame(error?.message || 'Native response exceeded the browser frame limit.', 800),
137
+ originalOk: response?.ok === true
138
+ }
139
+ };
140
+ }
141
+
142
+ function truncateForNativeFrame(value, limit) {
143
+ const text = String(value || '');
144
+ if (text.length <= limit) {
145
+ return text;
146
+ }
147
+ return `${text.slice(0, Math.max(0, limit - 24))}... [truncated]`;
148
+ }
149
+
150
+ function safeJsonStringify(value) {
151
+ try {
152
+ return JSON.stringify(value);
153
+ } catch (_error) {
154
+ return '';
155
+ }
156
+ }
157
+
92
158
  function summarizeRequest(message) {
93
159
  const params = message?.params || {};
94
160
  return {
@@ -25,6 +25,11 @@ const { version: PACKAGE_VERSION } = require('../../package.json');
25
25
 
26
26
  const activeProjectLocks = new Map();
27
27
  const activeRunControllers = new Map();
28
+ // Parallel index of active runs by projectKey so codex.cancel can find a
29
+ // controller even when the original request id is unknown (e.g. after the
30
+ // Overleaf tab was reloaded — the requestId lived in content-side JS state
31
+ // and is gone, but the native-host-side controller is still running).
32
+ const activeRunByProject = new Map();
28
33
  const pendingPlans = new Map();
29
34
  const PENDING_PLAN_TTL_MS = 30 * 60 * 1000;
30
35
  const CODEX_RUN_PASSTHROUGH_ERROR_CODES = new Set(['thread_resume_failed']);
@@ -138,6 +143,7 @@ async function handleCodexRun(request, env, emit) {
138
143
  const abortController = new AbortController();
139
144
  if (request.id) {
140
145
  activeRunControllers.set(request.id, abortController);
146
+ activeRunByProject.set(projectKey, { id: request.id, controller: abortController });
141
147
  }
142
148
  try {
143
149
  if (params.useExistingMirror) {
@@ -213,24 +219,63 @@ async function handleCodexRun(request, env, emit) {
213
219
  if (request.id && activeRunControllers.get(request.id) === abortController) {
214
220
  activeRunControllers.delete(request.id);
215
221
  }
222
+ if (activeRunByProject.get(projectKey)?.controller === abortController) {
223
+ activeRunByProject.delete(projectKey);
224
+ }
216
225
  releaseProjectLock(projectKey, lockToken);
217
226
  }
218
227
  }
219
228
 
229
+ // Cancel paths, in priority order:
230
+ // 1. By requestId (legacy + primary, when the caller still has it)
231
+ // 2. By projectKey (after page refresh — requestId is lost but projectKey
232
+ // is derivable from the Overleaf URL)
233
+ // 3. Force-release the project lock when no controller is registered for
234
+ // the given projectKey. Covers the zombie-lock case where a previous
235
+ // run leaked the lock (unhandled error path, process bug, etc.) and
236
+ // the user otherwise has no way to recover short of restarting Chrome.
237
+ // Only fires when `force: true` is explicitly set so accidental calls
238
+ // can't punch through a real live run.
220
239
  function handleCodexCancel(request) {
221
- const targetId = request.params?.requestId || request.params?.id;
222
- if (!targetId || !activeRunControllers.has(targetId)) {
240
+ const params = request.params || {};
241
+ const targetId = params.requestId || params.id;
242
+ const projectKey = typeof params.projectKey === 'string' ? params.projectKey : '';
243
+ const force = params.force === true;
244
+
245
+ if (targetId && activeRunControllers.has(targetId)) {
246
+ activeRunControllers.get(targetId).abort(createCancellationError());
223
247
  return okResponse(request.id, {
224
- cancelled: false,
225
- reason: 'No active Codex run matched the cancellation request'
248
+ cancelled: true,
249
+ requestId: targetId
226
250
  });
227
251
  }
228
252
 
229
- const controller = activeRunControllers.get(targetId);
230
- controller.abort(createCancellationError());
253
+ if (projectKey) {
254
+ const entry = activeRunByProject.get(projectKey);
255
+ if (entry?.controller) {
256
+ entry.controller.abort(createCancellationError());
257
+ return okResponse(request.id, {
258
+ cancelled: true,
259
+ projectKey,
260
+ requestId: entry.id || ''
261
+ });
262
+ }
263
+ if (force && activeProjectLocks.has(projectKey)) {
264
+ activeProjectLocks.delete(projectKey);
265
+ activeRunByProject.delete(projectKey);
266
+ logDebug('codex.cancel.force_released_zombie_lock', { projectKey });
267
+ return okResponse(request.id, {
268
+ cancelled: false,
269
+ lockReleased: true,
270
+ projectKey,
271
+ reason: 'No active controller; force-released the project lock entry'
272
+ });
273
+ }
274
+ }
275
+
231
276
  return okResponse(request.id, {
232
- cancelled: true,
233
- requestId: targetId
277
+ cancelled: false,
278
+ reason: 'No active Codex run matched the cancellation request'
234
279
  });
235
280
  }
236
281
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-overleaf-link",
3
- "version": "1.3.8",
3
+ "version": "1.4.0",
4
4
  "description": "Cross-platform Chrome bridge that connects Codex to the active Overleaf project.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",