oh-my-codex 0.11.12 → 0.11.13

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 (248) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +23 -0
  4. package/README.vi.md +144 -185
  5. package/crates/omx-runtime-core/src/engine.rs +122 -4
  6. package/crates/omx-runtime-core/src/lib.rs +17 -0
  7. package/dist/cli/__tests__/autoresearch.test.js +11 -0
  8. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  9. package/dist/cli/__tests__/cleanup.test.js +117 -4
  10. package/dist/cli/__tests__/cleanup.test.js.map +1 -1
  11. package/dist/cli/__tests__/error-handling-warnings.test.js +13 -0
  12. package/dist/cli/__tests__/error-handling-warnings.test.js.map +1 -1
  13. package/dist/cli/__tests__/exec.test.js +6 -0
  14. package/dist/cli/__tests__/exec.test.js.map +1 -1
  15. package/dist/cli/__tests__/index.test.js +94 -1
  16. package/dist/cli/__tests__/index.test.js.map +1 -1
  17. package/dist/cli/__tests__/launch-fallback.test.js +3 -0
  18. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  19. package/dist/cli/__tests__/package-bin-contract.test.js +10 -0
  20. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  21. package/dist/cli/__tests__/packaged-script-resolution.test.js +4 -3
  22. package/dist/cli/__tests__/packaged-script-resolution.test.js.map +1 -1
  23. package/dist/cli/__tests__/resume.test.js +6 -0
  24. package/dist/cli/__tests__/resume.test.js.map +1 -1
  25. package/dist/cli/__tests__/setup-refresh.test.js +29 -12
  26. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  27. package/dist/cli/__tests__/star-prompt.test.js +16 -0
  28. package/dist/cli/__tests__/star-prompt.test.js.map +1 -1
  29. package/dist/cli/__tests__/uninstall.test.js +112 -1
  30. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  31. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts +2 -0
  32. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts.map +1 -0
  33. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +30 -0
  34. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -0
  35. package/dist/cli/cleanup.d.ts +2 -0
  36. package/dist/cli/cleanup.d.ts.map +1 -1
  37. package/dist/cli/cleanup.js +26 -1
  38. package/dist/cli/cleanup.js.map +1 -1
  39. package/dist/cli/index.d.ts +7 -0
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +161 -50
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/cli/setup.d.ts.map +1 -1
  44. package/dist/cli/setup.js +15 -14
  45. package/dist/cli/setup.js.map +1 -1
  46. package/dist/cli/star-prompt.d.ts.map +1 -1
  47. package/dist/cli/star-prompt.js +1 -0
  48. package/dist/cli/star-prompt.js.map +1 -1
  49. package/dist/cli/team.d.ts.map +1 -1
  50. package/dist/cli/team.js +5 -1
  51. package/dist/cli/team.js.map +1 -1
  52. package/dist/cli/uninstall.d.ts.map +1 -1
  53. package/dist/cli/uninstall.js +26 -0
  54. package/dist/cli/uninstall.js.map +1 -1
  55. package/dist/cli/update.d.ts.map +1 -1
  56. package/dist/cli/update.js +1 -0
  57. package/dist/cli/update.js.map +1 -1
  58. package/dist/config/__tests__/generator-idempotent.test.js +4 -4
  59. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  60. package/dist/config/__tests__/mcp-registry.test.js +13 -16
  61. package/dist/config/__tests__/mcp-registry.test.js.map +1 -1
  62. package/dist/config/mcp-registry.d.ts +1 -0
  63. package/dist/config/mcp-registry.d.ts.map +1 -1
  64. package/dist/config/mcp-registry.js +4 -4
  65. package/dist/config/mcp-registry.js.map +1 -1
  66. package/dist/config/models.d.ts +1 -0
  67. package/dist/config/models.d.ts.map +1 -1
  68. package/dist/config/models.js +39 -1
  69. package/dist/config/models.js.map +1 -1
  70. package/dist/hooks/__tests__/keyword-detector.test.js +12 -1
  71. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  72. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +499 -17
  73. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  74. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +140 -14
  75. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  76. package/dist/hooks/__tests__/notify-hook-modules.test.js +5 -0
  77. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  78. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts +2 -0
  79. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts.map +1 -0
  80. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +597 -0
  81. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -0
  82. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +15 -1
  83. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  84. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +73 -53
  85. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  86. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +193 -2
  87. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  88. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +183 -0
  89. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  90. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +255 -97
  91. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  92. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
  93. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  94. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +46 -0
  95. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  96. package/dist/hooks/keyword-detector.d.ts +1 -0
  97. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  98. package/dist/hooks/keyword-detector.js +48 -0
  99. package/dist/hooks/keyword-detector.js.map +1 -1
  100. package/dist/hooks/session.d.ts.map +1 -1
  101. package/dist/hooks/session.js +1 -0
  102. package/dist/hooks/session.js.map +1 -1
  103. package/dist/hud/__tests__/state.test.js +70 -1
  104. package/dist/hud/__tests__/state.test.js.map +1 -1
  105. package/dist/hud/state.d.ts.map +1 -1
  106. package/dist/hud/state.js +10 -37
  107. package/dist/hud/state.js.map +1 -1
  108. package/dist/mcp/state-server.d.ts.map +1 -1
  109. package/dist/mcp/state-server.js +5 -0
  110. package/dist/mcp/state-server.js.map +1 -1
  111. package/dist/modes/__tests__/base-session-scope.test.js +46 -0
  112. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  113. package/dist/modes/base.d.ts.map +1 -1
  114. package/dist/modes/base.js +4 -0
  115. package/dist/modes/base.js.map +1 -1
  116. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts +2 -0
  117. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts.map +1 -0
  118. package/dist/notifications/__tests__/custom-alias-enablement.test.js +84 -0
  119. package/dist/notifications/__tests__/custom-alias-enablement.test.js.map +1 -0
  120. package/dist/notifications/__tests__/idle-cooldown.test.js +55 -0
  121. package/dist/notifications/__tests__/idle-cooldown.test.js.map +1 -1
  122. package/dist/notifications/idle-cooldown.d.ts +8 -6
  123. package/dist/notifications/idle-cooldown.d.ts.map +1 -1
  124. package/dist/notifications/idle-cooldown.js +53 -22
  125. package/dist/notifications/idle-cooldown.js.map +1 -1
  126. package/dist/notifications/notifier.js +1 -1
  127. package/dist/notifications/notifier.js.map +1 -1
  128. package/dist/notifications/reply-listener.d.ts.map +1 -1
  129. package/dist/notifications/reply-listener.js +1 -0
  130. package/dist/notifications/reply-listener.js.map +1 -1
  131. package/dist/openclaw/config.js +2 -2
  132. package/dist/openclaw/config.js.map +1 -1
  133. package/dist/runtime/bridge.d.ts +1 -0
  134. package/dist/runtime/bridge.d.ts.map +1 -1
  135. package/dist/runtime/bridge.js +2 -6
  136. package/dist/runtime/bridge.js.map +1 -1
  137. package/dist/scripts/notify-fallback-watcher.js +97 -59
  138. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  139. package/dist/scripts/notify-hook/auto-nudge.d.ts +2 -1
  140. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  141. package/dist/scripts/notify-hook/auto-nudge.js +72 -238
  142. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  143. package/dist/scripts/notify-hook/managed-tmux.d.ts +19 -0
  144. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -0
  145. package/dist/scripts/notify-hook/managed-tmux.js +320 -0
  146. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -0
  147. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +22 -0
  148. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -0
  149. package/dist/scripts/notify-hook/ralph-session-resume.js +277 -0
  150. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -0
  151. package/dist/scripts/notify-hook/state-io.d.ts +1 -1
  152. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  153. package/dist/scripts/notify-hook/state-io.js +2 -10
  154. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  155. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  156. package/dist/scripts/notify-hook/team-dispatch.js +60 -59
  157. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  158. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -1
  159. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  160. package/dist/scripts/notify-hook/team-leader-nudge.js +13 -5
  161. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  162. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  163. package/dist/scripts/notify-hook/team-tmux-guard.js +1 -19
  164. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  165. package/dist/scripts/notify-hook/team-worker.js +4 -4
  166. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  167. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -1
  168. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  169. package/dist/scripts/notify-hook/tmux-injection.js +102 -35
  170. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  171. package/dist/scripts/notify-hook.js +144 -20
  172. package/dist/scripts/notify-hook.js.map +1 -1
  173. package/dist/scripts/tmux-hook-engine.d.ts +1 -0
  174. package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
  175. package/dist/scripts/tmux-hook-engine.js +3 -0
  176. package/dist/scripts/tmux-hook-engine.js.map +1 -1
  177. package/dist/team/__tests__/api-interop.test.js +96 -4
  178. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  179. package/dist/team/__tests__/leader-activity.test.js +107 -2
  180. package/dist/team/__tests__/leader-activity.test.js.map +1 -1
  181. package/dist/team/__tests__/runtime-cli.test.js +32 -0
  182. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  183. package/dist/team/__tests__/runtime.test.js +148 -0
  184. package/dist/team/__tests__/runtime.test.js.map +1 -1
  185. package/dist/team/__tests__/shutdown-fallback.test.js +13 -0
  186. package/dist/team/__tests__/shutdown-fallback.test.js.map +1 -1
  187. package/dist/team/__tests__/state-root.test.js +11 -1
  188. package/dist/team/__tests__/state-root.test.js.map +1 -1
  189. package/dist/team/__tests__/state.test.js +16 -5
  190. package/dist/team/__tests__/state.test.js.map +1 -1
  191. package/dist/team/__tests__/tmux-session.test.js +460 -2
  192. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  193. package/dist/team/api-interop.d.ts.map +1 -1
  194. package/dist/team/api-interop.js +34 -7
  195. package/dist/team/api-interop.js.map +1 -1
  196. package/dist/team/commit-hygiene.d.ts +60 -0
  197. package/dist/team/commit-hygiene.d.ts.map +1 -0
  198. package/dist/team/commit-hygiene.js +232 -0
  199. package/dist/team/commit-hygiene.js.map +1 -0
  200. package/dist/team/leader-activity.d.ts.map +1 -1
  201. package/dist/team/leader-activity.js +17 -35
  202. package/dist/team/leader-activity.js.map +1 -1
  203. package/dist/team/runtime-cli.d.ts +9 -1
  204. package/dist/team/runtime-cli.d.ts.map +1 -1
  205. package/dist/team/runtime-cli.js +15 -6
  206. package/dist/team/runtime-cli.js.map +1 -1
  207. package/dist/team/runtime.d.ts +7 -2
  208. package/dist/team/runtime.d.ts.map +1 -1
  209. package/dist/team/runtime.js +391 -63
  210. package/dist/team/runtime.js.map +1 -1
  211. package/dist/team/state/dispatch.js +1 -1
  212. package/dist/team/state/dispatch.js.map +1 -1
  213. package/dist/team/state/mailbox.d.ts +1 -0
  214. package/dist/team/state/mailbox.d.ts.map +1 -1
  215. package/dist/team/state/mailbox.js +54 -8
  216. package/dist/team/state/mailbox.js.map +1 -1
  217. package/dist/team/state-root.d.ts +1 -1
  218. package/dist/team/state-root.d.ts.map +1 -1
  219. package/dist/team/state-root.js +8 -3
  220. package/dist/team/state-root.js.map +1 -1
  221. package/dist/team/state.d.ts.map +1 -1
  222. package/dist/team/state.js +66 -3
  223. package/dist/team/state.js.map +1 -1
  224. package/dist/team/tmux-session.d.ts.map +1 -1
  225. package/dist/team/tmux-session.js +69 -27
  226. package/dist/team/tmux-session.js.map +1 -1
  227. package/dist/utils/__tests__/platform-command.test.js +101 -2
  228. package/dist/utils/__tests__/platform-command.test.js.map +1 -1
  229. package/dist/utils/git-layout.d.ts +8 -0
  230. package/dist/utils/git-layout.d.ts.map +1 -0
  231. package/dist/utils/git-layout.js +58 -0
  232. package/dist/utils/git-layout.js.map +1 -0
  233. package/dist/utils/platform-command.d.ts.map +1 -1
  234. package/dist/utils/platform-command.js +32 -1
  235. package/dist/utils/platform-command.js.map +1 -1
  236. package/package.json +6 -6
  237. package/src/scripts/notify-fallback-watcher.ts +96 -58
  238. package/src/scripts/notify-hook/auto-nudge.ts +75 -230
  239. package/src/scripts/notify-hook/managed-tmux.ts +324 -0
  240. package/src/scripts/notify-hook/ralph-session-resume.ts +337 -0
  241. package/src/scripts/notify-hook/state-io.ts +2 -10
  242. package/src/scripts/notify-hook/team-dispatch.ts +70 -54
  243. package/src/scripts/notify-hook/team-leader-nudge.ts +19 -5
  244. package/src/scripts/notify-hook/team-tmux-guard.ts +0 -20
  245. package/src/scripts/notify-hook/team-worker.ts +4 -4
  246. package/src/scripts/notify-hook/tmux-injection.ts +103 -33
  247. package/src/scripts/notify-hook.ts +150 -21
  248. package/src/scripts/tmux-hook-engine.ts +4 -0
