oh-my-codex 0.3.9 → 0.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 +25 -0
- package/dist/cli/__tests__/doctor-team.test.js +58 -0
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +9 -3
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/lifecycle-notifications.test.d.ts +2 -0
- package/dist/cli/__tests__/lifecycle-notifications.test.d.ts.map +1 -0
- package/dist/cli/__tests__/lifecycle-notifications.test.js +48 -0
- package/dist/cli/__tests__/lifecycle-notifications.test.js.map +1 -0
- package/dist/cli/doctor.js +28 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/hooks.d.ts +4 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +201 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +181 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/config/__tests__/models.test.d.ts +2 -0
- package/dist/config/__tests__/models.test.d.ts.map +1 -0
- package/dist/config/__tests__/models.test.js +69 -0
- package/dist/config/__tests__/models.test.js.map +1 -0
- package/dist/config/models.d.ts +24 -0
- package/dist/config/models.d.ts.map +1 -0
- package/dist/config/models.js +53 -0
- package/dist/config/models.js.map +1 -0
- package/dist/hooks/__tests__/notify-hook-linked-sync.test.js +6 -0
- package/dist/hooks/__tests__/notify-hook-linked-sync.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +6 -0
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +224 -36
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +4 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js +1 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.d.ts +2 -0
- package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.d.ts.map +1 -0
- package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.js +153 -0
- package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.js.map +1 -0
- package/dist/hooks/extensibility/dispatcher.d.ts +4 -0
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -0
- package/dist/hooks/extensibility/dispatcher.js +223 -0
- package/dist/hooks/extensibility/dispatcher.js.map +1 -0
- package/dist/hooks/extensibility/events.d.ts +18 -0
- package/dist/hooks/extensibility/events.d.ts.map +1 -0
- package/dist/hooks/extensibility/events.js +53 -0
- package/dist/hooks/extensibility/events.js.map +1 -0
- package/dist/hooks/extensibility/index.d.ts +6 -0
- package/dist/hooks/extensibility/index.d.ts.map +1 -0
- package/dist/hooks/extensibility/index.js +6 -0
- package/dist/hooks/extensibility/index.js.map +1 -0
- package/dist/hooks/extensibility/loader.d.ts +14 -0
- package/dist/hooks/extensibility/loader.d.ts.map +1 -0
- package/dist/hooks/extensibility/loader.js +102 -0
- package/dist/hooks/extensibility/loader.js.map +1 -0
- package/dist/hooks/extensibility/logging.d.ts +4 -0
- package/dist/hooks/extensibility/logging.d.ts.map +1 -0
- package/dist/hooks/extensibility/logging.js +16 -0
- package/dist/hooks/extensibility/logging.js.map +1 -0
- package/dist/hooks/extensibility/plugin-runner.d.ts +2 -0
- package/dist/hooks/extensibility/plugin-runner.d.ts.map +1 -0
- package/dist/hooks/extensibility/plugin-runner.js +69 -0
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -0
- package/dist/hooks/extensibility/runtime.d.ts +3 -0
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -0
- package/dist/hooks/extensibility/runtime.js +29 -0
- package/dist/hooks/extensibility/runtime.js.map +1 -0
- package/dist/hooks/extensibility/sdk.d.ts +11 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -0
- package/dist/hooks/extensibility/sdk.js +240 -0
- package/dist/hooks/extensibility/sdk.js.map +1 -0
- package/dist/hooks/extensibility/types.d.ts +122 -0
- package/dist/hooks/extensibility/types.d.ts.map +1 -0
- package/dist/hooks/extensibility/types.js +2 -0
- package/dist/hooks/extensibility/types.js.map +1 -0
- package/dist/mcp/__tests__/state-paths.test.js +21 -1
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server-team-tools.test.js +53 -1
- package/dist/mcp/__tests__/state-server-team-tools.test.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +1 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +34 -1
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +46 -11
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/notifications/__tests__/config.test.d.ts +2 -0
- package/dist/notifications/__tests__/config.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/config.test.js +186 -0
- package/dist/notifications/__tests__/config.test.js.map +1 -0
- package/dist/notifications/__tests__/dispatcher.test.d.ts +2 -0
- package/dist/notifications/__tests__/dispatcher.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/dispatcher.test.js +202 -0
- package/dist/notifications/__tests__/dispatcher.test.js.map +1 -0
- package/dist/notifications/__tests__/formatter.test.d.ts +2 -0
- package/dist/notifications/__tests__/formatter.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/formatter.test.js +103 -0
- package/dist/notifications/__tests__/formatter.test.js.map +1 -0
- package/dist/notifications/__tests__/notifier.test.d.ts +2 -0
- package/dist/notifications/__tests__/notifier.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/notifier.test.js +104 -0
- package/dist/notifications/__tests__/notifier.test.js.map +1 -0
- package/dist/notifications/__tests__/profiles.test.d.ts +2 -0
- package/dist/notifications/__tests__/profiles.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/profiles.test.js +404 -0
- package/dist/notifications/__tests__/profiles.test.js.map +1 -0
- package/dist/notifications/__tests__/reply-listener.test.d.ts +2 -0
- package/dist/notifications/__tests__/reply-listener.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/reply-listener.test.js +58 -0
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -0
- package/dist/notifications/__tests__/session-registry.test.d.ts +2 -0
- package/dist/notifications/__tests__/session-registry.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/session-registry.test.js +147 -0
- package/dist/notifications/__tests__/session-registry.test.js.map +1 -0
- package/dist/notifications/__tests__/tmux-detector.test.d.ts +2 -0
- package/dist/notifications/__tests__/tmux-detector.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/tmux-detector.test.js +77 -0
- package/dist/notifications/__tests__/tmux-detector.test.js.map +1 -0
- package/dist/notifications/__tests__/tmux.test.d.ts +2 -0
- package/dist/notifications/__tests__/tmux.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/tmux.test.js +90 -0
- package/dist/notifications/__tests__/tmux.test.js.map +1 -0
- package/dist/notifications/config.d.ts +44 -0
- package/dist/notifications/config.d.ts.map +1 -0
- package/dist/notifications/config.js +407 -0
- package/dist/notifications/config.js.map +1 -0
- package/dist/notifications/dispatcher.d.ts +15 -0
- package/dist/notifications/dispatcher.d.ts.map +1 -0
- package/dist/notifications/dispatcher.js +410 -0
- package/dist/notifications/dispatcher.js.map +1 -0
- package/dist/notifications/formatter.d.ts +14 -0
- package/dist/notifications/formatter.d.ts.map +1 -0
- package/dist/notifications/formatter.js +134 -0
- package/dist/notifications/formatter.js.map +1 -0
- package/dist/notifications/index.d.ts +32 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +93 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/reply-listener.d.ts +47 -0
- package/dist/notifications/reply-listener.d.ts.map +1 -0
- package/dist/notifications/reply-listener.js +656 -0
- package/dist/notifications/reply-listener.js.map +1 -0
- package/dist/notifications/session-registry.d.ts +26 -0
- package/dist/notifications/session-registry.d.ts.map +1 -0
- package/dist/notifications/session-registry.js +275 -0
- package/dist/notifications/session-registry.js.map +1 -0
- package/dist/notifications/tmux-detector.d.ts +17 -0
- package/dist/notifications/tmux-detector.d.ts.map +1 -0
- package/dist/notifications/tmux-detector.js +82 -0
- package/dist/notifications/tmux-detector.js.map +1 -0
- package/dist/notifications/tmux.d.ts +28 -0
- package/dist/notifications/tmux.d.ts.map +1 -0
- package/dist/notifications/tmux.js +210 -0
- package/dist/notifications/tmux.js.map +1 -0
- package/dist/notifications/types.d.ts +181 -0
- package/dist/notifications/types.d.ts.map +1 -0
- package/dist/notifications/types.js +9 -0
- package/dist/notifications/types.js.map +1 -0
- package/dist/team/__tests__/runtime.test.js +54 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +30 -0
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +2 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/runtime.d.ts +2 -2
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +19 -12
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state.d.ts +1 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +5 -0
- package/dist/team/state.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +59 -15
- 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 +4 -0
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/package.json +1 -1
- package/scripts/hook-derived-watcher.js +335 -0
- package/scripts/notify-hook.js +168 -7
- package/scripts/tmux-hook-engine.js +3 -2
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { appendFile, mkdir, readFile, readdir, stat, writeFile } from 'fs/promises';
|
|
5
|
+
import { dirname, join, resolve } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
|
|
8
|
+
function argValue(name, fallback = '') {
|
|
9
|
+
const idx = process.argv.indexOf(name);
|
|
10
|
+
if (idx < 0 || idx + 1 >= process.argv.length) return fallback;
|
|
11
|
+
return process.argv[idx + 1];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function asNumber(value, fallback) {
|
|
15
|
+
const parsed = Number(value);
|
|
16
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const cwd = resolve(argValue('--cwd', process.cwd()));
|
|
20
|
+
const runOnce = process.argv.includes('--once');
|
|
21
|
+
const pollMs = Math.max(250, asNumber(argValue('--poll-ms', process.env.OMX_HOOK_DERIVED_POLL_MS || '800'), 800));
|
|
22
|
+
const maxFileAgeMs = Math.max(10_000, asNumber(argValue('--file-age-ms', process.env.OMX_HOOK_DERIVED_FILE_AGE_MS || '90000'), 90000));
|
|
23
|
+
|
|
24
|
+
const omxDir = join(cwd, '.omx');
|
|
25
|
+
const logsDir = join(omxDir, 'logs');
|
|
26
|
+
const stateDir = join(omxDir, 'state');
|
|
27
|
+
const watcherStatePath = join(stateDir, 'hook-derived-watcher-state.json');
|
|
28
|
+
const logPath = join(logsDir, `hook-derived-watcher-${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
29
|
+
|
|
30
|
+
const fileState = new Map();
|
|
31
|
+
let stopping = false;
|
|
32
|
+
let flushedOnShutdown = false;
|
|
33
|
+
|
|
34
|
+
function safeString(value) {
|
|
35
|
+
return typeof value === 'string' ? value : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function derivedLog(entry) {
|
|
39
|
+
return appendFile(logPath, `${JSON.stringify({ timestamp: new Date().toISOString(), ...entry })}\n`).catch(() => {});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseJsonLine(line) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(line);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sessionDirs() {
|
|
51
|
+
const now = new Date();
|
|
52
|
+
const today = join(
|
|
53
|
+
homedir(),
|
|
54
|
+
'.codex',
|
|
55
|
+
'sessions',
|
|
56
|
+
String(now.getUTCFullYear()),
|
|
57
|
+
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
58
|
+
String(now.getUTCDate()).padStart(2, '0')
|
|
59
|
+
);
|
|
60
|
+
const yesterdayDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
61
|
+
const yesterday = join(
|
|
62
|
+
homedir(),
|
|
63
|
+
'.codex',
|
|
64
|
+
'sessions',
|
|
65
|
+
String(yesterdayDate.getUTCFullYear()),
|
|
66
|
+
String(yesterdayDate.getUTCMonth() + 1).padStart(2, '0'),
|
|
67
|
+
String(yesterdayDate.getUTCDate()).padStart(2, '0')
|
|
68
|
+
);
|
|
69
|
+
return Array.from(new Set([today, yesterday]));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readFirstLine(path) {
|
|
73
|
+
const content = await readFile(path, 'utf-8');
|
|
74
|
+
const idx = content.indexOf('\n');
|
|
75
|
+
return idx >= 0 ? content.slice(0, idx) : content;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function shouldTrackSessionMeta(line) {
|
|
79
|
+
const parsed = parseJsonLine(line);
|
|
80
|
+
if (!parsed || parsed.type !== 'session_meta' || !parsed.payload) return null;
|
|
81
|
+
const payload = parsed.payload;
|
|
82
|
+
if (safeString(payload.cwd) !== cwd) return null;
|
|
83
|
+
const threadId = safeString(payload.id);
|
|
84
|
+
if (!threadId) return null;
|
|
85
|
+
return {
|
|
86
|
+
threadId,
|
|
87
|
+
sessionId: threadId,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function discoverRolloutFiles() {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const discovered = [];
|
|
94
|
+
for (const dir of sessionDirs()) {
|
|
95
|
+
if (!existsSync(dir)) continue;
|
|
96
|
+
const names = await readdir(dir).catch(() => []);
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
if (!name.startsWith('rollout-') || !name.endsWith('.jsonl')) continue;
|
|
99
|
+
const path = join(dir, name);
|
|
100
|
+
const st = await stat(path).catch(() => null);
|
|
101
|
+
if (!st) continue;
|
|
102
|
+
if (now - st.mtimeMs > maxFileAgeMs) continue;
|
|
103
|
+
discovered.push(path);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
discovered.sort();
|
|
107
|
+
return discovered;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function inferDerivedEvent(parsed, meta) {
|
|
111
|
+
if (!parsed || parsed.type !== 'event_msg' || !parsed.payload) return null;
|
|
112
|
+
|
|
113
|
+
const payload = parsed.payload;
|
|
114
|
+
const payloadType = safeString(payload.type).toLowerCase();
|
|
115
|
+
const timestamp = safeString(parsed.timestamp) || new Date().toISOString();
|
|
116
|
+
const turnId = safeString(payload.turn_id || parsed.turn_id || parsed.id);
|
|
117
|
+
|
|
118
|
+
const base = {
|
|
119
|
+
schema_version: '1',
|
|
120
|
+
timestamp,
|
|
121
|
+
source: 'derived',
|
|
122
|
+
context: {
|
|
123
|
+
parser_reason: '',
|
|
124
|
+
payload_type: payloadType || 'unknown',
|
|
125
|
+
},
|
|
126
|
+
session_id: meta.sessionId,
|
|
127
|
+
thread_id: meta.threadId,
|
|
128
|
+
turn_id: turnId || undefined,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (['tool_call_start', 'tool_use_start', 'tool_start', 'tool_invocation_start'].includes(payloadType)) {
|
|
132
|
+
return {
|
|
133
|
+
...base,
|
|
134
|
+
event: 'pre-tool-use',
|
|
135
|
+
confidence: 0.8,
|
|
136
|
+
parser_reason: `payload_type:${payloadType}`,
|
|
137
|
+
context: {
|
|
138
|
+
...base.context,
|
|
139
|
+
parser_reason: `payload_type:${payloadType}`,
|
|
140
|
+
tool_name: safeString(payload.tool_name || payload.tool || payload.name),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (['tool_call_end', 'tool_use_end', 'tool_end', 'tool_invocation_end'].includes(payloadType)) {
|
|
146
|
+
return {
|
|
147
|
+
...base,
|
|
148
|
+
event: 'post-tool-use',
|
|
149
|
+
confidence: 0.8,
|
|
150
|
+
parser_reason: `payload_type:${payloadType}`,
|
|
151
|
+
context: {
|
|
152
|
+
...base.context,
|
|
153
|
+
parser_reason: `payload_type:${payloadType}`,
|
|
154
|
+
tool_name: safeString(payload.tool_name || payload.tool || payload.name),
|
|
155
|
+
tool_ok: payload.ok === true,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (payloadType === 'assistant_message') {
|
|
161
|
+
const message = safeString(payload.text || payload.message || payload.content);
|
|
162
|
+
const looksLikeQuestion = /\?|\b(can you|could you|please provide|need input|what should)/i.test(message);
|
|
163
|
+
if (looksLikeQuestion) {
|
|
164
|
+
return {
|
|
165
|
+
...base,
|
|
166
|
+
event: 'needs-input',
|
|
167
|
+
confidence: 0.55,
|
|
168
|
+
parser_reason: 'assistant_message_heuristic_question',
|
|
169
|
+
context: {
|
|
170
|
+
...base.context,
|
|
171
|
+
parser_reason: 'assistant_message_heuristic_question',
|
|
172
|
+
preview: message.slice(0, 200),
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function dispatchDerivedEvent(event) {
|
|
182
|
+
try {
|
|
183
|
+
const { dispatchHookEvent } = await import('../dist/hooks/extensibility/dispatcher.js');
|
|
184
|
+
await dispatchHookEvent(event, {
|
|
185
|
+
cwd,
|
|
186
|
+
allowTeamWorkerSideEffects: false,
|
|
187
|
+
});
|
|
188
|
+
await derivedLog({
|
|
189
|
+
type: 'derived_event_dispatch',
|
|
190
|
+
event: event.event,
|
|
191
|
+
source: event.source,
|
|
192
|
+
confidence: event.confidence,
|
|
193
|
+
thread_id: event.thread_id,
|
|
194
|
+
turn_id: event.turn_id,
|
|
195
|
+
parser_reason: event.parser_reason,
|
|
196
|
+
ok: true,
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
await derivedLog({
|
|
200
|
+
type: 'derived_event_dispatch',
|
|
201
|
+
event: event.event,
|
|
202
|
+
source: event.source,
|
|
203
|
+
thread_id: event.thread_id,
|
|
204
|
+
turn_id: event.turn_id,
|
|
205
|
+
parser_reason: event.parser_reason,
|
|
206
|
+
ok: false,
|
|
207
|
+
error: err instanceof Error ? err.message : 'dispatch_failed',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function ensureTrackedFiles() {
|
|
213
|
+
const files = await discoverRolloutFiles();
|
|
214
|
+
for (const path of files) {
|
|
215
|
+
if (fileState.has(path)) continue;
|
|
216
|
+
const firstLine = await readFirstLine(path).catch(() => '');
|
|
217
|
+
const meta = shouldTrackSessionMeta(firstLine);
|
|
218
|
+
if (!meta) continue;
|
|
219
|
+
const size = (await stat(path).catch(() => ({ size: 0 }))).size || 0;
|
|
220
|
+
const offset = runOnce ? 0 : size;
|
|
221
|
+
fileState.set(path, {
|
|
222
|
+
...meta,
|
|
223
|
+
offset,
|
|
224
|
+
partial: '',
|
|
225
|
+
dispatched: 0,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function processLine(meta, line) {
|
|
231
|
+
const parsed = parseJsonLine(line);
|
|
232
|
+
const derived = inferDerivedEvent(parsed, meta);
|
|
233
|
+
if (!derived) return;
|
|
234
|
+
await dispatchDerivedEvent(derived);
|
|
235
|
+
meta.dispatched += 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function pollFiles() {
|
|
239
|
+
for (const [path, meta] of fileState.entries()) {
|
|
240
|
+
const currentSize = (await stat(path).catch(() => ({ size: 0 }))).size || 0;
|
|
241
|
+
if (currentSize <= meta.offset) continue;
|
|
242
|
+
|
|
243
|
+
const content = await readFile(path, 'utf-8').catch(() => '');
|
|
244
|
+
if (!content) continue;
|
|
245
|
+
|
|
246
|
+
const delta = content.slice(meta.offset);
|
|
247
|
+
meta.offset = currentSize;
|
|
248
|
+
const merged = meta.partial + delta;
|
|
249
|
+
const lines = merged.split('\n');
|
|
250
|
+
meta.partial = lines.pop() || '';
|
|
251
|
+
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
if (!line.trim()) continue;
|
|
254
|
+
await processLine(meta, line);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function writeState() {
|
|
260
|
+
await mkdir(stateDir, { recursive: true }).catch(() => {});
|
|
261
|
+
const tracked = Array.from(fileState.values()).reduce((sum, item) => sum + item.dispatched, 0);
|
|
262
|
+
const state = {
|
|
263
|
+
pid: process.pid,
|
|
264
|
+
started_at: new Date().toISOString(),
|
|
265
|
+
cwd,
|
|
266
|
+
poll_ms: pollMs,
|
|
267
|
+
max_file_age_ms: maxFileAgeMs,
|
|
268
|
+
tracked_files: fileState.size,
|
|
269
|
+
dispatched_events: tracked,
|
|
270
|
+
};
|
|
271
|
+
await writeFile(watcherStatePath, JSON.stringify(state, null, 2)).catch(() => {});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function flushOnce(reason) {
|
|
275
|
+
if (flushedOnShutdown) return;
|
|
276
|
+
flushedOnShutdown = true;
|
|
277
|
+
await ensureTrackedFiles();
|
|
278
|
+
await pollFiles();
|
|
279
|
+
await writeState();
|
|
280
|
+
await derivedLog({ type: 'watcher_flush', reason });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function tick() {
|
|
284
|
+
if (stopping) return;
|
|
285
|
+
await ensureTrackedFiles();
|
|
286
|
+
await pollFiles();
|
|
287
|
+
await writeState();
|
|
288
|
+
setTimeout(tick, pollMs);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function shutdown(signal) {
|
|
292
|
+
stopping = true;
|
|
293
|
+
flushOnce(`signal:${signal}`)
|
|
294
|
+
.finally(() => derivedLog({ type: 'watcher_stop', signal }))
|
|
295
|
+
.finally(() => process.exit(0));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function main() {
|
|
299
|
+
if (process.env.OMX_HOOK_DERIVED_SIGNALS !== '1') {
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await mkdir(dirname(logPath), { recursive: true }).catch(() => {});
|
|
304
|
+
await mkdir(stateDir, { recursive: true }).catch(() => {});
|
|
305
|
+
|
|
306
|
+
await derivedLog({
|
|
307
|
+
type: 'watcher_start',
|
|
308
|
+
cwd,
|
|
309
|
+
poll_ms: pollMs,
|
|
310
|
+
max_file_age_ms: maxFileAgeMs,
|
|
311
|
+
once: runOnce,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
315
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
316
|
+
process.on('SIGHUP', () => shutdown('SIGHUP'));
|
|
317
|
+
|
|
318
|
+
if (runOnce) {
|
|
319
|
+
await flushOnce('once');
|
|
320
|
+
await derivedLog({ type: 'watcher_once_complete' });
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await tick();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
main().catch(async (err) => {
|
|
328
|
+
await mkdir(dirname(logPath), { recursive: true }).catch(() => {});
|
|
329
|
+
await derivedLog({
|
|
330
|
+
type: 'watcher_error',
|
|
331
|
+
reason: 'fatal',
|
|
332
|
+
error: err instanceof Error ? err.message : 'unknown_error',
|
|
333
|
+
});
|
|
334
|
+
process.exit(1);
|
|
335
|
+
});
|
package/scripts/notify-hook.js
CHANGED
|
@@ -437,7 +437,63 @@ function resolveLeaderNudgeIntervalMs() {
|
|
|
437
437
|
return 120_000;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
|
|
440
|
+
function resolveLeaderStalenessThresholdMs() {
|
|
441
|
+
const raw = safeString(process.env.OMX_TEAM_LEADER_STALE_MS || '');
|
|
442
|
+
const parsed = asNumber(raw);
|
|
443
|
+
// Default: 3 minutes. Guard against unreasonable values.
|
|
444
|
+
if (parsed !== null && parsed >= 10_000 && parsed <= 30 * 60_000) return parsed;
|
|
445
|
+
return 180_000;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function checkWorkerPanesAlive(tmuxTarget) {
|
|
449
|
+
// Check if the team tmux session has worker panes running.
|
|
450
|
+
// tmuxTarget is either "omx-team-foo" or "session:window".
|
|
451
|
+
const sessionName = tmuxTarget.split(':')[0];
|
|
452
|
+
try {
|
|
453
|
+
const result = await runProcess('tmux', ['list-panes', '-t', sessionName, '-F', '#{pane_id} #{pane_pid}'], 2000);
|
|
454
|
+
const lines = (result.stdout || '')
|
|
455
|
+
.split('\n')
|
|
456
|
+
.map(l => l.trim())
|
|
457
|
+
.filter(Boolean);
|
|
458
|
+
return { alive: lines.length > 0, paneCount: lines.length };
|
|
459
|
+
} catch {
|
|
460
|
+
return { alive: false, paneCount: 0 };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function isLeaderStale(stateDir, thresholdMs, nowMs) {
|
|
465
|
+
// Check HUD state (updated by the notify hook on each leader turn) for staleness.
|
|
466
|
+
const hudStatePath = join(stateDir, 'hud-state.json');
|
|
467
|
+
const hudState = await readJsonIfExists(hudStatePath, null);
|
|
468
|
+
if (!hudState || typeof hudState !== 'object') return true;
|
|
469
|
+
const lastTurnAt = safeString(hudState.last_turn_at || '');
|
|
470
|
+
if (!lastTurnAt) return true;
|
|
471
|
+
const lastMs = Date.parse(lastTurnAt);
|
|
472
|
+
if (!Number.isFinite(lastMs)) return true;
|
|
473
|
+
return (nowMs - lastMs) >= thresholdMs;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function emitTeamNudgeEvent(cwd, teamName, reason, nowIso) {
|
|
477
|
+
// Write a team_leader_nudge event to the team's events.ndjson log.
|
|
478
|
+
const eventsDir = join(cwd, '.omx', 'state', 'team', teamName, 'events');
|
|
479
|
+
const eventsPath = join(eventsDir, 'events.ndjson');
|
|
480
|
+
try {
|
|
481
|
+
await mkdir(eventsDir, { recursive: true });
|
|
482
|
+
const event = {
|
|
483
|
+
event_id: `nudge-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
|
484
|
+
team: teamName,
|
|
485
|
+
type: 'team_leader_nudge',
|
|
486
|
+
worker: 'leader-fixed',
|
|
487
|
+
reason,
|
|
488
|
+
created_at: nowIso,
|
|
489
|
+
};
|
|
490
|
+
await appendFile(eventsPath, JSON.stringify(event) + '\n');
|
|
491
|
+
} catch {
|
|
492
|
+
// Best effort
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputedLeaderStale }) {
|
|
441
497
|
const intervalMs = resolveLeaderNudgeIntervalMs();
|
|
442
498
|
const nowMs = Date.now();
|
|
443
499
|
const nowIso = new Date().toISOString();
|
|
@@ -467,6 +523,9 @@ async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir }) {
|
|
|
467
523
|
// Non-critical
|
|
468
524
|
}
|
|
469
525
|
|
|
526
|
+
// Use pre-computed staleness (captured before HUD state was updated this turn)
|
|
527
|
+
const leaderStale = typeof preComputedLeaderStale === 'boolean' ? preComputedLeaderStale : false;
|
|
528
|
+
|
|
470
529
|
for (const teamName of activeTeamNames) {
|
|
471
530
|
// Resolve tmux target (session:window) from manifest/config. Best effort.
|
|
472
531
|
let tmuxTarget = '';
|
|
@@ -483,6 +542,9 @@ async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir }) {
|
|
|
483
542
|
}
|
|
484
543
|
if (!tmuxTarget) continue;
|
|
485
544
|
|
|
545
|
+
// Check if worker panes are still alive in tmux
|
|
546
|
+
const paneStatus = await checkWorkerPanesAlive(tmuxTarget);
|
|
547
|
+
|
|
486
548
|
let mailbox = null;
|
|
487
549
|
try {
|
|
488
550
|
const mailboxPath = join(omxDir, 'state', 'team', teamName, 'mailbox', 'leader-fixed.json');
|
|
@@ -503,17 +565,51 @@ async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir }) {
|
|
|
503
565
|
|
|
504
566
|
const hasNewMessage = newestId && newestId !== prevMsgId;
|
|
505
567
|
const dueByTime = !Number.isFinite(prevAtMs) || (nowMs - prevAtMs >= intervalMs);
|
|
506
|
-
if (!hasNewMessage && !dueByTime) continue;
|
|
507
568
|
|
|
569
|
+
// New condition: worker panes alive + leader stale = always nudge
|
|
570
|
+
const stalePanesNudge = paneStatus.alive && leaderStale;
|
|
571
|
+
|
|
572
|
+
if (!hasNewMessage && !dueByTime && !stalePanesNudge) continue;
|
|
573
|
+
|
|
574
|
+
// Build contextual nudge message
|
|
508
575
|
const msgCount = messages.length;
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
576
|
+
let nudgeReason = '';
|
|
577
|
+
let text = '';
|
|
578
|
+
if (stalePanesNudge && hasNewMessage) {
|
|
579
|
+
nudgeReason = 'stale_leader_with_messages';
|
|
580
|
+
text = `Team ${teamName}: leader stale, ${paneStatus.paneCount} pane(s) active, ${msgCount} msg(s) pending. Run: omx team status ${teamName}`;
|
|
581
|
+
} else if (stalePanesNudge) {
|
|
582
|
+
nudgeReason = 'stale_leader_panes_alive';
|
|
583
|
+
text = `Team ${teamName}: leader stale, ${paneStatus.paneCount} worker pane(s) still active. Run: omx team status ${teamName}`;
|
|
584
|
+
} else if (hasNewMessage) {
|
|
585
|
+
nudgeReason = 'new_mailbox_message';
|
|
586
|
+
text = `Team ${teamName}: ${msgCount} msg(s) for leader. Run: omx team status ${teamName}`;
|
|
587
|
+
} else {
|
|
588
|
+
nudgeReason = 'periodic_check';
|
|
589
|
+
text = `Team ${teamName} active. Run: omx team status ${teamName}`;
|
|
590
|
+
}
|
|
512
591
|
const capped = text.length > 180 ? `${text.slice(0, 177)}...` : text;
|
|
513
592
|
|
|
514
593
|
try {
|
|
515
|
-
await runProcess('tmux', ['
|
|
594
|
+
await runProcess('tmux', ['send-keys', '-t', tmuxTarget, capped, 'C-m', 'C-m'], 1200);
|
|
516
595
|
nudgeState.last_nudged_by_team[teamName] = { at: nowIso, last_message_id: newestId || prevMsgId || '' };
|
|
596
|
+
|
|
597
|
+
// Emit team event for the nudge
|
|
598
|
+
await emitTeamNudgeEvent(cwd, teamName, nudgeReason, nowIso);
|
|
599
|
+
|
|
600
|
+
// Log the nudge
|
|
601
|
+
try {
|
|
602
|
+
await logTmuxHookEvent(logsDir, {
|
|
603
|
+
timestamp: nowIso,
|
|
604
|
+
type: 'team_leader_nudge',
|
|
605
|
+
team: teamName,
|
|
606
|
+
tmux_target: tmuxTarget,
|
|
607
|
+
reason: nudgeReason,
|
|
608
|
+
pane_count: paneStatus.paneCount,
|
|
609
|
+
leader_stale: leaderStale,
|
|
610
|
+
message_count: msgCount,
|
|
611
|
+
});
|
|
612
|
+
} catch { /* ignore */ }
|
|
517
613
|
} catch (err) {
|
|
518
614
|
// Best effort. Log only in debug mode to avoid noise.
|
|
519
615
|
try {
|
|
@@ -522,6 +618,7 @@ async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir }) {
|
|
|
522
618
|
type: 'team_leader_nudge',
|
|
523
619
|
team: teamName,
|
|
524
620
|
tmux_target: tmuxTarget,
|
|
621
|
+
reason: nudgeReason,
|
|
525
622
|
error: safeString(err && err.message ? err.message : err),
|
|
526
623
|
});
|
|
527
624
|
} catch { /* ignore */ }
|
|
@@ -812,6 +909,22 @@ function parseTeamWorkerEnv(rawValue) {
|
|
|
812
909
|
return { teamName: match[1], workerName: match[2] };
|
|
813
910
|
}
|
|
814
911
|
|
|
912
|
+
async function dispatchNativeHookEvent(cwd, eventName, payload, context = {}) {
|
|
913
|
+
try {
|
|
914
|
+
const { buildNativeHookEvent } = await import('../dist/hooks/extensibility/events.js');
|
|
915
|
+
const { dispatchHookEvent } = await import('../dist/hooks/extensibility/dispatcher.js');
|
|
916
|
+
const event = buildNativeHookEvent(eventName, context, {
|
|
917
|
+
session_id: safeString(payload.session_id || payload['session-id'] || ''),
|
|
918
|
+
thread_id: safeString(payload['thread-id'] || payload.thread_id || ''),
|
|
919
|
+
turn_id: safeString(payload['turn-id'] || payload.turn_id || ''),
|
|
920
|
+
mode: safeString(payload.mode || ''),
|
|
921
|
+
});
|
|
922
|
+
await dispatchHookEvent(event, { cwd });
|
|
923
|
+
} catch {
|
|
924
|
+
// Non-fatal: extensibility modules may not be built yet
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
815
928
|
async function main() {
|
|
816
929
|
const rawPayload = process.argv[process.argv.length - 1];
|
|
817
930
|
if (!rawPayload || rawPayload.startsWith('-')) {
|
|
@@ -968,6 +1081,17 @@ async function main() {
|
|
|
968
1081
|
}
|
|
969
1082
|
}
|
|
970
1083
|
|
|
1084
|
+
// 3.5. Pre-compute leader staleness BEFORE updating HUD state (used by nudge in step 6)
|
|
1085
|
+
let preComputedLeaderStale = false;
|
|
1086
|
+
if (!isTeamWorker) {
|
|
1087
|
+
try {
|
|
1088
|
+
const stalenessMs = resolveLeaderStalenessThresholdMs();
|
|
1089
|
+
preComputedLeaderStale = await isLeaderStale(stateDir, stalenessMs, Date.now());
|
|
1090
|
+
} catch {
|
|
1091
|
+
// Non-critical
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
971
1095
|
// 4. Write HUD state summary for `omx hud` (lead session only)
|
|
972
1096
|
if (!isTeamWorker) {
|
|
973
1097
|
const hudStatePath = join(stateDir, 'hud-state.json');
|
|
@@ -1026,11 +1150,48 @@ async function main() {
|
|
|
1026
1150
|
// 6. Team leader nudge (lead session only): remind the leader to check teammate/mailbox state.
|
|
1027
1151
|
if (!isTeamWorker) {
|
|
1028
1152
|
try {
|
|
1029
|
-
await maybeNudgeTeamLeader({ cwd, stateDir, logsDir });
|
|
1153
|
+
await maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputedLeaderStale });
|
|
1030
1154
|
} catch {
|
|
1031
1155
|
// Non-critical
|
|
1032
1156
|
}
|
|
1033
1157
|
}
|
|
1158
|
+
|
|
1159
|
+
// 7. Dispatch native turn-complete hook event (best effort, post-dedupe)
|
|
1160
|
+
await dispatchNativeHookEvent(cwd, 'turn-complete', payload, {
|
|
1161
|
+
source: safeString(payload.source || 'native'),
|
|
1162
|
+
type: safeString(payload.type || 'agent-turn-complete'),
|
|
1163
|
+
input_messages: normalizeInputMessages(payload),
|
|
1164
|
+
output_preview: safeString(payload['last-assistant-message'] || payload.last_assistant_message || '').slice(0, 400),
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
// 8. Dispatch session-idle lifecycle notification (lead session only, best effort)
|
|
1168
|
+
if (!isTeamWorker) {
|
|
1169
|
+
try {
|
|
1170
|
+
const { notifyLifecycle } = await import('../dist/notifications/index.js');
|
|
1171
|
+
const sessionJsonPath = join(stateDir, 'session.json');
|
|
1172
|
+
let notifySessionId = '';
|
|
1173
|
+
try {
|
|
1174
|
+
const sessionData = JSON.parse(await readFile(sessionJsonPath, 'utf-8'));
|
|
1175
|
+
notifySessionId = safeString(sessionData && sessionData.session_id ? sessionData.session_id : '');
|
|
1176
|
+
} catch { /* no session file */ }
|
|
1177
|
+
|
|
1178
|
+
if (notifySessionId) {
|
|
1179
|
+
await notifyLifecycle('session-idle', {
|
|
1180
|
+
sessionId: notifySessionId,
|
|
1181
|
+
projectPath: cwd,
|
|
1182
|
+
});
|
|
1183
|
+
await dispatchNativeHookEvent(cwd, 'session-idle', {
|
|
1184
|
+
...payload,
|
|
1185
|
+
session_id: notifySessionId,
|
|
1186
|
+
}, {
|
|
1187
|
+
project_path: cwd,
|
|
1188
|
+
reason: 'post_turn_idle_notification',
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
} catch {
|
|
1192
|
+
// Non-fatal: notification module may not be built or config may not exist
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1034
1195
|
}
|
|
1035
1196
|
|
|
1036
1197
|
async function readdir(dir) {
|
|
@@ -149,10 +149,11 @@ export function buildSendKeysArgv({ paneTarget, prompt, dryRun }) {
|
|
|
149
149
|
// 1) literal prompt bytes, 2) explicit carriage return.
|
|
150
150
|
return {
|
|
151
151
|
typeArgv: ['send-keys', '-t', paneTarget, '-l', prompt],
|
|
152
|
-
//
|
|
152
|
+
// Codex CLI uses raw input mode where 'Enter' key name is unreliable;
|
|
153
|
+
// send 'C-m' (carriage return) twice for reliable prompt submission.
|
|
153
154
|
submitArgv: [
|
|
154
155
|
['send-keys', '-t', paneTarget, 'C-m'],
|
|
155
|
-
['send-keys', '-t', paneTarget, '
|
|
156
|
+
['send-keys', '-t', paneTarget, 'C-m'],
|
|
156
157
|
],
|
|
157
158
|
};
|
|
158
159
|
}
|