helloagents 3.0.23 → 3.0.25
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +15 -8
- package/README_CN.md +15 -8
- package/bootstrap-lite.md +1 -1
- package/bootstrap.md +1 -1
- package/gemini-extension.json +1 -1
- package/install.ps1 +11 -11
- package/package.json +1 -1
- package/scripts/cli-codex-config.mjs +50 -3
- package/scripts/cli-codex-hooks-state.mjs +264 -0
- package/scripts/cli-codex.mjs +21 -14
- package/scripts/cli-doctor-codex.mjs +26 -3
- package/scripts/cli-host-detect.mjs +3 -3
- package/scripts/cli-messages.mjs +1 -1
- package/scripts/cli-utils.mjs +4 -3
- package/scripts/delivery-gate.mjs +20 -11
- package/scripts/notify-closeout.mjs +22 -2
- package/scripts/notify-route.mjs +22 -15
- package/scripts/notify-sound.mjs +94 -0
- package/scripts/notify-ui.mjs +43 -11
- package/scripts/notify.mjs +241 -66
- package/scripts/project-session-cleanup.mjs +27 -1
- package/scripts/ralph-loop.mjs +76 -81
- package/scripts/runtime-scope.mjs +45 -17
- package/scripts/session-capsule.mjs +1 -0
- package/scripts/turn-state-cli.mjs +24 -2
- package/scripts/turn-stop-gate.mjs +7 -5
- package/skills/commands/help/SKILL.md +1 -1
package/scripts/notify.mjs
CHANGED
|
@@ -7,13 +7,17 @@ import { readFileSync } from 'node:fs';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
beginCodexCloseoutClaim,
|
|
12
|
+
finalizeCodexCloseoutClaim,
|
|
13
|
+
hasCodexQuickNotifyEvidence,
|
|
14
|
+
writeCodexQuickNotifyEvidence,
|
|
15
|
+
} from './notify-closeout.mjs';
|
|
11
16
|
import { resolveNotificationSource } from './notify-source.mjs';
|
|
12
17
|
import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
|
|
13
18
|
import { resolveNotifyHost, shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
|
|
14
|
-
import { runGateScript } from './notify-gates.mjs';
|
|
15
19
|
import { normalizeNotifyPayload } from './notify-payload.mjs';
|
|
16
|
-
import { cleanupProjectSessions } from './project-session-cleanup.mjs';
|
|
20
|
+
import { cleanupProjectSessions, PROJECT_SESSION_CLEANUP_COOLDOWN_MS } from './project-session-cleanup.mjs';
|
|
17
21
|
import { handleRouteCommand, resolveBootstrapFile } from './notify-route.mjs';
|
|
18
22
|
import { readSettings, readStdinJson, output, suppressedOutput, emptySuppress } from './notify-shared.mjs';
|
|
19
23
|
import { clearRouteContext, getApplicableRouteContext, writeRouteContext } from './runtime-context.mjs';
|
|
@@ -38,8 +42,15 @@ const EVENT_NAME = {
|
|
|
38
42
|
PreCompact: IS_GEMINI ? 'BeforeAgent' : 'PreCompact',
|
|
39
43
|
};
|
|
40
44
|
const RALPH_LOOP_ROUTE_COMMANDS = new Set(['verify', 'loop']);
|
|
45
|
+
const CODEX_HOOKS_FILE = join(homedir(), '.codex', 'hooks.json');
|
|
46
|
+
const GATE_MODULE_LOADERS = {
|
|
47
|
+
'turn-stop-gate': () => import('./turn-stop-gate.mjs'),
|
|
48
|
+
'delivery-gate': () => import('./delivery-gate.mjs'),
|
|
49
|
+
'ralph-loop': () => import('./ralph-loop.mjs'),
|
|
50
|
+
};
|
|
51
|
+
const gateEvaluatorCache = new Map();
|
|
41
52
|
|
|
42
|
-
const playSound = (event) => _playSound(PKG_ROOT, event);
|
|
53
|
+
const playSound = (event, options) => _playSound(PKG_ROOT, event, options);
|
|
43
54
|
const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
|
|
44
55
|
|
|
45
56
|
function normalizeNotifyLevel(value) {
|
|
@@ -47,13 +58,13 @@ function normalizeNotifyLevel(value) {
|
|
|
47
58
|
return [0, 1, 2, 3].includes(level) ? level : 0;
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
function notifyByLevel(event, extra, settings = getSettings()) {
|
|
61
|
+
function notifyByLevel(event, extra, settings = getSettings(), options = {}) {
|
|
51
62
|
const level = normalizeNotifyLevel(settings.notify_level ?? 0);
|
|
52
63
|
if (level === 1) desktopNotify(event, extra);
|
|
53
|
-
if (level === 2) playSound(event);
|
|
64
|
+
if (level === 2) playSound(event, options);
|
|
54
65
|
if (level === 3) {
|
|
55
66
|
desktopNotify(event, extra);
|
|
56
|
-
playSound(event);
|
|
67
|
+
playSound(event, options);
|
|
57
68
|
}
|
|
58
69
|
}
|
|
59
70
|
|
|
@@ -84,50 +95,184 @@ function shouldRunRalphLoop(cwd, turnState, payload = {}) {
|
|
|
84
95
|
return RALPH_LOOP_ROUTE_COMMANDS.has(routeContext?.skillName);
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
function
|
|
98
|
+
function buildGateErrorReason(source, detail = '') {
|
|
99
|
+
return [
|
|
100
|
+
`[HelloAGENTS Runtime] ${source} 执行失败,已暂停完成通知。`,
|
|
101
|
+
detail ? `原因:${detail}` : '',
|
|
102
|
+
'请修复脚本或重新运行验证后再报告完成。',
|
|
103
|
+
].filter(Boolean).join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function emitInlineGateError(payload, source, detail = '') {
|
|
107
|
+
const reason = buildGateErrorReason(source, detail);
|
|
108
|
+
appendReplayEvent(payload.cwd || process.cwd(), {
|
|
109
|
+
host: HOST,
|
|
110
|
+
event: 'runtime_gate_error',
|
|
111
|
+
source,
|
|
112
|
+
reason,
|
|
113
|
+
payload,
|
|
114
|
+
});
|
|
115
|
+
output({
|
|
116
|
+
decision: 'block',
|
|
117
|
+
reason,
|
|
118
|
+
suppressOutput: true,
|
|
119
|
+
});
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stringifyStreamChunk(chunk, encoding) {
|
|
124
|
+
if (typeof chunk === 'string') return chunk;
|
|
125
|
+
if (chunk instanceof Uint8Array || Buffer.isBuffer(chunk)) {
|
|
126
|
+
return Buffer.from(chunk).toString(typeof encoding === 'string' ? encoding : 'utf-8');
|
|
127
|
+
}
|
|
128
|
+
return String(chunk ?? '');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function importGateModule(source) {
|
|
132
|
+
const loader = GATE_MODULE_LOADERS[source];
|
|
133
|
+
if (!loader) {
|
|
134
|
+
throw new Error(`无法解析的 JSON:未知 gate 模块 ${source}。`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
138
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
139
|
+
let capturedStdout = '';
|
|
140
|
+
let capturedStderr = '';
|
|
141
|
+
|
|
142
|
+
process.stdout.write = (chunk, encoding, callback) => {
|
|
143
|
+
capturedStdout += stringifyStreamChunk(chunk, encoding);
|
|
144
|
+
if (typeof encoding === 'function') encoding();
|
|
145
|
+
if (typeof callback === 'function') callback();
|
|
146
|
+
return true;
|
|
147
|
+
};
|
|
148
|
+
process.stderr.write = (chunk, encoding, callback) => {
|
|
149
|
+
capturedStderr += stringifyStreamChunk(chunk, encoding);
|
|
150
|
+
if (typeof encoding === 'function') encoding();
|
|
151
|
+
if (typeof callback === 'function') callback();
|
|
152
|
+
return true;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const module = await loader();
|
|
157
|
+
if (capturedStdout.trim() || capturedStderr.trim()) {
|
|
158
|
+
throw new Error(`无法解析的 JSON:模块导入时输出了意外内容。${capturedStdout || capturedStderr}`);
|
|
159
|
+
}
|
|
160
|
+
return module;
|
|
161
|
+
} finally {
|
|
162
|
+
process.stdout.write = originalStdoutWrite;
|
|
163
|
+
process.stderr.write = originalStderrWrite;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function loadGateEvaluator(source, exportName) {
|
|
168
|
+
let evaluatorPromise = gateEvaluatorCache.get(source);
|
|
169
|
+
if (!evaluatorPromise) {
|
|
170
|
+
evaluatorPromise = importGateModule(source).then((module) => {
|
|
171
|
+
const evaluate = module?.[exportName];
|
|
172
|
+
if (typeof evaluate !== 'function') {
|
|
173
|
+
throw new Error(`无法解析的 JSON:模块未导出 ${exportName}。`);
|
|
174
|
+
}
|
|
175
|
+
return evaluate;
|
|
176
|
+
});
|
|
177
|
+
gateEvaluatorCache.set(source, evaluatorPromise);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return await evaluatorPromise;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
gateEvaluatorCache.delete(source);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function runInlineGate({ payload, source, blockEvent, exportName, evaluateArgs }) {
|
|
189
|
+
let evaluate;
|
|
190
|
+
try {
|
|
191
|
+
evaluate = await loadGateEvaluator(source, exportName);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return emitInlineGateError(payload, source, error?.message || String(error));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let gateOutput;
|
|
197
|
+
try {
|
|
198
|
+
gateOutput = await evaluate(...evaluateArgs);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return emitInlineGateError(payload, source, error?.message || String(error));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!gateOutput || typeof gateOutput !== 'object' || Array.isArray(gateOutput)) {
|
|
204
|
+
return emitInlineGateError(payload, source, '无法解析的 JSON:gate 返回值不是对象。');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (gateOutput?.decision === 'block') {
|
|
208
|
+
appendReplayEvent(payload.cwd || process.cwd(), {
|
|
209
|
+
host: HOST,
|
|
210
|
+
event: blockEvent,
|
|
211
|
+
source,
|
|
212
|
+
reason: gateOutput.reason || '',
|
|
213
|
+
payload,
|
|
214
|
+
});
|
|
215
|
+
output(gateOutput);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function runRalphLoop(payload, { turnState } = {}) {
|
|
88
223
|
const settings = getSettings();
|
|
89
224
|
if (settings.ralph_loop_enabled === false) return false;
|
|
90
225
|
const cwd = payload.cwd || process.cwd();
|
|
91
226
|
if (!shouldRunRalphLoop(cwd, turnState, payload)) return false;
|
|
92
|
-
return
|
|
227
|
+
return await runInlineGate({
|
|
93
228
|
payload,
|
|
94
|
-
host: HOST,
|
|
95
|
-
scriptPath: join(__dirname, 'ralph-loop.mjs'),
|
|
96
|
-
args: IS_GEMINI ? ['--gemini'] : HOST === 'codex' ? ['--codex'] : [],
|
|
97
229
|
source: 'ralph-loop',
|
|
98
230
|
blockEvent: 'verify_gate_blocked',
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
231
|
+
exportName: 'evaluateRalphLoop',
|
|
232
|
+
evaluateArgs: [payload, {
|
|
233
|
+
isSubagent: false,
|
|
234
|
+
isGemini: IS_GEMINI,
|
|
235
|
+
hookEventName: HOST === 'codex' ? 'Stop' : (IS_GEMINI ? 'SessionEnd' : 'Stop'),
|
|
236
|
+
}],
|
|
102
237
|
});
|
|
103
238
|
}
|
|
104
239
|
|
|
105
|
-
function runDeliveryGate(payload) {
|
|
106
|
-
return
|
|
240
|
+
async function runDeliveryGate(payload) {
|
|
241
|
+
return await runInlineGate({
|
|
107
242
|
payload,
|
|
108
|
-
host: HOST,
|
|
109
|
-
scriptPath: join(__dirname, 'delivery-gate.mjs'),
|
|
110
243
|
source: 'delivery-gate',
|
|
111
244
|
blockEvent: 'delivery_gate_blocked',
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
output,
|
|
245
|
+
exportName: 'evaluateDeliveryGate',
|
|
246
|
+
evaluateArgs: [payload],
|
|
115
247
|
});
|
|
116
248
|
}
|
|
117
249
|
|
|
118
|
-
function runTurnStopGate(payload) {
|
|
119
|
-
return
|
|
250
|
+
async function runTurnStopGate(payload) {
|
|
251
|
+
return await runInlineGate({
|
|
120
252
|
payload,
|
|
121
|
-
host: HOST,
|
|
122
|
-
scriptPath: join(__dirname, 'turn-stop-gate.mjs'),
|
|
123
253
|
source: 'turn-stop-gate',
|
|
124
254
|
blockEvent: 'turn_stop_blocked',
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
output,
|
|
255
|
+
exportName: 'evaluateTurnStopGate',
|
|
256
|
+
evaluateArgs: [payload],
|
|
128
257
|
});
|
|
129
258
|
}
|
|
130
259
|
|
|
260
|
+
function hasManagedCodexStopHook() {
|
|
261
|
+
if (!IS_CODEX) return false;
|
|
262
|
+
try {
|
|
263
|
+
const hooksData = JSON.parse(readFileSync(CODEX_HOOKS_FILE, 'utf-8'));
|
|
264
|
+
const groups = Array.isArray(hooksData?.hooks?.Stop) ? hooksData.hooks.Stop : [];
|
|
265
|
+
return groups.some((group) => Array.isArray(group?.hooks) && group.hooks.some((handler) =>
|
|
266
|
+
handler?.type === 'command'
|
|
267
|
+
&& typeof handler.command === 'string'
|
|
268
|
+
&& handler.command.includes('helloagents-js')
|
|
269
|
+
&& handler.command.includes('notify stop --codex'),
|
|
270
|
+
));
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
131
276
|
function attachTurnSession(payload = {}, cwd = payload.cwd || process.cwd()) {
|
|
132
277
|
const sessionId = resolveSessionToken({
|
|
133
278
|
payload,
|
|
@@ -148,21 +293,23 @@ function consumeMainTurnState(cwd, turnState, payload = {}) {
|
|
|
148
293
|
if (turnState?.role === 'main') clearTurnState(cwd, { payload });
|
|
149
294
|
}
|
|
150
295
|
|
|
151
|
-
function
|
|
296
|
+
function shouldEmitManagedCodexCompleteNotify(cwd, turnState, payload = {}) {
|
|
152
297
|
if (turnState) return turnState.kind === 'complete';
|
|
153
|
-
|
|
298
|
+
const routeContext = getApplicableRouteContext({ cwd, payload });
|
|
299
|
+
return routeContext?.skillName !== 'auto';
|
|
154
300
|
}
|
|
155
301
|
|
|
156
|
-
function processTurnCloseout(payload, turnPayload, turnState, settings = getSettings()) {
|
|
302
|
+
async function processTurnCloseout(payload, turnPayload, turnState, settings = getSettings(), options = {}) {
|
|
157
303
|
const cwd = turnPayload.cwd || process.cwd();
|
|
304
|
+
const skipCompleteNotify = options.skipCompleteNotify === true;
|
|
158
305
|
|
|
159
|
-
if (runTurnStopGate(turnPayload)) {
|
|
306
|
+
if (await runTurnStopGate(turnPayload)) {
|
|
160
307
|
if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState, turnPayload);
|
|
161
308
|
return { blocked: true };
|
|
162
309
|
}
|
|
163
310
|
|
|
164
311
|
if (!turnState) {
|
|
165
|
-
notifyByLevel('complete', buildNotifyExtra(turnPayload), settings);
|
|
312
|
+
if (!skipCompleteNotify) notifyByLevel('complete', buildNotifyExtra(turnPayload), settings);
|
|
166
313
|
clearRouteContext({ cwd, payload: turnPayload });
|
|
167
314
|
return { blocked: false };
|
|
168
315
|
}
|
|
@@ -173,18 +320,18 @@ function processTurnCloseout(payload, turnPayload, turnState, settings = getSett
|
|
|
173
320
|
return { blocked: false };
|
|
174
321
|
}
|
|
175
322
|
|
|
176
|
-
if (runRalphLoop(turnPayload, { turnState })) {
|
|
323
|
+
if (await runRalphLoop(turnPayload, { turnState })) {
|
|
177
324
|
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
178
325
|
notifyByLevel('warning', buildNotifyExtra(payload), settings);
|
|
179
326
|
return { blocked: true };
|
|
180
327
|
}
|
|
181
|
-
if (runDeliveryGate(turnPayload)) {
|
|
328
|
+
if (await runDeliveryGate(turnPayload)) {
|
|
182
329
|
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
183
330
|
notifyByLevel('warning', buildNotifyExtra(payload), settings);
|
|
184
331
|
return { blocked: true };
|
|
185
332
|
}
|
|
186
333
|
|
|
187
|
-
notifyByLevel('complete', buildNotifyExtra(payload), settings);
|
|
334
|
+
if (!skipCompleteNotify) notifyByLevel('complete', buildNotifyExtra(payload), settings);
|
|
188
335
|
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
189
336
|
clearRouteContext({ cwd, payload: turnPayload });
|
|
190
337
|
return { blocked: false };
|
|
@@ -230,6 +377,7 @@ function cmdRoute() {
|
|
|
230
377
|
clearRouteContext,
|
|
231
378
|
appendReplayEvent,
|
|
232
379
|
getWorkflowRecommendation,
|
|
380
|
+
recordReplayEvents: !IS_SILENT,
|
|
233
381
|
suppress: (context) => IS_SILENT
|
|
234
382
|
? emptySuppress()
|
|
235
383
|
: suppressedOutput(EVENT_NAME.UserPromptSubmit, context),
|
|
@@ -251,20 +399,24 @@ function cmdInject() {
|
|
|
251
399
|
installMode: settings.install_mode || '',
|
|
252
400
|
payload,
|
|
253
401
|
});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
402
|
+
if (!IS_SILENT) {
|
|
403
|
+
appendReplayEvent(cwd, {
|
|
404
|
+
host: HOST,
|
|
405
|
+
event: 'session_injected',
|
|
406
|
+
source,
|
|
407
|
+
payload,
|
|
408
|
+
details: {
|
|
409
|
+
bootstrapFile,
|
|
410
|
+
installMode: settings.install_mode || '',
|
|
411
|
+
activatedProject: isProjectRuntimeActive(cwd),
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
}
|
|
265
415
|
clearRouteContext({ cwd, payload });
|
|
266
416
|
clearTurnState(cwd, { payload });
|
|
267
|
-
cleanupProjectSessions(cwd
|
|
417
|
+
cleanupProjectSessions(cwd, {
|
|
418
|
+
minIntervalMs: IS_SILENT ? PROJECT_SESSION_CLEANUP_COOLDOWN_MS : 0,
|
|
419
|
+
});
|
|
268
420
|
if (IS_SILENT) {
|
|
269
421
|
emptySuppress();
|
|
270
422
|
return;
|
|
@@ -286,11 +438,16 @@ function cmdInject() {
|
|
|
286
438
|
suppressedOutput(EVENT_NAME.SessionStart, context || undefined);
|
|
287
439
|
}
|
|
288
440
|
|
|
289
|
-
function cmdStop() {
|
|
441
|
+
async function cmdStop() {
|
|
290
442
|
const payload = readPayloadFromStdin();
|
|
291
443
|
const cwd = payload.cwd || process.cwd();
|
|
292
444
|
const turnPayload = attachTurnSession(payload, cwd);
|
|
293
445
|
const turnState = readMainTurnState(cwd, turnPayload);
|
|
446
|
+
const managedCodexStopHook = IS_CODEX && hasManagedCodexStopHook();
|
|
447
|
+
const skipCompleteNotify = managedCodexStopHook && hasCodexQuickNotifyEvidence(cwd, {
|
|
448
|
+
payload: turnPayload,
|
|
449
|
+
turnState,
|
|
450
|
+
});
|
|
294
451
|
const closeoutClaim = IS_CODEX
|
|
295
452
|
? beginCodexCloseoutClaim(cwd, { payload: turnPayload, turnState, source: 'stop' })
|
|
296
453
|
: null;
|
|
@@ -302,7 +459,9 @@ function cmdStop() {
|
|
|
302
459
|
let handled = false;
|
|
303
460
|
let result = { blocked: false };
|
|
304
461
|
try {
|
|
305
|
-
result = processTurnCloseout(payload, turnPayload, turnState, getSettings()
|
|
462
|
+
result = await processTurnCloseout(payload, turnPayload, turnState, getSettings(), {
|
|
463
|
+
skipCompleteNotify,
|
|
464
|
+
});
|
|
306
465
|
handled = true;
|
|
307
466
|
} finally {
|
|
308
467
|
finalizeCodexCloseoutClaim(closeoutClaim, {
|
|
@@ -317,14 +476,14 @@ function cmdStop() {
|
|
|
317
476
|
}
|
|
318
477
|
|
|
319
478
|
function cmdSound() {
|
|
320
|
-
playSound(process.argv[3] || 'complete');
|
|
479
|
+
playSound(process.argv[3] || 'complete', { mode: 'blocking' });
|
|
321
480
|
}
|
|
322
481
|
|
|
323
482
|
function cmdDesktop() {
|
|
324
483
|
desktopNotify(process.argv[3] || 'complete', buildNotifyExtra({ cwd: process.cwd() }));
|
|
325
484
|
}
|
|
326
485
|
|
|
327
|
-
function cmdCodexNotify() {
|
|
486
|
+
async function cmdCodexNotify() {
|
|
328
487
|
let data = {};
|
|
329
488
|
try { data = JSON.parse(process.argv[3] || '{}'); } catch {}
|
|
330
489
|
data = normalizeNotifyPayload(data);
|
|
@@ -336,10 +495,22 @@ function cmdCodexNotify() {
|
|
|
336
495
|
if (shouldIgnoreCodexNotifyClient(client)) return;
|
|
337
496
|
|
|
338
497
|
if (type === 'approval-requested') {
|
|
339
|
-
notifyByLevel('confirm', buildNotifyExtra(data));
|
|
498
|
+
notifyByLevel('confirm', buildNotifyExtra(data), getSettings(), { mode: 'blocking' });
|
|
340
499
|
return;
|
|
341
500
|
}
|
|
342
501
|
if (type !== 'agent-turn-complete') return;
|
|
502
|
+
if (hasManagedCodexStopHook()) {
|
|
503
|
+
const turnState = readMainTurnState(cwd, turnPayload);
|
|
504
|
+
if (shouldEmitManagedCodexCompleteNotify(cwd, turnState, turnPayload)) {
|
|
505
|
+
notifyByLevel('complete', buildNotifyExtra(data), getSettings(), { mode: 'blocking' });
|
|
506
|
+
writeCodexQuickNotifyEvidence(cwd, {
|
|
507
|
+
payload: turnPayload,
|
|
508
|
+
turnState,
|
|
509
|
+
event: type,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
343
514
|
|
|
344
515
|
const turnState = readMainTurnState(cwd, turnPayload);
|
|
345
516
|
const closeoutClaim = beginCodexCloseoutClaim(cwd, {
|
|
@@ -351,7 +522,7 @@ function cmdCodexNotify() {
|
|
|
351
522
|
|
|
352
523
|
let handled = false;
|
|
353
524
|
try {
|
|
354
|
-
processTurnCloseout(data, turnPayload, turnState, getSettings());
|
|
525
|
+
await processTurnCloseout(data, turnPayload, turnState, getSettings());
|
|
355
526
|
handled = true;
|
|
356
527
|
} finally {
|
|
357
528
|
finalizeCodexCloseoutClaim(closeoutClaim, {
|
|
@@ -363,15 +534,19 @@ function cmdCodexNotify() {
|
|
|
363
534
|
}
|
|
364
535
|
}
|
|
365
536
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
537
|
+
async function main() {
|
|
538
|
+
switch (cmd) {
|
|
539
|
+
case 'inject': cmdInject(); break;
|
|
540
|
+
case 'stop': await cmdStop(); break;
|
|
541
|
+
case 'pre-compact': cmdPreCompact(); break;
|
|
542
|
+
case 'route': cmdRoute(); break;
|
|
543
|
+
case 'sound': cmdSound(); break;
|
|
544
|
+
case 'desktop': cmdDesktop(); break;
|
|
545
|
+
case 'codex-notify': await cmdCodexNotify(); break;
|
|
546
|
+
default:
|
|
547
|
+
process.stderr.write(`notify.mjs: unknown command "${cmd}"\n`);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
377
550
|
}
|
|
551
|
+
|
|
552
|
+
await main();
|
|
@@ -10,8 +10,11 @@ import {
|
|
|
10
10
|
getProjectActivationDir,
|
|
11
11
|
getProjectRoot,
|
|
12
12
|
readJsonFile,
|
|
13
|
+
writeJsonFileAtomic,
|
|
13
14
|
} from './runtime-scope.mjs'
|
|
14
15
|
|
|
16
|
+
export const PROJECT_SESSION_CLEANUP_COOLDOWN_MS = 10 * 60 * 1000
|
|
17
|
+
|
|
15
18
|
function removePath(filePath, result, bucket) {
|
|
16
19
|
try {
|
|
17
20
|
rmSync(filePath, { recursive: true, force: true })
|
|
@@ -58,7 +61,21 @@ function shouldKeepSession(active, workspace, session) {
|
|
|
58
61
|
return activeWorkspace === workspace && active.session === session
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
function readCleanupCheckedAt(active) {
|
|
65
|
+
const raw = active && typeof active === 'object' ? active.cleanupCheckedAt : ''
|
|
66
|
+
const timestamp = Date.parse(raw || '')
|
|
67
|
+
return Number.isFinite(timestamp) ? timestamp : 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeCleanupCheckpoint(activePath, active, now) {
|
|
71
|
+
if (!active || typeof active !== 'object' || Object.keys(active).length === 0) return
|
|
72
|
+
writeJsonFileAtomic(activePath, {
|
|
73
|
+
...active,
|
|
74
|
+
cleanupCheckedAt: new Date(now).toISOString(),
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs = 0 } = {}) {
|
|
62
79
|
const projectRoot = getProjectRoot(cwd)
|
|
63
80
|
const activationDir = getProjectActivationDir(projectRoot)
|
|
64
81
|
const sessionsDir = join(activationDir, PROJECT_SESSIONS_DIR_NAME)
|
|
@@ -69,9 +86,17 @@ export function cleanupProjectSessions(cwd) {
|
|
|
69
86
|
removedEmptyDirs: [],
|
|
70
87
|
removedRouteOnlyDirs: [],
|
|
71
88
|
errors: [],
|
|
89
|
+
skipped: false,
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
if (!existsSync(sessionsDir)) return result
|
|
93
|
+
if (minIntervalMs > 0) {
|
|
94
|
+
const lastCleanupAt = readCleanupCheckedAt(active)
|
|
95
|
+
if (lastCleanupAt > 0 && now - lastCleanupAt < minIntervalMs) {
|
|
96
|
+
result.skipped = true
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
}
|
|
75
100
|
|
|
76
101
|
for (const workspaceEntry of readdirSync(sessionsDir, { withFileTypes: true })) {
|
|
77
102
|
if (!workspaceEntry.isDirectory()) continue
|
|
@@ -102,5 +127,6 @@ export function cleanupProjectSessions(cwd) {
|
|
|
102
127
|
}
|
|
103
128
|
}
|
|
104
129
|
|
|
130
|
+
writeCleanupCheckpoint(activePath, active, now)
|
|
105
131
|
return result
|
|
106
132
|
}
|