@@ -10,7 +10,7 @@ import { resolveBridgeStateDir, resolveRuntimeBinaryPath } from '../../runtime/b
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
12
12
  import { runProcess } from './process-runner.js';
13
- import { resolvePaneTarget } from './tmux-injection.js';
13
+ import { resolvePaneTarget, resolveSessionToPane } from './tmux-injection.js';
14
14
  import { evaluatePaneInjectionReadiness, sendPaneInput } from './team-tmux-guard.js';
15
15
  import {
16
16
  buildCapturePaneArgv,
@@ -70,7 +70,7 @@ async function readBridgeDispatchRequests(stateDir, teamName) {
70
70
  transport_preference: safeString(metadata.transport_preference).trim() || 'hook_preferred_with_fallback',
71
71
  fallback_allowed: typeof metadata.fallback_allowed === 'boolean' ? metadata.fallback_allowed : true,
72
72
  status: safeString(record.status).trim() || 'pending',
73
- attempt_count: 0,
73
+ attempt_count: Number.isFinite(metadata.attempt_count) ? Number(metadata.attempt_count) : 0,
74
74
  created_at: safeString(record.created_at).trim() || new Date().toISOString(),
75
75
  updated_at:
76
76
  safeString(record.delivered_at).trim()
@@ -182,8 +182,7 @@ function parseTriggerCooldownEntry(entry) {
182
182
  };
183
183
  }
184
184
 
185
- async function withDispatchLock(teamDirPath, fn) {
186
- const lockDir = join(teamDirPath, 'dispatch', '.lock');
185
+ async function withLockDirectory(lockDir, timeoutError, fn) {
187
186
  const ownerPath = join(lockDir, 'owner');
188
187
  const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
189
188
  const deadline = Date.now() + 5_000;
@@ -210,7 +209,7 @@ async function withDispatchLock(teamDirPath, fn) {
210
209
  } catch {
211
210
  // best effort
212
211
  }
213
- if (Date.now() > deadline) throw new Error(`Timed out acquiring dispatch lock for ${teamDirPath}`);
212
+ if (Date.now() > deadline) throw new Error(timeoutError);
214
213
  await new Promise((resolveDelay) => setTimeout(resolveDelay, 25));
215
214
  }
216
215
  }
@@ -229,57 +228,63 @@ async function withDispatchLock(teamDirPath, fn) {
229
228
  }
230
229
  }
