oh-my-codex 0.8.0 → 0.8.2
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.de.md +7 -2
- package/README.es.md +7 -2
- package/README.fr.md +7 -2
- package/README.it.md +7 -2
- package/README.ja.md +7 -2
- package/README.ko.md +7 -2
- package/README.md +61 -11
- package/README.pt.md +7 -2
- package/README.ru.md +7 -2
- package/README.tr.md +7 -2
- package/README.vi.md +7 -2
- package/README.zh-TW.md +366 -0
- package/README.zh.md +7 -2
- package/dist/cli/__tests__/index.test.js +70 -4
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +100 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +219 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/catalog-contract.d.ts.map +1 -1
- package/dist/cli/catalog-contract.js +8 -2
- package/dist/cli/catalog-contract.js.map +1 -1
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +58 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +50 -17
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +257 -0
- package/dist/cli/team.js.map +1 -1
- package/dist/config/__tests__/models.test.js +11 -11
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/models.d.ts +4 -3
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +6 -5
- package/dist/config/models.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +46 -3
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +23 -7
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +176 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +61 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +17 -7
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js +26 -16
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +2 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +41 -4
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +5 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/mcp/__tests__/path-traversal.test.js +9 -227
- package/dist/mcp/__tests__/path-traversal.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server-schema.test.js +16 -20
- package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server-team-tools.test.js +30 -487
- package/dist/mcp/__tests__/state-server-team-tools.test.js.map +1 -1
- package/dist/mcp/state-server.d.ts +179 -0
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +217 -1111
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/mcp/team-server.d.ts.map +1 -1
- package/dist/mcp/team-server.js +28 -7
- package/dist/mcp/team-server.js.map +1 -1
- package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts +5 -0
- package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/dispatch-cooldown.test.js +100 -0
- package/dist/notifications/__tests__/dispatch-cooldown.test.js.map +1 -0
- package/dist/notifications/__tests__/temp-mode.test.d.ts +2 -0
- package/dist/notifications/__tests__/temp-mode.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/temp-mode.test.js +172 -0
- package/dist/notifications/__tests__/temp-mode.test.js.map +1 -0
- package/dist/notifications/config.d.ts.map +1 -1
- package/dist/notifications/config.js +59 -6
- package/dist/notifications/config.js.map +1 -1
- package/dist/notifications/dispatch-cooldown.d.ts +36 -0
- package/dist/notifications/dispatch-cooldown.d.ts.map +1 -0
- package/dist/notifications/dispatch-cooldown.js +109 -0
- package/dist/notifications/dispatch-cooldown.js.map +1 -0
- package/dist/notifications/index.d.ts +5 -0
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +39 -8
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/temp-contract.d.ts +22 -0
- package/dist/notifications/temp-contract.d.ts.map +1 -0
- package/dist/notifications/temp-contract.js +147 -0
- package/dist/notifications/temp-contract.js.map +1 -0
- package/dist/notifications/types.d.ts +18 -0
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/openclaw/__tests__/config.test.js +81 -0
- package/dist/openclaw/__tests__/config.test.js.map +1 -1
- package/dist/openclaw/__tests__/dispatcher.test.js +50 -7
- package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
- package/dist/openclaw/config.d.ts +4 -0
- package/dist/openclaw/config.d.ts.map +1 -1
- package/dist/openclaw/config.js +110 -16
- package/dist/openclaw/config.js.map +1 -1
- package/dist/openclaw/dispatcher.d.ts +10 -4
- package/dist/openclaw/dispatcher.d.ts.map +1 -1
- package/dist/openclaw/dispatcher.js +40 -10
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/openclaw/types.d.ts +5 -1
- package/dist/openclaw/types.d.ts.map +1 -1
- package/dist/team/__tests__/api-interop.test.d.ts +2 -0
- package/dist/team/__tests__/api-interop.test.d.ts.map +1 -0
- package/dist/team/__tests__/api-interop.test.js +1052 -0
- package/dist/team/__tests__/api-interop.test.js.map +1 -0
- package/dist/team/__tests__/mcp-comm.test.js +30 -0
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +6 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +52 -22
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js +190 -0
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -0
- package/dist/team/__tests__/tmux-session.test.js +45 -2
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +20 -12
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/api-interop.d.ts +19 -0
- package/dist/team/api-interop.d.ts.map +1 -0
- package/dist/team/api-interop.js +578 -0
- package/dist/team/api-interop.js.map +1 -0
- package/dist/team/mcp-comm.d.ts.map +1 -1
- package/dist/team/mcp-comm.js +26 -0
- package/dist/team/mcp-comm.js.map +1 -1
- package/dist/team/runtime-cli.d.ts +3 -0
- package/dist/team/runtime-cli.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +24 -2
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +67 -11
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/types.d.ts +1 -1
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state.d.ts +1 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/tmux-session.d.ts +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +17 -5
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +48 -19
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/package.json +1 -1
- package/scripts/demo-claude-workers.sh +241 -0
- package/scripts/demo-team-e2e.sh +179 -0
- package/scripts/notify-hook/team-dispatch.js +186 -12
- package/scripts/notify-hook/team-leader-nudge.js +42 -2
- package/scripts/notify-hook/team-worker.js +63 -4
- package/skills/configure-notifications/SKILL.md +193 -185
- package/skills/omx-setup/SKILL.md +1 -1
- package/skills/team/SKILL.md +47 -5
- package/skills/worker/SKILL.md +40 -10
- package/templates/AGENTS.md +7 -3
- package/templates/catalog-manifest.json +26 -3
- package/skills/configure-discord/SKILL.md +0 -256
- package/skills/configure-openclaw/SKILL.md +0 -264
- package/skills/configure-slack/SKILL.md +0 -226
- package/skills/configure-telegram/SKILL.md +0 -232
|
@@ -23,6 +23,10 @@ async function writeJsonAtomic(path, value) {
|
|
|
23
23
|
const DISPATCH_LOCK_STALE_MS = 5 * 60 * 1000;
|
|
24
24
|
const DEFAULT_ISSUE_DISPATCH_COOLDOWN_MS = 15 * 60 * 1000;
|
|
25
25
|
const ISSUE_DISPATCH_COOLDOWN_ENV = 'OMX_TEAM_DISPATCH_ISSUE_COOLDOWN_MS';
|
|
26
|
+
const DEFAULT_DISPATCH_TRIGGER_COOLDOWN_MS = 30 * 1000;
|
|
27
|
+
const DISPATCH_TRIGGER_COOLDOWN_ENV = 'OMX_TEAM_DISPATCH_TRIGGER_COOLDOWN_MS';
|
|
28
|
+
const LEADER_PANE_MISSING_DEFERRED_REASON = 'leader_pane_missing_deferred';
|
|
29
|
+
const LEADER_NOTIFICATION_DEFERRED_TYPE = 'leader_notification_deferred';
|
|
26
30
|
|
|
27
31
|
function resolveIssueDispatchCooldownMs(env = process.env) {
|
|
28
32
|
const raw = safeString(env[ISSUE_DISPATCH_COOLDOWN_ENV]).trim();
|
|
@@ -32,6 +36,14 @@ function resolveIssueDispatchCooldownMs(env = process.env) {
|
|
|
32
36
|
return parsed;
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
function resolveDispatchTriggerCooldownMs(env = process.env) {
|
|
40
|
+
const raw = safeString(env[DISPATCH_TRIGGER_COOLDOWN_ENV]).trim();
|
|
41
|
+
if (raw === '') return DEFAULT_DISPATCH_TRIGGER_COOLDOWN_MS;
|
|
42
|
+
const parsed = Number.parseInt(raw, 10);
|
|
43
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_DISPATCH_TRIGGER_COOLDOWN_MS;
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
function extractIssueKey(triggerMessage) {
|
|
36
48
|
const match = safeString(triggerMessage).match(/\b([A-Z][A-Z0-9]+-\d+)\b/i);
|
|
37
49
|
return match?.[1]?.toUpperCase() || null;
|
|
@@ -41,6 +53,10 @@ function issueCooldownStatePath(teamDirPath) {
|
|
|
41
53
|
return join(teamDirPath, 'dispatch', 'issue-cooldown.json');
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
function triggerCooldownStatePath(teamDirPath) {
|
|
57
|
+
return join(teamDirPath, 'dispatch', 'trigger-cooldown.json');
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
async function readIssueCooldownState(teamDirPath) {
|
|
45
61
|
const fallback = { by_issue: {} };
|
|
46
62
|
const parsed = await readJson(issueCooldownStatePath(teamDirPath), fallback);
|
|
@@ -50,6 +66,32 @@ async function readIssueCooldownState(teamDirPath) {
|
|
|
50
66
|
return parsed;
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
async function readTriggerCooldownState(teamDirPath) {
|
|
70
|
+
const fallback = { by_trigger: {} };
|
|
71
|
+
const parsed = await readJson(triggerCooldownStatePath(teamDirPath), fallback);
|
|
72
|
+
if (!parsed || typeof parsed !== 'object' || typeof parsed.by_trigger !== 'object' || parsed.by_trigger === null) {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeTriggerKey(value) {
|
|
79
|
+
return safeString(value).replace(/\s+/g, ' ').trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseTriggerCooldownEntry(entry) {
|
|
83
|
+
if (typeof entry === 'number') {
|
|
84
|
+
return { at: entry, lastRequestId: '' };
|
|
85
|
+
}
|
|
86
|
+
if (!entry || typeof entry !== 'object') {
|
|
87
|
+
return { at: NaN, lastRequestId: '' };
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
at: Number(entry.at),
|
|
91
|
+
lastRequestId: safeString(entry.last_request_id).trim(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
53
95
|
async function withDispatchLock(teamDirPath, fn) {
|
|
54
96
|
const lockDir = join(teamDirPath, 'dispatch', '.lock');
|
|
55
97
|
const ownerPath = join(lockDir, 'owner');
|
|
@@ -145,18 +187,15 @@ async function withMailboxLock(teamDirPath, workerName, fn) {
|
|
|
145
187
|
}
|
|
146
188
|
|
|
147
189
|
function defaultInjectTarget(request, config) {
|
|
190
|
+
if (request.to_worker === 'leader-fixed') {
|
|
191
|
+
if (config.leader_pane_id) return { type: 'pane', value: config.leader_pane_id };
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
148
194
|
if (request.pane_id) return { type: 'pane', value: request.pane_id };
|
|
149
195
|
if (typeof request.worker_index === 'number' && Array.isArray(config?.workers)) {
|
|
150
196
|
const worker = config.workers.find((candidate) => Number(candidate?.index) === request.worker_index);
|
|
151
197
|
if (worker?.pane_id) return { type: 'pane', value: worker.pane_id };
|
|
152
198
|
}
|
|
153
|
-
// Leader-fixed fallback: use config.leader_pane_id when request has no
|
|
154
|
-
// pane_id or worker_index (leader is not a worker). Without this, leader
|
|
155
|
-
// dispatch falls through to the session target which hits the active pane
|
|
156
|
-
// (likely a worker). Fixes #433.
|
|
157
|
-
if (request.to_worker === 'leader-fixed' && config.leader_pane_id) {
|
|
158
|
-
return { type: 'pane', value: config.leader_pane_id };
|
|
159
|
-
}
|
|
160
199
|
if (typeof request.worker_index === 'number' && config.tmux_session) {
|
|
161
200
|
return { type: 'pane', value: `${config.tmux_session}.${request.worker_index}` };
|
|
162
201
|
}
|
|
@@ -164,6 +203,30 @@ function defaultInjectTarget(request, config) {
|
|
|
164
203
|
return null;
|
|
165
204
|
}
|
|
166
205
|
|
|
206
|
+
async function appendLeaderNotificationDeferredEvent({
|
|
207
|
+
stateDir,
|
|
208
|
+
teamName,
|
|
209
|
+
request,
|
|
210
|
+
reason,
|
|
211
|
+
nowIso,
|
|
212
|
+
}) {
|
|
213
|
+
const eventsDir = join(stateDir, 'team', teamName, 'events');
|
|
214
|
+
const eventsPath = join(eventsDir, 'events.ndjson');
|
|
215
|
+
const event = {
|
|
216
|
+
event_id: `leader-deferred-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
|
217
|
+
team: teamName,
|
|
218
|
+
type: LEADER_NOTIFICATION_DEFERRED_TYPE,
|
|
219
|
+
worker: request.to_worker,
|
|
220
|
+
to_worker: request.to_worker,
|
|
221
|
+
reason,
|
|
222
|
+
created_at: nowIso,
|
|
223
|
+
request_id: request.request_id,
|
|
224
|
+
...(request.message_id ? { message_id: request.message_id } : {}),
|
|
225
|
+
};
|
|
226
|
+
await mkdir(eventsDir, { recursive: true }).catch(() => {});
|
|
227
|
+
await appendFile(eventsPath, JSON.stringify(event) + '\n').catch(() => {});
|
|
228
|
+
}
|
|
229
|
+
|
|
167
230
|
function resolveWorkerCliForRequest(request, config) {
|
|
168
231
|
const workers = Array.isArray(config?.workers) ? config.workers : [];
|
|
169
232
|
const idx = Number.isFinite(request?.worker_index) ? Number(request.worker_index) : null;
|
|
@@ -184,6 +247,19 @@ function capturedPaneContainsTrigger(captured, trigger) {
|
|
|
184
247
|
return normalizeCaptureText(captured).includes(normalizeCaptureText(trigger));
|
|
185
248
|
}
|
|
186
249
|
|
|
250
|
+
function capturedPaneContainsTriggerNearTail(captured, trigger, nonEmptyTailLines = 24) {
|
|
251
|
+
if (!captured || !trigger) return false;
|
|
252
|
+
const normalizedTrigger = normalizeCaptureText(trigger);
|
|
253
|
+
if (!normalizedTrigger) return false;
|
|
254
|
+
const lines = safeString(captured)
|
|
255
|
+
.split('\n')
|
|
256
|
+
.map((line) => line.replace(/\r/g, '').trim())
|
|
257
|
+
.filter((line) => line.length > 0);
|
|
258
|
+
if (lines.length === 0) return false;
|
|
259
|
+
const tail = lines.slice(-Math.max(1, nonEmptyTailLines)).join(' ');
|
|
260
|
+
return normalizeCaptureText(tail).includes(normalizedTrigger);
|
|
261
|
+
}
|
|
262
|
+
|
|
187
263
|
// Ported from src/team/tmux-session.ts:949-963 — detects active CLI task indicators.
|
|
188
264
|
function paneHasActiveTask(captured) {
|
|
189
265
|
const lines = safeString(captured)
|
|
@@ -200,6 +276,40 @@ function paneHasActiveTask(captured) {
|
|
|
200
276
|
return false;
|
|
201
277
|
}
|
|
202
278
|
|
|
279
|
+
function paneIsBootstrapping(captured) {
|
|
280
|
+
const lines = safeString(captured)
|
|
281
|
+
.split('\n')
|
|
282
|
+
.map((line) => line.replace(/\r/g, '').trim())
|
|
283
|
+
.filter((line) => line.length > 0);
|
|
284
|
+
return lines.some((line) =>
|
|
285
|
+
/\b(loading|initializing|starting up)\b/i.test(line)
|
|
286
|
+
|| /\bmodel:\s*loading\b/i.test(line)
|
|
287
|
+
|| /\bconnecting\s+to\b/i.test(line),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function paneLooksReady(captured) {
|
|
292
|
+
const content = safeString(captured).trimEnd();
|
|
293
|
+
if (content === '') return false;
|
|
294
|
+
|
|
295
|
+
const lines = content
|
|
296
|
+
.split('\n')
|
|
297
|
+
.map((line) => line.replace(/\r/g, ''))
|
|
298
|
+
.map((line) => line.trimEnd())
|
|
299
|
+
.filter((line) => line.trim() !== '');
|
|
300
|
+
|
|
301
|
+
if (paneIsBootstrapping(content)) return false;
|
|
302
|
+
|
|
303
|
+
const lastLine = lines.length > 0 ? lines[lines.length - 1] : '';
|
|
304
|
+
if (/^\s*[›>❯]\s*/u.test(lastLine)) return true;
|
|
305
|
+
|
|
306
|
+
const hasCodexPromptLine = lines.some((line) => /^\s*›\s*/u.test(line));
|
|
307
|
+
const hasClaudePromptLine = lines.some((line) => /^\s*❯\s*/u.test(line));
|
|
308
|
+
if (hasCodexPromptLine || hasClaudePromptLine) return true;
|
|
309
|
+
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
203
313
|
const INJECT_VERIFY_DELAY_MS = 250;
|
|
204
314
|
const INJECT_VERIFY_ROUNDS = 3;
|
|
205
315
|
|
|
@@ -266,16 +376,29 @@ async function injectDispatchRequest(request, config, cwd) {
|
|
|
266
376
|
for (let round = 0; round < INJECT_VERIFY_ROUNDS; round++) {
|
|
267
377
|
await new Promise((r) => setTimeout(r, INJECT_VERIFY_DELAY_MS));
|
|
268
378
|
try {
|
|
269
|
-
// Primary: trigger text no longer in narrow input area
|
|
379
|
+
// Primary: trigger text no longer in narrow input area.
|
|
380
|
+
// Secondary guard: also inspect the recent non-empty tail of wide capture.
|
|
381
|
+
// This avoids false confirmations when Codex leaves the unsent draft just
|
|
382
|
+
// above a large blank area (narrow capture misses it) while still avoiding
|
|
383
|
+
// full-scrollback false positives.
|
|
270
384
|
const narrowCap = await runProcess('tmux', verifyNarrowArgv, 2000);
|
|
271
|
-
if (!capturedPaneContainsTrigger(narrowCap.stdout, request.trigger_message)) {
|
|
272
|
-
return { ok: true, reason: 'tmux_send_keys_confirmed', pane: resolution.paneTarget };
|
|
273
|
-
}
|
|
274
|
-
// Secondary: worker is actively processing (mirrors sync path tmux-session.ts:1292-1294)
|
|
275
385
|
const wideCap = await runProcess('tmux', verifyWideArgv, 2000);
|
|
386
|
+
// Worker is actively processing (mirrors sync path tmux-session.ts:1292-1294)
|
|
276
387
|
if (paneHasActiveTask(wideCap.stdout)) {
|
|
277
388
|
return { ok: true, reason: 'tmux_send_keys_confirmed_active_task', pane: resolution.paneTarget };
|
|
278
389
|
}
|
|
390
|
+
// Do not declare success while a *worker* pane is still bootstrapping / not
|
|
391
|
+
// input-ready. Otherwise a pre-ready send can be marked "confirmed" and later
|
|
392
|
+
// appear as a stuck unsent draft once the UI finishes loading.
|
|
393
|
+
// Keep leader-fixed behavior unchanged to avoid regressing leader notification flow.
|
|
394
|
+
if (request.to_worker !== 'leader-fixed' && !paneLooksReady(wideCap.stdout)) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const triggerInNarrow = capturedPaneContainsTrigger(narrowCap.stdout, request.trigger_message);
|
|
398
|
+
const triggerNearTail = capturedPaneContainsTriggerNearTail(wideCap.stdout, request.trigger_message);
|
|
399
|
+
if (!triggerInNarrow && !triggerNearTail) {
|
|
400
|
+
return { ok: true, reason: 'tmux_send_keys_confirmed', pane: resolution.paneTarget };
|
|
401
|
+
}
|
|
279
402
|
} catch {
|
|
280
403
|
// capture failed; fall through to retry C-m
|
|
281
404
|
}
|
|
@@ -332,6 +455,7 @@ export async function drainPendingTeamDispatch({
|
|
|
332
455
|
let skipped = 0;
|
|
333
456
|
let failed = 0;
|
|
334
457
|
const issueCooldownMs = resolveIssueDispatchCooldownMs();
|
|
458
|
+
const triggerCooldownMs = resolveDispatchTriggerCooldownMs();
|
|
335
459
|
|
|
336
460
|
for (const teamName of teams) {
|
|
337
461
|
if (processed >= maxPerTick) break;
|
|
@@ -346,7 +470,9 @@ export async function drainPendingTeamDispatch({
|
|
|
346
470
|
const requests = await readJson(requestsPath, []);
|
|
347
471
|
if (!Array.isArray(requests)) return;
|
|
348
472
|
const issueCooldownState = await readIssueCooldownState(teamDirPath);
|
|
473
|
+
const triggerCooldownState = await readTriggerCooldownState(teamDirPath);
|
|
349
474
|
const issueCooldownByIssue = issueCooldownState.by_issue || {};
|
|
475
|
+
const triggerCooldownByKey = triggerCooldownState.by_trigger || {};
|
|
350
476
|
const nowMs = Date.now();
|
|
351
477
|
|
|
352
478
|
let mutated = false;
|
|
@@ -358,6 +484,34 @@ export async function drainPendingTeamDispatch({
|
|
|
358
484
|
continue;
|
|
359
485
|
}
|
|
360
486
|
|
|
487
|
+
if (request.to_worker === 'leader-fixed' && !safeString(config?.leader_pane_id).trim()) {
|
|
488
|
+
const nowIso = new Date().toISOString();
|
|
489
|
+
request.updated_at = nowIso;
|
|
490
|
+
request.last_reason = LEADER_PANE_MISSING_DEFERRED_REASON;
|
|
491
|
+
request.status = 'pending';
|
|
492
|
+
skipped += 1;
|
|
493
|
+
mutated = true;
|
|
494
|
+
await appendDispatchLog(logsDir, {
|
|
495
|
+
type: 'dispatch_deferred',
|
|
496
|
+
team: teamName,
|
|
497
|
+
request_id: request.request_id,
|
|
498
|
+
worker: request.to_worker,
|
|
499
|
+
to_worker: request.to_worker,
|
|
500
|
+
message_id: request.message_id || null,
|
|
501
|
+
reason: LEADER_PANE_MISSING_DEFERRED_REASON,
|
|
502
|
+
status: 'pending',
|
|
503
|
+
tmux_injection_attempted: false,
|
|
504
|
+
});
|
|
505
|
+
await appendLeaderNotificationDeferredEvent({
|
|
506
|
+
stateDir,
|
|
507
|
+
teamName,
|
|
508
|
+
request,
|
|
509
|
+
reason: LEADER_PANE_MISSING_DEFERRED_REASON,
|
|
510
|
+
nowIso,
|
|
511
|
+
});
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
361
515
|
const issueKey = extractIssueKey(request.trigger_message);
|
|
362
516
|
if (issueCooldownMs > 0 && issueKey) {
|
|
363
517
|
const lastInjectedMs = Number(issueCooldownByIssue[issueKey]);
|
|
@@ -367,11 +521,29 @@ export async function drainPendingTeamDispatch({
|
|
|
367
521
|
}
|
|
368
522
|
}
|
|
369
523
|
|
|
524
|
+
const triggerKey = normalizeTriggerKey(request.trigger_message);
|
|
525
|
+
if (triggerCooldownMs > 0 && triggerKey) {
|
|
526
|
+
const parsed = parseTriggerCooldownEntry(triggerCooldownByKey[triggerKey]);
|
|
527
|
+
const withinCooldown = Number.isFinite(parsed.at) && parsed.at > 0 && nowMs - parsed.at < triggerCooldownMs;
|
|
528
|
+
const sameRequestRetry = parsed.lastRequestId !== '' && parsed.lastRequestId === safeString(request.request_id).trim();
|
|
529
|
+
if (withinCooldown && !sameRequestRetry) {
|
|
530
|
+
skipped += 1;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
370
535
|
const result = await injector(request, config, resolve(cwd));
|
|
371
536
|
if (issueKey && issueCooldownMs > 0) {
|
|
372
537
|
issueCooldownByIssue[issueKey] = Date.now();
|
|
373
538
|
mutated = true;
|
|
374
539
|
}
|
|
540
|
+
if (triggerKey && triggerCooldownMs > 0) {
|
|
541
|
+
triggerCooldownByKey[triggerKey] = {
|
|
542
|
+
at: Date.now(),
|
|
543
|
+
last_request_id: safeString(request.request_id).trim(),
|
|
544
|
+
};
|
|
545
|
+
mutated = true;
|
|
546
|
+
}
|
|
375
547
|
const nowIso = new Date().toISOString();
|
|
376
548
|
request.attempt_count = Number.isFinite(request.attempt_count) ? Math.max(0, request.attempt_count + 1) : 1;
|
|
377
549
|
request.updated_at = nowIso;
|
|
@@ -449,6 +621,8 @@ export async function drainPendingTeamDispatch({
|
|
|
449
621
|
if (mutated) {
|
|
450
622
|
issueCooldownState.by_issue = issueCooldownByIssue;
|
|
451
623
|
await writeJsonAtomic(issueCooldownStatePath(teamDirPath), issueCooldownState);
|
|
624
|
+
triggerCooldownState.by_trigger = triggerCooldownByKey;
|
|
625
|
+
await writeJsonAtomic(triggerCooldownStatePath(teamDirPath), triggerCooldownState);
|
|
452
626
|
await writeJsonAtomic(requestsPath, requests);
|
|
453
627
|
}
|
|
454
628
|
});
|
|
@@ -10,6 +10,8 @@ import { readJsonIfExists, getScopedStateDirsForCurrentSession } from './state-i
|
|
|
10
10
|
import { runProcess } from './process-runner.js';
|
|
11
11
|
import { logTmuxHookEvent } from './log.js';
|
|
12
12
|
import { DEFAULT_MARKER } from '../tmux-hook-engine.js';
|
|
13
|
+
const LEADER_PANE_MISSING_NO_INJECTION_REASON = 'leader_pane_missing_no_injection';
|
|
14
|
+
const LEADER_NOTIFICATION_DEFERRED_TYPE = 'leader_notification_deferred';
|
|
13
15
|
|
|
14
16
|
export function resolveLeaderNudgeIntervalMs() {
|
|
15
17
|
const raw = safeString(process.env.OMX_TEAM_LEADER_NUDGE_MS || '');
|
|
@@ -109,6 +111,26 @@ export async function emitTeamNudgeEvent(cwd, teamName, reason, nowIso) {
|
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
async function emitLeaderNudgeDeferredEvent(cwd, teamName, reason, nowIso) {
|
|
115
|
+
const eventsDir = join(cwd, '.omx', 'state', 'team', teamName, 'events');
|
|
116
|
+
const eventsPath = join(eventsDir, 'events.ndjson');
|
|
117
|
+
try {
|
|
118
|
+
await mkdir(eventsDir, { recursive: true });
|
|
119
|
+
const event = {
|
|
120
|
+
event_id: `leader-deferred-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
|
121
|
+
team: teamName,
|
|
122
|
+
type: LEADER_NOTIFICATION_DEFERRED_TYPE,
|
|
123
|
+
worker: 'leader-fixed',
|
|
124
|
+
to_worker: 'leader-fixed',
|
|
125
|
+
reason,
|
|
126
|
+
created_at: nowIso,
|
|
127
|
+
};
|
|
128
|
+
await appendFile(eventsPath, JSON.stringify(event) + '\n');
|
|
129
|
+
} catch {
|
|
130
|
+
// Best effort
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputedLeaderStale }) {
|
|
113
135
|
const intervalMs = resolveLeaderNudgeIntervalMs();
|
|
114
136
|
const idleCooldownMs = resolveLeaderAllIdleNudgeCooldownMs();
|
|
@@ -163,8 +185,8 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
|
|
|
163
185
|
} catch {
|
|
164
186
|
// ignore
|
|
165
187
|
}
|
|
166
|
-
|
|
167
|
-
|
|
188
|
+
if (!tmuxSession && !leaderPaneId) continue;
|
|
189
|
+
const tmuxTarget = leaderPaneId;
|
|
168
190
|
|
|
169
191
|
const paneStatus = tmuxSession
|
|
170
192
|
? await checkWorkerPanesAlive(tmuxSession)
|
|
@@ -234,6 +256,24 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
|
|
|
234
256
|
const capped = text.length > 180 ? `${text.slice(0, 177)}...` : text;
|
|
235
257
|
const markedText = `${capped} ${DEFAULT_MARKER}`;
|
|
236
258
|
|
|
259
|
+
if (!tmuxTarget) {
|
|
260
|
+
await emitLeaderNudgeDeferredEvent(cwd, teamName, LEADER_PANE_MISSING_NO_INJECTION_REASON, nowIso);
|
|
261
|
+
try {
|
|
262
|
+
await logTmuxHookEvent(logsDir, {
|
|
263
|
+
timestamp: nowIso,
|
|
264
|
+
type: LEADER_NOTIFICATION_DEFERRED_TYPE,
|
|
265
|
+
team: teamName,
|
|
266
|
+
worker: 'leader-fixed',
|
|
267
|
+
to_worker: 'leader-fixed',
|
|
268
|
+
reason: LEADER_PANE_MISSING_NO_INJECTION_REASON,
|
|
269
|
+
leader_pane_id: null,
|
|
270
|
+
tmux_session: tmuxSession || null,
|
|
271
|
+
tmux_injection_attempted: false,
|
|
272
|
+
});
|
|
273
|
+
} catch { /* ignore */ }
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
237
277
|
try {
|
|
238
278
|
await runProcess('tmux', ['send-keys', '-t', tmuxTarget, '-l', markedText], 3000);
|
|
239
279
|
await new Promise(r => setTimeout(r, 100));
|
|
@@ -194,6 +194,43 @@ export async function readTeamWorkersForIdleCheck(stateDir, teamName) {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
async function emitLeaderPaneMissingDeferred({
|
|
198
|
+
stateDir,
|
|
199
|
+
logsDir,
|
|
200
|
+
teamName,
|
|
201
|
+
workerName,
|
|
202
|
+
tmuxSession,
|
|
203
|
+
leaderPaneId,
|
|
204
|
+
reason = 'leader_pane_missing_no_injection',
|
|
205
|
+
}) {
|
|
206
|
+
const nowIso = new Date().toISOString();
|
|
207
|
+
await logTmuxHookEvent(logsDir, {
|
|
208
|
+
timestamp: nowIso,
|
|
209
|
+
type: 'leader_notification_deferred',
|
|
210
|
+
team: teamName,
|
|
211
|
+
worker: workerName,
|
|
212
|
+
to_worker: 'leader-fixed',
|
|
213
|
+
reason,
|
|
214
|
+
leader_pane_id: leaderPaneId || null,
|
|
215
|
+
tmux_session: tmuxSession || null,
|
|
216
|
+
tmux_injection_attempted: false,
|
|
217
|
+
}).catch(() => {});
|
|
218
|
+
|
|
219
|
+
const eventsDir = join(stateDir, 'team', teamName, 'events');
|
|
220
|
+
const eventsPath = join(eventsDir, 'events.ndjson');
|
|
221
|
+
await mkdir(eventsDir, { recursive: true }).catch(() => {});
|
|
222
|
+
const event = {
|
|
223
|
+
event_id: `leader-deferred-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
|
224
|
+
team: teamName,
|
|
225
|
+
type: 'leader_notification_deferred',
|
|
226
|
+
worker: workerName,
|
|
227
|
+
to_worker: 'leader-fixed',
|
|
228
|
+
reason,
|
|
229
|
+
created_at: nowIso,
|
|
230
|
+
};
|
|
231
|
+
await appendFile(eventsPath, JSON.stringify(event) + '\n').catch(() => {});
|
|
232
|
+
}
|
|
233
|
+
|
|
197
234
|
export async function updateWorkerHeartbeat(stateDir, teamName, workerName) {
|
|
198
235
|
const heartbeatPath = join(stateDir, 'team', teamName, 'workers', workerName, 'heartbeat.json');
|
|
199
236
|
let turnCount = 0;
|
|
@@ -228,8 +265,6 @@ export async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir,
|
|
|
228
265
|
const teamInfo = await readTeamWorkersForIdleCheck(stateDir, teamName);
|
|
229
266
|
if (!teamInfo) return;
|
|
230
267
|
const { workers, tmuxSession, leaderPaneId } = teamInfo;
|
|
231
|
-
const tmuxTarget = leaderPaneId || tmuxSession;
|
|
232
|
-
if (!tmuxTarget) return;
|
|
233
268
|
|
|
234
269
|
// Check cooldown to prevent notification spam
|
|
235
270
|
const idleStatePath = join(stateDir, 'team', teamName, 'all-workers-idle.json');
|
|
@@ -252,8 +287,21 @@ export async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir,
|
|
|
252
287
|
);
|
|
253
288
|
if (!allIdle) return;
|
|
254
289
|
|
|
290
|
+
if (!leaderPaneId) {
|
|
291
|
+
await emitLeaderPaneMissingDeferred({
|
|
292
|
+
stateDir,
|
|
293
|
+
logsDir,
|
|
294
|
+
teamName,
|
|
295
|
+
workerName,
|
|
296
|
+
tmuxSession,
|
|
297
|
+
leaderPaneId,
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
255
302
|
const N = workers.length;
|
|
256
303
|
const message = `[OMX] All ${N} worker${N === 1 ? '' : 's'} idle. Ready for next instructions. ${DEFAULT_MARKER}`;
|
|
304
|
+
const tmuxTarget = leaderPaneId;
|
|
257
305
|
|
|
258
306
|
try {
|
|
259
307
|
await runProcess('tmux', ['send-keys', '-t', tmuxTarget, '-l', message], 3000);
|
|
@@ -382,8 +430,19 @@ export async function maybeNotifyLeaderWorkerIdle({ cwd, stateDir, logsDir, pars
|
|
|
382
430
|
const teamInfo = await readTeamWorkersForIdleCheck(stateDir, teamName);
|
|
383
431
|
if (!teamInfo) return;
|
|
384
432
|
const { tmuxSession, leaderPaneId } = teamInfo;
|
|
385
|
-
|
|
386
|
-
if (!
|
|
433
|
+
|
|
434
|
+
if (!leaderPaneId) {
|
|
435
|
+
await emitLeaderPaneMissingDeferred({
|
|
436
|
+
stateDir,
|
|
437
|
+
logsDir,
|
|
438
|
+
teamName,
|
|
439
|
+
workerName,
|
|
440
|
+
tmuxSession,
|
|
441
|
+
leaderPaneId,
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const tmuxTarget = leaderPaneId;
|
|
387
446
|
|
|
388
447
|
// Build notification message with context
|
|
389
448
|
const parts = [`[OMX] ${workerName} idle`];
|