231
230
 
232
- async function withMailboxLock(teamDirPath, workerName, fn) {
233
- const lockDir = join(teamDirPath, 'mailbox', `.lock-${workerName}`);
234
- const ownerPath = join(lockDir, 'owner');
235
- const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
236
- const deadline = Date.now() + 5_000;
237
- await mkdir(dirname(lockDir), { recursive: true });
238
-
239
- while (true) {
240
- try {
241
- await mkdir(lockDir, { recursive: false });
242
- try {
243
- await writeFile(ownerPath, ownerToken, 'utf8');
244
- } catch (error) {
245
- await rm(lockDir, { recursive: true, force: true });
246
- throw error;
247
- }
248
- break;
249
- } catch (error) {
250
- if (error?.code !== 'EEXIST') throw error;
251
- try {
252
- const info = await stat(lockDir);
253
- if (Date.now() - info.mtimeMs > DISPATCH_LOCK_STALE_MS) {
254
- await rm(lockDir, { recursive: true, force: true });
255
- continue;
256
- }
257
- } catch {
258
- // best effort
259
- }
260
- if (Date.now() > deadline) throw new Error(`Timed out acquiring mailbox lock for ${teamDirPath}/${workerName}`);
261
- await new Promise((resolveDelay) => setTimeout(resolveDelay, 25));
262
- }
263
- }
231
+ async function withDispatchLock(teamDirPath, fn) {
232
+ return await withLockDirectory(
233
+ join(teamDirPath, 'dispatch', '.lock'),
234
+ `Timed out acquiring dispatch lock for ${teamDirPath}`,
235
+ fn,
236
+ );
237
+ }
264
238
 
265
- try {
266
- return await fn();
267
- } finally {
268
- try {
269
- const currentOwner = await readFile(ownerPath, 'utf8');
270
- if (currentOwner.trim() === ownerToken) {
271
- await rm(lockDir, { recursive: true, force: true });
272
- }
273
- } catch {
274
- // best effort
275
- }
276
- }
239
+ async function withMailboxLock(teamDirPath, workerName, fn) {
240
+ return await withLockDirectory(
241
+ join(teamDirPath, 'mailbox', `.lock-${workerName}`),
242
+ `Timed out acquiring mailbox lock for ${teamDirPath}/${workerName}`,
243
+ fn,
244
+ );
277
245
  }
278
246
 
279
247
  function resolveLeaderPaneId(config) {
280
248
  return safeString(config?.leader_pane_id).trim();
281
249
  }
282
250
 
251
+ function serializeDispatchRequestRecord(request) {
252
+ return {
253
+ request_id: safeString(request.request_id).trim(),
254
+ target: safeString(request.to_worker).trim(),
255
+ status: safeString(request.status).trim() || 'pending',
256
+ created_at: safeString(request.created_at).trim() || new Date().toISOString(),
257
+ notified_at: safeString(request.notified_at).trim() || null,
258
+ delivered_at: safeString(request.delivered_at).trim() || null,
259
+ failed_at: safeString(request.failed_at).trim() || null,
260
+ reason: safeString(request.last_reason).trim() || null,
261
+ metadata: {
262
+ kind: safeString(request.kind).trim() || 'inbox',
263
+ team_name: safeString(request.team_name).trim(),
264
+ worker_index: Number.isFinite(request.worker_index) ? Number(request.worker_index) : undefined,
265
+ pane_id: safeString(request.pane_id).trim() || undefined,
266
+ trigger_message: safeString(request.trigger_message).trim(),
267
+ message_id: safeString(request.message_id).trim() || undefined,
268
+ inbox_correlation_key: safeString(request.inbox_correlation_key).trim() || undefined,
269
+ transport_preference: safeString(request.transport_preference).trim() || 'hook_preferred_with_fallback',
270
+ fallback_allowed: typeof request.fallback_allowed === 'boolean' ? request.fallback_allowed : true,
271
+ attempt_count: Number.isFinite(request.attempt_count) ? Number(request.attempt_count) : 0,
272
+ },
273
+ };
274
+ }
275
+
276
+ async function writeBridgeDispatchCompat(stateDir, teamName, requests) {
277
+ const compatPath = join(stateDir, 'dispatch.json');
278
+ const current = await readJson(compatPath, { records: [] });
279
+ const existing = Array.isArray(current?.records) ? current.records : [];
280
+ const otherTeams = existing.filter((record) => {
281
+ const metadata = record?.metadata && typeof record.metadata === 'object' ? record.metadata : {};
282
+ return safeString(metadata.team_name).trim() !== teamName;
283
+ });
284
+ const records = [...otherTeams, ...requests.map(serializeDispatchRequestRecord)];
285
+ await writeJsonAtomic(compatPath, { records });
286
+ }
287
+
283
288
 
284
289
  function defaultInjectTarget(request, config) {
285
290
  if (request.to_worker === 'leader-fixed') {
@@ -367,7 +372,15 @@ async function injectDispatchRequest(request, config, cwd, stateDir) {
367
372
  if (!target) {
368
373
  return { ok: false, reason: 'missing_tmux_target' };
369
374
  }
370
- const resolution = await resolvePaneTarget(target, '', cwd, '');
375
+ let resolution;
376
+ if (target.type === 'session') {
377
+ const paneId = await resolveSessionToPane(target.value).catch(() => null);
378
+ resolution = paneId
379
+ ? { paneTarget: paneId, reason: 'session_target_resolved' }
380
+ : { paneTarget: null, reason: 'target_not_found' };
381
+ } else {
382
+ resolution = await resolvePaneTarget(target, '', '', '', {});
383
+ }
371
384
  if (!resolution.paneTarget) {
372
385
  return { ok: false, reason: `target_resolution_failed:${resolution.reason}` };
373
386
  }
@@ -676,9 +689,11 @@ export async function drainPendingTeamDispatch({
676
689
  request.notified_at = nowIso;
677
690
  request.last_reason = result.reason;
678
691
  runtimeExec({ command: 'MarkNotified', request_id: request.request_id, channel: 'tmux' }, stateDir);
679
- if (usingLegacyRequests && request.kind === 'mailbox' && request.message_id) {
692
+ if (request.kind === 'mailbox' && request.message_id) {
680
693
  runtimeExec({ command: 'MarkMailboxNotified', message_id: request.message_id }, stateDir);
681
- await updateMailboxNotified(stateDir, teamName, request.to_worker, request.message_id).catch(() => {});
694
+ if (usingLegacyRequests) {
695
+ await updateMailboxNotified(stateDir, teamName, request.to_worker, request.message_id).catch(() => {});
696
+ }
682
697
  }
683
698
  processed += 1;
684
699
  mutated = true;
@@ -725,8 +740,9 @@ export async function drainPendingTeamDispatch({
725
740
  await writeJsonAtomic(issueCooldownStatePath(teamDirPath), issueCooldownState);
726
741
  triggerCooldownState.by_trigger = triggerCooldownByKey;
727
742
  await writeJsonAtomic(triggerCooldownStatePath(teamDirPath), triggerCooldownState);
728
- if (usingLegacyRequests) {
729
- await writeJsonAtomic(requestsPath, requests);
743
+ await writeJsonAtomic(requestsPath, requests);
744
+ if (!usingLegacyRequests) {
745
+ await writeBridgeDispatchCompat(stateDir, teamName, requests);
730
746
  }
731
747
  }
732
748
  });
@@ -510,7 +510,13 @@ async function emitLeaderNudgeDeferredEvent(cwd, teamName, reason, nowIso, { tmu
510
510
  }
511
511
  }
512
512
 
513
- export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputedLeaderStale }) {
513
+ export async function maybeNudgeTeamLeader({
514
+ cwd,
515
+ stateDir,
516
+ logsDir,
517
+ preComputedLeaderStale,
518
+ allowFreshMailboxNudges = true,
519
+ }) {
514
520
  const intervalMs = resolveLeaderNudgeIntervalMs();
515
521
  const idleCooldownMs = resolveLeaderAllIdleNudgeCooldownMs();
516
522
  const fallbackProgressStallThresholdMs = resolveFallbackProgressStallThresholdMs();
@@ -683,7 +689,8 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
683
689
  const stalledTeamNudge = teamProgressStalled && (dueByTime || !previousStalledTeamNudge);
684
690
  const staleFollowupDue = stalePanesNudge && dueByTime;
685
691
 
686
- if (!shouldSendAllIdleNudge && !hasNewMessage && !stalledTeamNudge && !staleFollowupDue) continue;
692
+ const hasActionableNewMessage = hasNewMessage && (allowFreshMailboxNudges || leaderStale);
693
+ if (!shouldSendAllIdleNudge && !hasActionableNewMessage && !stalledTeamNudge && !staleFollowupDue) continue;
687
694
 
688
695
  let nudgeReason = '';
689
696
  let text = '';
@@ -717,7 +724,7 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
717
724
  `Team ${teamName}: ${stallPrefix}no progress ${formatDurationMs(stalledForMs)}. `
718
725
  + `${leaderActionGuidance} `
719
726
  + `(p:${pending} ip:${in_progress} b:${blocked}${missingSignals})`;
720
- } else if (stalePanesNudge && hasNewMessage) {
727
+ } else if (stalePanesNudge && hasActionableNewMessage) {
721
728
  nudgeReason = 'stale_leader_with_messages';
722
729
  text =
723
730
  `Team ${teamName}: leader stale, ${paneStatus.paneCount} pane(s) active, ${messages.length} msg(s) pending. `
@@ -727,7 +734,7 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
727
734
  text =
728
735
  `Team ${teamName}: leader stale, ${paneStatus.paneCount} worker pane(s) still active. `
729
736
  + leaderActionGuidance;
730
- } else if (hasNewMessage) {
737
+ } else if (hasActionableNewMessage) {
731
738
  nudgeReason = 'new_mailbox_message';
732
739
  text = `Team ${teamName}: ${messages.length} msg(s) for leader. ${buildMailboxCheckReminder(teamName)}`;
733
740
  } else {
@@ -763,7 +770,14 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
763
770
  continue;
764
771
  }
765
772
 
766
- const paneGuard = await evaluatePaneInjectionReadiness(tmuxTarget, { skipIfScrolling: true });
773
+ const paneGuard = await evaluatePaneInjectionReadiness(tmuxTarget, {
774
+ skipIfScrolling: true,
775
+ // Leader nudges should still queue into a live Codex pane even while the
776
+ // agent is busy; shell/copy-mode guards stay enforced.
777
+ requireRunningAgent: true,
778
+ requireReady: false,
779
+ requireIdle: false,
780
+ });
767
781
  if (!paneGuard.ok) {
768
782
  const deferredReason = paneGuard.reason === 'pane_running_shell'
769
783
  ? LEADER_PANE_SHELL_NO_INJECTION_REASON
@@ -8,7 +8,6 @@ import {
8
8
  isPaneRunningShell,
9
9
  paneHasActiveTask,
10
10
  paneLooksReady,
11
- resolveCodexPane,
12
11
  } from '../tmux-hook-engine.js';
13
12
 
14
13
  export function mapPaneInjectionReadinessReason(reason: any): any {
@@ -33,25 +32,6 @@ export async function evaluatePaneInjectionReadiness(paneTarget: any, {
33
32
  paneCapture: '',
34
33
  };
35
34
  }
36
-
37
- // Canonical bypass: if resolveCodexPane confirms this is a codex pane
38
- // (via pane_start_command), skip all readiness guards. The pane IS running
39
- // codex even though tmux may report cmd=sh (shell wrapper).
40
- try {
41
- if (resolveCodexPane() === target) {
42
- return {
43
- ok: true,
44
- sent: false,
45
- reason: 'ok',
46
- paneTarget: target,
47
- paneCurrentCommand: 'codex',
48
- paneCapture: '',
49
- };
50
- }
51
- } catch {
52
- // Non-fatal: fall through to normal readiness checks
53
- }
54
-
55
35
  if (skipIfScrolling) {
56
36
  try {
57
37
  const modeResult = await runProcess('tmux', buildPaneInModeArgv(target), 1000);
@@ -262,7 +262,7 @@ async function resolveCanonicalLeaderPaneId(_tmuxSession, leaderPaneId) {
262
262
  const normalizedLeaderPaneId = safeString(leaderPaneId).trim();
263
263
  if (normalizedLeaderPaneId) {
264
264
  try {
265
- const resolved = await resolvePaneTarget({ type: 'pane', value: normalizedLeaderPaneId }, '', '', '');
265
+ const resolved = await resolvePaneTarget({ type: 'pane', value: normalizedLeaderPaneId }, '', '', '', {});
266
266
  const paneTarget = safeString(resolved?.paneTarget).trim();
267
267
  if (paneTarget) return paneTarget;
268
268
  } catch {
@@ -541,8 +541,8 @@ export async function maybeNotifyLeaderWorkerIdle({ cwd, stateDir, logsDir, pars
541
541
  await rename(tmpPath, prevStatePath);
542
542
  } catch { /* best effort */ }
543
543
 
544
- // Only fire on working->idle transition (non-idle to idle)
545
- if (currentState !== 'idle') return;
544
+ // Fire when a worker leaves active work into an idle-ish terminal state.
545
+ if (currentState !== 'idle' && currentState !== 'done') return;
546
546
  if (!statusFresh) return;
547
547
  if (prevState === 'idle' || prevState === 'done') return;
548
548
 
@@ -608,7 +608,7 @@ export async function maybeNotifyLeaderWorkerIdle({ cwd, stateDir, logsDir, pars
608
608
  }
609
609
 
610
610
  // Build notification message with context
611
- const parts = [`[OMX] ${workerName} idle`];
611
+ const parts = [`[OMX] ${workerName} ${currentState}`];
612
612
  if (prevState && prevState !== 'unknown') parts.push(`(was: ${prevState})`);
613
613
  if (currentTaskId) parts.push(`task: ${currentTaskId}`);
614
614
  if (currentReason) parts.push(`reason: ${currentReason}`);
@@ -17,13 +17,13 @@ import {
17
17
  } from './state-io.js';
18
18
  import { runProcess } from './process-runner.js';
19
19
  import { logTmuxHookEvent } from './log.js';
20
+ import { resolveManagedCurrentPane, resolveManagedSessionContext, verifyManagedPaneTarget } from './managed-tmux.js';
20
21
  import { evaluatePaneInjectionReadiness, mapPaneInjectionReadinessReason, sendPaneInput } from './team-tmux-guard.js';
21
22
  import {
22
23
  normalizeTmuxHookConfig,
23
24
  pickActiveMode,
24
25
  evaluateInjectionGuards,
25
26
  buildSendKeysArgv,
26
- resolveCodexPane,
27
27
  } from '../tmux-hook-engine.js';
28
28
 
29
29
  function isHudPaneStartCommand(startCommand: any): boolean {
@@ -96,6 +96,25 @@ async function resolveCanonicalPaneFromPaneTarget(paneTarget: any, expectedCwd:
96
96
  return finalizeResolvedPane(healedPaneId, 'healed_hud_pane_target', expectedCwd);
97
97
  }
98
98
 
99
+ async function resolvePreferredModePane(stateDir: string, allowedModes: string[]): Promise<{ mode: string; state: any; pane: string } | null> {
100
+ const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir).catch(() => [stateDir]);
101
+ const dirs = [...scopedDirs];
102
+ if (!dirs.map((dir) => resolvePath(dir)).includes(resolvePath(stateDir))) {
103
+ dirs.push(stateDir);
104
+ }
105
+ for (const dir of dirs) {
106
+ for (const mode of allowedModes || []) {
107
+ const path = join(dir, `${mode}-state.json`);
108
+ const parsed = await readJsonIfExists(path, null);
109
+ const pane = safeString(parsed?.tmux_pane_id || '').trim();
110
+ if (parsed?.active && pane) {
111
+ return { mode, state: parsed, pane };
112
+ }
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+
99
118
  export async function resolveSessionToPane(sessionName: any): Promise<string | null> {
100
119
  const result = await runProcess('tmux', ['list-panes', '-t', sessionName, '-F', '#{pane_id}\t#{pane_active}\t#{pane_current_command}\t#{pane_start_command}']);
101
120
  const rows = result.stdout
@@ -128,27 +147,35 @@ export async function resolveSessionToPane(sessionName: any): Promise<string | n
128
147
  return nonHudRows[0]?.paneId || null;
129
148
  }
130
149
 
131
- export async function resolvePaneTarget(target: any, fallbackPane: any, expectedCwd: any, modePane: any): Promise<any> {
132
- const canonicalFallbackPane = safeString(fallbackPane).trim();
133
- if (canonicalFallbackPane) {
134
- try {
135
- return await finalizeResolvedPane(canonicalFallbackPane, 'fallback_current_pane', expectedCwd);
136
- } catch {
137
- // Fall through to mode/config probes
138
- }
150
+ export async function resolvePaneTarget(target: any, expectedCwd: any, modePane: any, cwd: string, payload: any): Promise<any> {
151
+ const requiresManagedOwnership = safeString(cwd).trim() !== '' && safeString(payload?.session_id || payload?.['session-id'] || process.env.OMX_SESSION_ID || '').trim() !== '';
152
+ const managedContext = requiresManagedOwnership
153
+ ? await resolveManagedSessionContext(cwd, payload, { allowTeamWorker: false })
154
+ : { managed: false, reason: 'not_required', invocationSessionId: '', sessionState: null, expectedTmuxSessionName: '', currentTmuxSessionName: '' };
155
+ if (requiresManagedOwnership && !managedContext.managed) {
156
+ return { paneTarget: null, reason: managedContext.reason || 'unmanaged_session' };
139
157
  }
140
158
 
141
- if (modePane) {
159
+ const canonicalModePane = safeString(modePane).trim();
160
+ if (canonicalModePane) {
142
161
  try {
143
- const resolved = await resolveCanonicalPaneFromPaneTarget(modePane, expectedCwd);
162
+ const resolved = await resolveCanonicalPaneFromPaneTarget(canonicalModePane, expectedCwd);
144
163
  if (resolved.paneTarget) {
145
- return {
146
- ...resolved,
147
- reason: resolved.reason === 'ok' ? 'fallback_mode_state_pane' : resolved.reason,
148
- };
164
+ const ownership = requiresManagedOwnership
165
+ ? await verifyManagedPaneTarget(resolved.paneTarget, cwd, payload, { allowTeamWorker: false })
166
+ : { ok: true };
167
+ if (ownership.ok) {
168
+ return {
169
+ ...resolved,
170
+ reason: resolved.reason === 'ok' ? 'fallback_mode_state_pane' : resolved.reason,
171
+ source: 'mode_state',
172
+ healTarget: true,
173
+ };
174
+ }
175
+ return { paneTarget: null, reason: ownership.reason || 'pane_not_managed_session' };
149
176
  }
150
177
  } catch {
151
- // Fall through to config probes
178
+ // Fall through to explicit config target
152
179
  }
153
180
  }
154
181
 
@@ -157,20 +184,52 @@ export async function resolvePaneTarget(target: any, fallbackPane: any, expected
157
184
  if (target.type === 'pane') {
158
185
  try {
159
186
  const resolved = await resolveCanonicalPaneFromPaneTarget(target.value, expectedCwd);
160
- if (resolved.paneTarget) return resolved;
161
- } catch {
162
- // Fall through
163
- }
164
- } else {
165
- try {
166
- const paneId = await resolveSessionToPane(target.value);
167
- if (paneId) return await finalizeResolvedPane(paneId, 'ok', expectedCwd);
187
+ if (resolved.paneTarget) {
188
+ const ownership = requiresManagedOwnership
189
+ ? await verifyManagedPaneTarget(resolved.paneTarget, cwd, payload, { allowTeamWorker: false })
190
+ : { ok: true };
191
+ if (ownership.ok) {
192
+ return {
193
+ ...resolved,
194
+ reason: resolved.reason === 'ok' ? 'explicit_pane_target' : resolved.reason,
195
+ source: 'explicit_target',
196
+ healTarget: true,
197
+ };
198
+ }
199
+ return { paneTarget: null, reason: ownership.reason || 'pane_not_managed_session' };
200
+ }
168
201
  } catch {
169
202
  // Fall through
170
203
  }
204
+ return { paneTarget: null, reason: 'target_not_found' };
171
205
  }
172
206
 
173
- return { paneTarget: null, reason: 'target_not_found' };
207
+ try {
208
+ if (!requiresManagedOwnership) return { paneTarget: null, reason: 'target_session_requires_managed_context' };
209
+ const explicitSessionTarget = safeString(target.value).trim();
210
+ const expectedSessionTarget = safeString(managedContext.expectedTmuxSessionName).trim();
211
+ const sessionIdTarget = safeString(managedContext.invocationSessionId).trim();
212
+ const stateSessionTarget = safeString(managedContext.sessionState?.session_id).trim();
213
+ const allowedSessionTargets = new Set([expectedSessionTarget, sessionIdTarget, stateSessionTarget].filter(Boolean));
214
+ if (!allowedSessionTargets.has(explicitSessionTarget)) {
215
+ return { paneTarget: null, reason: 'target_session_not_managed' };
216
+ }
217
+ const paneId = await resolveSessionToPane(expectedSessionTarget);
218
+ if (!paneId) return { paneTarget: null, reason: 'target_not_found' };
219
+ const resolved = await finalizeResolvedPane(paneId, 'managed_session_target', expectedCwd);
220
+ if (!resolved.paneTarget) return resolved;
221
+ const ownership = await verifyManagedPaneTarget(resolved.paneTarget, cwd, payload, { allowTeamWorker: false });
222
+ if (!ownership.ok) {
223
+ return { paneTarget: null, reason: ownership.reason || 'pane_not_managed_session' };
224
+ }
225
+ return {
226
+ ...resolved,
227
+ source: 'explicit_target',
228
+ healTarget: true,
229
+ };
230
+ } catch {
231
+ return { paneTarget: null, reason: 'target_not_found' };
232
+ }
174
233
  }
175
234
 
176
235
  export async function handleTmuxInjection({
@@ -202,7 +261,6 @@ export async function handleTmuxInjection({
202
261
  const activeModes: string[] = [];
203
262
  const activeModeStates: Record<string, any> = {};
204
263
  const scannedStateDirs = new Set<string>();
205
- const payloadSessionId = safeString(payload.session_id || payload['session-id'] || '');
206
264
  const scanActiveModeStateDirs = async (dirs: string[], preserveExisting = false) => {
207
265
  for (const scopedDir of dirs) {
208
266
  const resolvedScopedDir = resolvePath(scopedDir);
@@ -225,7 +283,7 @@ export async function handleTmuxInjection({
225
283
  }
226
284
  };
227
285
  try {
228
- const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir, payloadSessionId);
286
+ const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir);
229
287
  await scanActiveModeStateDirs(scopedDirs);
230
288
 
231
289
  if (!pickActiveMode(activeModes, config.allowed_modes) && !scannedStateDirs.has(resolvePath(stateDir))) {
@@ -235,9 +293,10 @@ export async function handleTmuxInjection({
235
293
  // Non-fatal
236
294
  }
237
295
 
238
- const mode = pickActiveMode(activeModes, config.allowed_modes);
239
- const modeState = mode ? (activeModeStates[mode] || {}) : {};
240
- const modePane = safeString(modeState.tmux_pane_id || '');
296
+ const preferredModePane = await resolvePreferredModePane(stateDir, config.allowed_modes).catch(() => null);
297
+ const mode = preferredModePane?.mode || pickActiveMode(activeModes, config.allowed_modes);
298
+ const modeState = preferredModePane?.state || (mode ? (activeModeStates[mode] || {}) : {});
299
+ const modePane = preferredModePane?.pane || safeString(modeState.tmux_pane_id || '');
241
300
  const preGuard = evaluateInjectionGuards({
242
301
  config,
243
302
  mode,
@@ -280,8 +339,19 @@ export async function handleTmuxInjection({
280
339
  turnId,
281
340
  timestamp: nowIso,
282
341
  }), sourceText);
283
- const fallbackPane = resolveCodexPane();
284
- const resolution = await resolvePaneTarget(config.target, fallbackPane, cwd, modePane);
342
+ const preferredPaneTarget = modePane || await resolveManagedCurrentPane(cwd, payload, { allowTeamWorker: false });
343
+ let resolution = preferredModePane
344
+ ? await resolveCanonicalPaneFromPaneTarget(preferredModePane.pane, cwd).then((resolved) => (
345
+ resolved.paneTarget
346
+ ? { ...resolved, reason: 'fallback_mode_state_pane', source: 'mode_state', healTarget: true }
347
+ : resolved
348
+ ))
349
+ : preferredPaneTarget
350
+ ? await resolvePaneTarget({ type: 'pane', value: preferredPaneTarget }, cwd, '', cwd, payload)
351
+ : await resolvePaneTarget(config.target, cwd, modePane, cwd, payload);
352
+ if (!resolution.paneTarget && preferredPaneTarget) {
353
+ resolution = await resolvePaneTarget(config.target, cwd, modePane, cwd, payload);
354
+ }
285
355
  if (!resolution.paneTarget) {
286
356
  state.last_reason = resolution.reason;
287
357
  state.last_event_at = nowIso;
@@ -319,7 +389,7 @@ export async function handleTmuxInjection({
319
389
  }
320
390
 
321
391
  // Pane-canonical healing: persist resolved pane target so routing stops depending on session names or stale pane ids.
322
- if (config.target && (config.target.type !== 'pane' || safeString(config.target.value).trim() !== paneTarget)) {
392
+ if (resolution.healTarget && config.target && (config.target.type !== 'pane' || safeString(config.target.value).trim() !== paneTarget)) {
323
393
  try {
324
394
  const healed = {
325
395
  ...(rawConfig && typeof rawConfig === 'object' ? rawConfig : {}),