u-foo 1.8.1 → 1.8.3
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/package.json +1 -1
- package/src/agent/launcher.js +41 -1
- package/src/agent/notifier.js +16 -3
- package/src/daemon/index.js +35 -3
- package/src/daemon/providerSessions.js +205 -23
- package/src/group/promptProfiles.js +13 -0
package/package.json
CHANGED
package/src/agent/launcher.js
CHANGED
|
@@ -132,6 +132,12 @@ function findPreviousSession(cwd, agentType, tty, tmuxPane) {
|
|
|
132
132
|
continue;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// 检查旧 shell 进程是否还存活(防止 tty 回收导致误匹配)
|
|
136
|
+
if (meta.tty_shell_pid && !isAgentPidAlive(meta.tty_shell_pid)) {
|
|
137
|
+
// shell 已退出,说明是不同的终端窗口复用了同一个 tty
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
135
141
|
// 找到了可以复用的旧 session
|
|
136
142
|
const parts = id.split(":");
|
|
137
143
|
if (parts.length !== 2) continue;
|
|
@@ -504,6 +510,8 @@ class AgentLauncher {
|
|
|
504
510
|
* 启动 agent
|
|
505
511
|
*/
|
|
506
512
|
async launch(args) {
|
|
513
|
+
// 保存用户/group 显式传入的 nickname(在 session 复用覆盖前)
|
|
514
|
+
this._originalNickname = process.env.UFOO_NICKNAME || "";
|
|
507
515
|
try {
|
|
508
516
|
// 1. 确保初始化
|
|
509
517
|
await this.ensureInit();
|
|
@@ -537,8 +545,9 @@ class AgentLauncher {
|
|
|
537
545
|
|
|
538
546
|
// 6. 启动消息通知监听器(ufoo-code 改为内部 bus 轮询消费)
|
|
539
547
|
const shouldStartNotifier = String(this.agentType || "").trim().toLowerCase() !== "ufoo-code";
|
|
548
|
+
let notifier = null;
|
|
540
549
|
if (shouldStartNotifier) {
|
|
541
|
-
|
|
550
|
+
notifier = new AgentNotifier(this.cwd, subscriberId);
|
|
542
551
|
notifier.start();
|
|
543
552
|
}
|
|
544
553
|
|
|
@@ -605,6 +614,10 @@ class AgentLauncher {
|
|
|
605
614
|
});
|
|
606
615
|
readyDetector.onReady(async () => {
|
|
607
616
|
launcherActivityDetector.markReady();
|
|
617
|
+
// 通知 notifier launcher 已检测到 ready,可以开始设 idle 状态
|
|
618
|
+
if (notifier) {
|
|
619
|
+
notifier.markLauncherReady();
|
|
620
|
+
}
|
|
608
621
|
// Claude Code's Ink TUI renders ❯ prompt before the input handler
|
|
609
622
|
// is fully mounted. Wait a short period for the TUI to be ready to
|
|
610
623
|
// accept injected text, otherwise only the trailing CR is processed
|
|
@@ -612,6 +625,26 @@ class AgentLauncher {
|
|
|
612
625
|
if (this.agentType === "claude-code") {
|
|
613
626
|
await new Promise((r) => setTimeout(r, 800));
|
|
614
627
|
}
|
|
628
|
+
|
|
629
|
+
// Claude Code: inject /rename 设置 session 标签(在 AGENT_READY/bootstrap 之前)
|
|
630
|
+
// /rename 是 slash 命令,瞬间完成,不调 LLM
|
|
631
|
+
// 仅在有显式 nickname 时注入(UFOO_NICKNAME 由 group launch 或用户指定)
|
|
632
|
+
// 使用启动前保存的原始值,避免复用 nickname 污染
|
|
633
|
+
const explicitNickname = this._originalNickname || "";
|
|
634
|
+
if (this.agentType === "claude-code" && explicitNickname && wrapper.pty) {
|
|
635
|
+
try {
|
|
636
|
+
const safeNick = AgentLauncher._sanitizeNickname(explicitNickname);
|
|
637
|
+
if (safeNick) {
|
|
638
|
+
wrapper.write(`/rename ${safeNick}\r`);
|
|
639
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
640
|
+
}
|
|
641
|
+
} catch (err) {
|
|
642
|
+
if (process.env.UFOO_DEBUG) {
|
|
643
|
+
console.error("[rename] inject failed:", err.message);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
615
648
|
const startTime = Date.now();
|
|
616
649
|
try {
|
|
617
650
|
const daemonSock = await connectWithRetry(daemonSockPath, 3, 100);
|
|
@@ -619,6 +652,7 @@ class AgentLauncher {
|
|
|
619
652
|
daemonSock.write(`${JSON.stringify({
|
|
620
653
|
type: IPC_REQUEST_TYPES.AGENT_READY,
|
|
621
654
|
subscriberId,
|
|
655
|
+
agentPid: wrapper.pty ? wrapper.pty.pid : 0,
|
|
622
656
|
})}\n`);
|
|
623
657
|
daemonSock.end();
|
|
624
658
|
|
|
@@ -637,6 +671,7 @@ class AgentLauncher {
|
|
|
637
671
|
console.error(`[ready] daemon notification error: ${err.message}, will use fallback delay`);
|
|
638
672
|
}
|
|
639
673
|
}
|
|
674
|
+
|
|
640
675
|
});
|
|
641
676
|
|
|
642
677
|
// Fallback:如果10秒后还没检测到ready,强制标记为ready
|
|
@@ -670,6 +705,7 @@ class AgentLauncher {
|
|
|
670
705
|
wrapper.spawn();
|
|
671
706
|
wrapper.attachStreams(process.stdin, process.stdout, process.stderr);
|
|
672
707
|
|
|
708
|
+
|
|
673
709
|
// 启动inject监听socket(用于外部注入命令到PTY)
|
|
674
710
|
const injectSockPath = path.join(
|
|
675
711
|
getUfooPaths(this.cwd).busQueuesDir,
|
|
@@ -873,4 +909,8 @@ class AgentLauncher {
|
|
|
873
909
|
}
|
|
874
910
|
}
|
|
875
911
|
|
|
912
|
+
// Exported for testing
|
|
913
|
+
AgentLauncher._sanitizeNickname = (nick) => nick.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
914
|
+
AgentLauncher._findPreviousSession = findPreviousSession;
|
|
915
|
+
|
|
876
916
|
module.exports = AgentLauncher;
|
package/src/agent/notifier.js
CHANGED
|
@@ -29,6 +29,7 @@ class AgentNotifier {
|
|
|
29
29
|
this.lastNickname = "";
|
|
30
30
|
this.lastUbusWakeCount = -1;
|
|
31
31
|
|
|
32
|
+
|
|
32
33
|
// 计算队列文件路径
|
|
33
34
|
const safeSub = subscriber.replace(/:/g, "_");
|
|
34
35
|
const paths = getUfooPaths(projectRoot);
|
|
@@ -69,9 +70,18 @@ class AgentNotifier {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 通知 notifier launcher 已检测到 agent ready
|
|
75
|
+
* 在此之前 notifier 不会将 activity_state 设为 idle(避免 bootstrap 提前注入)
|
|
76
|
+
*/
|
|
77
|
+
markLauncherReady() {
|
|
78
|
+
this._launcherReady = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
/**
|
|
73
82
|
* 设置终端标题为昵称
|
|
74
|
-
*
|
|
83
|
+
* codex 等: OSC escape sequence + iTerm2 badge
|
|
84
|
+
* claude-code: OSC 会被 Claude 覆盖,仅设 iTerm2 badge
|
|
75
85
|
*/
|
|
76
86
|
setTitle(nickname) {
|
|
77
87
|
if (!nickname) return;
|
|
@@ -364,7 +374,7 @@ class AgentNotifier {
|
|
|
364
374
|
}
|
|
365
375
|
|
|
366
376
|
this.lastCount = this.getMessageCount();
|
|
367
|
-
if (!this.lastWorkingAt || nowMs - this.lastWorkingAt >= this.workingHoldMs) {
|
|
377
|
+
if (this._launcherReady && (!this.lastWorkingAt || nowMs - this.lastWorkingAt >= this.workingHoldMs)) {
|
|
368
378
|
this.updateActivityState("idle");
|
|
369
379
|
}
|
|
370
380
|
this.refreshTitle();
|
|
@@ -381,7 +391,10 @@ class AgentNotifier {
|
|
|
381
391
|
if (this.lastNickname) {
|
|
382
392
|
this.setTitle(this.lastNickname);
|
|
383
393
|
}
|
|
384
|
-
this.updateActivityState("
|
|
394
|
+
this.updateActivityState("starting");
|
|
395
|
+
// launcher 的 readyDetector 负责在 TUI 真正 ready 后标记 "ready"
|
|
396
|
+
// 在那之前 notifier 不应覆盖 activity_state
|
|
397
|
+
this._launcherReady = false;
|
|
385
398
|
|
|
386
399
|
// 启动轮询
|
|
387
400
|
this.timer = setInterval(() => {
|
package/src/daemon/index.js
CHANGED
|
@@ -13,7 +13,7 @@ const { createDaemonIpcServer } = require("./ipcServer");
|
|
|
13
13
|
const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
|
|
14
14
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
15
15
|
const { upsertProjectRuntime, markProjectStopped } = require("../projects/registry");
|
|
16
|
-
const { scheduleProviderSessionProbe, loadProviderSessionCache } = require("./providerSessions");
|
|
16
|
+
const { scheduleProviderSessionProbe, resolveSessionFromFile, persistProviderSession, loadProviderSessionCache } = require("./providerSessions");
|
|
17
17
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
18
18
|
const { createDaemonCronController } = require("./cronOps");
|
|
19
19
|
const { createGroupOrchestrator } = require("./groupOrchestrator");
|
|
@@ -499,6 +499,7 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
499
499
|
subscriberId,
|
|
500
500
|
agentType: probeAgentType,
|
|
501
501
|
nickname: resolvedNickname,
|
|
502
|
+
agentCwd: projectRoot,
|
|
502
503
|
onResolved: (id, resolved) => {
|
|
503
504
|
if (providerSessions) {
|
|
504
505
|
providerSessions.set(id, {
|
|
@@ -1973,6 +1974,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1973
1974
|
subscriberId,
|
|
1974
1975
|
agentType,
|
|
1975
1976
|
nickname: resolvedNickname,
|
|
1977
|
+
agentCwd: projectRoot,
|
|
1976
1978
|
onResolved: (id, resolved) => {
|
|
1977
1979
|
if (providerSessions) {
|
|
1978
1980
|
providerSessions.set(id, {
|
|
@@ -2009,11 +2011,41 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2009
2011
|
return;
|
|
2010
2012
|
}
|
|
2011
2013
|
if (req.type === IPC_REQUEST_TYPES.AGENT_READY) {
|
|
2012
|
-
const { subscriberId } = req;
|
|
2014
|
+
const { subscriberId, agentPid } = req;
|
|
2013
2015
|
if (!subscriberId) {
|
|
2014
2016
|
return;
|
|
2015
2017
|
}
|
|
2016
|
-
log(`agent_ready id=${subscriberId} -
|
|
2018
|
+
log(`agent_ready id=${subscriberId} pid=${agentPid || 0} - resolving session`);
|
|
2019
|
+
|
|
2020
|
+
// Try direct file read first if we have agentPid (fast path)
|
|
2021
|
+
const parsedAgentPid = Number.parseInt(agentPid, 10);
|
|
2022
|
+
if (Number.isFinite(parsedAgentPid) && parsedAgentPid > 0) {
|
|
2023
|
+
const agentType = subscriberId.split(":")[0] || "";
|
|
2024
|
+
const resolved = resolveSessionFromFile(agentType, {
|
|
2025
|
+
pid: parsedAgentPid,
|
|
2026
|
+
cwd: projectRoot,
|
|
2027
|
+
});
|
|
2028
|
+
if (resolved && resolved.sessionId) {
|
|
2029
|
+
log(`agent_ready session resolved from file for ${subscriberId}: ${resolved.sessionId}`);
|
|
2030
|
+
persistProviderSession(projectRoot, subscriberId, resolved);
|
|
2031
|
+
if (providerSessions) {
|
|
2032
|
+
providerSessions.set(subscriberId, {
|
|
2033
|
+
sessionId: resolved.sessionId,
|
|
2034
|
+
source: resolved.source || "",
|
|
2035
|
+
updated_at: new Date().toISOString(),
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
// Cancel the scheduled probe to prevent /ufoo injection
|
|
2039
|
+
const handle = probeHandles.get(subscriberId);
|
|
2040
|
+
if (handle && typeof handle.cancel === "function") {
|
|
2041
|
+
handle.cancel();
|
|
2042
|
+
}
|
|
2043
|
+
probeHandles.delete(subscriberId);
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// Fallback: trigger scheduled probe
|
|
2017
2049
|
const probeHandle = probeHandles.get(subscriberId);
|
|
2018
2050
|
if (probeHandle && typeof probeHandle.triggerNow === "function") {
|
|
2019
2051
|
probeHandle.triggerNow().catch((err) => {
|
|
@@ -233,74 +233,256 @@ async function executeProbe({
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
|
-
*
|
|
236
|
+
* Resolve Claude Code session ID directly from session file.
|
|
237
|
+
* Claude writes ~/.claude/sessions/<pid>.json with { sessionId, pid, cwd, ... }
|
|
238
|
+
*/
|
|
239
|
+
function resolveClaudeSessionFromFile(pid) {
|
|
240
|
+
if (!pid) return null;
|
|
241
|
+
const filePath = path.join(os.homedir(), ".claude", "sessions", `${pid}.json`);
|
|
242
|
+
try {
|
|
243
|
+
if (!fs.existsSync(filePath)) return null;
|
|
244
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
245
|
+
const sessionId = data.sessionId || data.session_id || "";
|
|
246
|
+
if (!sessionId) return null;
|
|
247
|
+
return { sessionId, source: filePath };
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolve Codex session ID from session rollout files.
|
|
255
|
+
* Codex writes ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl
|
|
256
|
+
* First line contains { type: "session_meta", payload: { id, cwd, ... } }
|
|
257
|
+
*/
|
|
258
|
+
function resolveCodexSessionFromFile(cwd) {
|
|
259
|
+
if (!cwd) return null;
|
|
260
|
+
try {
|
|
261
|
+
const now = new Date();
|
|
262
|
+
// Check today and yesterday (session may have started before midnight)
|
|
263
|
+
const dates = [now];
|
|
264
|
+
const yesterday = new Date(now);
|
|
265
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
266
|
+
dates.push(yesterday);
|
|
267
|
+
|
|
268
|
+
let bestMatch = null;
|
|
269
|
+
let bestMtime = 0;
|
|
270
|
+
|
|
271
|
+
for (const d of dates) {
|
|
272
|
+
const yyyy = String(d.getFullYear());
|
|
273
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
274
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
275
|
+
const dir = path.join(os.homedir(), ".codex", "sessions", yyyy, mm, dd);
|
|
276
|
+
if (!fs.existsSync(dir)) continue;
|
|
277
|
+
|
|
278
|
+
const files = fs.readdirSync(dir)
|
|
279
|
+
.filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl"));
|
|
280
|
+
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
const filePath = path.join(dir, file);
|
|
283
|
+
try {
|
|
284
|
+
const stat = fs.statSync(filePath);
|
|
285
|
+
if (stat.mtimeMs <= bestMtime) continue;
|
|
286
|
+
|
|
287
|
+
// Read first line for session_meta
|
|
288
|
+
const fd = fs.openSync(filePath, "r");
|
|
289
|
+
const buf = Buffer.alloc(4096);
|
|
290
|
+
const bytesRead = fs.readSync(fd, buf, 0, 4096, 0);
|
|
291
|
+
fs.closeSync(fd);
|
|
292
|
+
const firstLine = buf.toString("utf8", 0, bytesRead).split("\n")[0];
|
|
293
|
+
if (!firstLine) continue;
|
|
294
|
+
|
|
295
|
+
const record = JSON.parse(firstLine);
|
|
296
|
+
const payload = record.payload || record;
|
|
297
|
+
const sessionCwd = payload.cwd || "";
|
|
298
|
+
const sessionId = payload.id || "";
|
|
299
|
+
|
|
300
|
+
if (sessionId && sessionCwd === cwd && stat.mtimeMs > bestMtime) {
|
|
301
|
+
bestMatch = { sessionId, source: filePath };
|
|
302
|
+
bestMtime = stat.mtimeMs;
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return bestMatch;
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Resolve provider session ID directly from session files (no probe needed).
|
|
317
|
+
* @param {string} agentType - "claude-code" or "codex"
|
|
318
|
+
* @param {object} opts - { pid, cwd }
|
|
319
|
+
*/
|
|
320
|
+
function resolveSessionFromFile(agentType, opts = {}) {
|
|
321
|
+
if (agentType === "claude-code") {
|
|
322
|
+
return resolveClaudeSessionFromFile(opts.pid);
|
|
323
|
+
}
|
|
324
|
+
if (agentType === "codex") {
|
|
325
|
+
return resolveCodexSessionFromFile(opts.cwd);
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Retry reading session file (agent may not have written it yet)
|
|
332
|
+
*/
|
|
333
|
+
async function resolveSessionFromFileWithRetries(agentType, opts = {}, attempts = 10, intervalMs = 1000) {
|
|
334
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
335
|
+
const resolved = resolveSessionFromFile(agentType, opts);
|
|
336
|
+
if (resolved && resolved.sessionId) return resolved;
|
|
337
|
+
// eslint-disable-next-line no-await-in-loop
|
|
338
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Schedule provider session resolution.
|
|
345
|
+
* Tries direct file read first (fast, non-invasive), falls back to probe if needed.
|
|
237
346
|
*
|
|
238
347
|
* @param {Object} options
|
|
239
348
|
* @param {string} options.projectRoot - Project root directory
|
|
240
349
|
* @param {string} options.subscriberId - Subscriber ID (e.g., "claude-code:abc123")
|
|
241
350
|
* @param {string} options.agentType - Agent type ("claude-code" or "codex")
|
|
242
351
|
* @param {string} options.nickname - Agent nickname (e.g., "claude-47")
|
|
243
|
-
* @param {number} options.
|
|
244
|
-
* @param {
|
|
245
|
-
* @param {number} options.
|
|
352
|
+
* @param {number} options.agentPid - Agent child process PID (for claude-code)
|
|
353
|
+
* @param {string} options.agentCwd - Agent working directory (for codex)
|
|
354
|
+
* @param {number} options.delayMs - Delay before starting resolution
|
|
355
|
+
* @param {number} options.fileAttempts - File read retry attempts
|
|
356
|
+
* @param {number} options.fileIntervalMs - File read retry interval
|
|
357
|
+
* @param {number} options.probeAttempts - Probe retry attempts (fallback)
|
|
358
|
+
* @param {number} options.probeIntervalMs - Probe retry interval (fallback)
|
|
246
359
|
* @param {Function} options.onResolved - Callback when session ID is found
|
|
247
360
|
*/
|
|
248
|
-
function
|
|
361
|
+
function scheduleProviderSessionResolve({
|
|
249
362
|
projectRoot,
|
|
250
363
|
subscriberId,
|
|
251
364
|
agentType,
|
|
252
365
|
nickname,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
366
|
+
agentPid = 0,
|
|
367
|
+
agentCwd = "",
|
|
368
|
+
delayMs = 3000,
|
|
369
|
+
fileAttempts = 10,
|
|
370
|
+
fileIntervalMs = 1000,
|
|
371
|
+
probeAttempts = 15,
|
|
372
|
+
probeIntervalMs = 2000,
|
|
256
373
|
onResolved = null,
|
|
257
374
|
}) {
|
|
258
375
|
if (!subscriberId || !agentType) return null;
|
|
259
376
|
if (agentType !== "codex" && agentType !== "claude-code") return null;
|
|
260
|
-
if (!nickname) return null;
|
|
261
377
|
|
|
262
|
-
const marker = buildProbeMarker(nickname);
|
|
263
|
-
|
|
378
|
+
const marker = nickname ? buildProbeMarker(nickname) : "";
|
|
379
|
+
if (marker) {
|
|
380
|
+
persistProbeMarker(projectRoot, subscriberId, marker);
|
|
381
|
+
}
|
|
264
382
|
|
|
265
383
|
let executed = false;
|
|
384
|
+
let cancelled = false;
|
|
266
385
|
let timer = null;
|
|
267
386
|
|
|
268
387
|
const execute = async () => {
|
|
269
|
-
if (executed) return;
|
|
388
|
+
if (executed || cancelled) return;
|
|
270
389
|
executed = true;
|
|
271
390
|
if (timer) {
|
|
272
391
|
clearTimeout(timer);
|
|
273
392
|
timer = null;
|
|
274
393
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
394
|
+
|
|
395
|
+
// 1. Try direct file read (fast, non-invasive)
|
|
396
|
+
const fileOpts = { pid: agentPid, cwd: agentCwd || projectRoot };
|
|
397
|
+
const fileResolved = await resolveSessionFromFileWithRetries(
|
|
398
|
+
agentType, fileOpts, fileAttempts, fileIntervalMs,
|
|
399
|
+
);
|
|
400
|
+
if (cancelled) return;
|
|
401
|
+
if (fileResolved && fileResolved.sessionId) {
|
|
402
|
+
persistProviderSession(projectRoot, subscriberId, fileResolved);
|
|
403
|
+
if (typeof onResolved === "function") {
|
|
404
|
+
onResolved(subscriberId, fileResolved);
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 2. Fallback to probe (inject command + search history)
|
|
410
|
+
// Re-check cancelled: AGENT_READY may have resolved session while we were retrying
|
|
411
|
+
if (cancelled) return;
|
|
412
|
+
if (nickname) {
|
|
413
|
+
await executeProbe({
|
|
414
|
+
projectRoot,
|
|
415
|
+
subscriberId,
|
|
416
|
+
agentType,
|
|
417
|
+
nickname,
|
|
418
|
+
attempts: probeAttempts,
|
|
419
|
+
intervalMs: probeIntervalMs,
|
|
420
|
+
onResolved,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
284
423
|
};
|
|
285
424
|
|
|
286
|
-
// Schedule delayed execution
|
|
425
|
+
// Schedule delayed execution
|
|
287
426
|
timer = setTimeout(execute, delayMs);
|
|
288
427
|
|
|
289
|
-
// Return handle for early trigger
|
|
428
|
+
// Return handle for early trigger or cancellation
|
|
290
429
|
return {
|
|
291
430
|
subscriberId,
|
|
292
431
|
marker,
|
|
293
432
|
triggerNow: execute,
|
|
433
|
+
cancel: () => {
|
|
434
|
+
cancelled = true;
|
|
435
|
+
executed = true;
|
|
436
|
+
if (timer) {
|
|
437
|
+
clearTimeout(timer);
|
|
438
|
+
timer = null;
|
|
439
|
+
}
|
|
440
|
+
},
|
|
294
441
|
};
|
|
295
442
|
}
|
|
296
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Schedule a provider session probe (legacy wrapper)
|
|
446
|
+
*/
|
|
447
|
+
function scheduleProviderSessionProbe({
|
|
448
|
+
projectRoot,
|
|
449
|
+
subscriberId,
|
|
450
|
+
agentType,
|
|
451
|
+
nickname,
|
|
452
|
+
delayMs = 8000,
|
|
453
|
+
attempts = 15,
|
|
454
|
+
intervalMs = 2000,
|
|
455
|
+
onResolved = null,
|
|
456
|
+
agentPid = 0,
|
|
457
|
+
agentCwd = "",
|
|
458
|
+
}) {
|
|
459
|
+
// Delegate to new resolve function which tries file read first
|
|
460
|
+
return scheduleProviderSessionResolve({
|
|
461
|
+
projectRoot,
|
|
462
|
+
subscriberId,
|
|
463
|
+
agentType,
|
|
464
|
+
nickname,
|
|
465
|
+
agentPid,
|
|
466
|
+
agentCwd,
|
|
467
|
+
delayMs: agentPid || agentCwd ? 3000 : delayMs,
|
|
468
|
+
probeAttempts: attempts,
|
|
469
|
+
probeIntervalMs: intervalMs,
|
|
470
|
+
onResolved,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
297
474
|
module.exports = {
|
|
298
475
|
scheduleProviderSessionProbe,
|
|
476
|
+
scheduleProviderSessionResolve,
|
|
477
|
+
resolveSessionFromFile,
|
|
478
|
+
persistProviderSession,
|
|
299
479
|
loadProviderSessionCache,
|
|
300
480
|
__private: {
|
|
301
481
|
buildProbeCommand,
|
|
302
482
|
recordContainsMarker,
|
|
303
483
|
containsProbeCommand,
|
|
304
484
|
escapeRegExp,
|
|
485
|
+
resolveClaudeSessionFromFile,
|
|
486
|
+
resolveCodexSessionFromFile,
|
|
305
487
|
},
|
|
306
488
|
};
|
|
@@ -44,6 +44,7 @@ const BUILTIN_PROFILES = [
|
|
|
44
44
|
"Handoff:",
|
|
45
45
|
"- Send the architect a scoped brief.",
|
|
46
46
|
"- Send the scope challenger any assumptions that feel inflated or weak.",
|
|
47
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
47
48
|
].join("\n"),
|
|
48
49
|
},
|
|
49
50
|
{
|
|
@@ -71,6 +72,7 @@ const BUILTIN_PROFILES = [
|
|
|
71
72
|
"",
|
|
72
73
|
"Handoff:",
|
|
73
74
|
"- Send approved scope decisions to the architect and builder.",
|
|
75
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
74
76
|
].join("\n"),
|
|
75
77
|
},
|
|
76
78
|
{
|
|
@@ -99,6 +101,7 @@ const BUILTIN_PROFILES = [
|
|
|
99
101
|
"Handoff:",
|
|
100
102
|
"- Send execution-ready slices to the implementation lead.",
|
|
101
103
|
"- Send risk hotspots to the reviewer and QA roles.",
|
|
104
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
102
105
|
].join("\n"),
|
|
103
106
|
},
|
|
104
107
|
{
|
|
@@ -126,6 +129,7 @@ const BUILTIN_PROFILES = [
|
|
|
126
129
|
"",
|
|
127
130
|
"Handoff:",
|
|
128
131
|
"- Send changed areas and known risk points to review-critic and qa-driver.",
|
|
132
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
129
133
|
].join("\n"),
|
|
130
134
|
},
|
|
131
135
|
{
|
|
@@ -154,6 +158,7 @@ const BUILTIN_PROFILES = [
|
|
|
154
158
|
"",
|
|
155
159
|
"Handoff:",
|
|
156
160
|
"- Send changed surfaces and known UI tradeoffs to design-critic and qa-driver.",
|
|
161
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
157
162
|
].join("\n"),
|
|
158
163
|
},
|
|
159
164
|
{
|
|
@@ -183,6 +188,7 @@ const BUILTIN_PROFILES = [
|
|
|
183
188
|
"Handoff:",
|
|
184
189
|
"- Send ranked UI issues and concrete polish guidance to frontend-refiner.",
|
|
185
190
|
"- Send user-visible risk items and regression watch points to qa-driver.",
|
|
191
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
186
192
|
].join("\n"),
|
|
187
193
|
},
|
|
188
194
|
{
|
|
@@ -209,6 +215,7 @@ const BUILTIN_PROFILES = [
|
|
|
209
215
|
"Handoff:",
|
|
210
216
|
"- Send must-fix items back to implementation lead.",
|
|
211
217
|
"- Send user-visible risk items to qa-driver.",
|
|
218
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
212
219
|
].join("\n"),
|
|
213
220
|
},
|
|
214
221
|
{
|
|
@@ -235,6 +242,7 @@ const BUILTIN_PROFILES = [
|
|
|
235
242
|
"Handoff:",
|
|
236
243
|
"- Send fixable bugs to implementation lead.",
|
|
237
244
|
"- Send suspicious root-cause patterns to debug-investigator.",
|
|
245
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
238
246
|
].join("\n"),
|
|
239
247
|
},
|
|
240
248
|
{
|
|
@@ -262,6 +270,7 @@ const BUILTIN_PROFILES = [
|
|
|
262
270
|
"",
|
|
263
271
|
"Handoff:",
|
|
264
272
|
"- Send confirmed cause and fix guidance to implementation lead.",
|
|
273
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
265
274
|
].join("\n"),
|
|
266
275
|
},
|
|
267
276
|
{
|
|
@@ -288,6 +297,7 @@ const BUILTIN_PROFILES = [
|
|
|
288
297
|
"Handoff:",
|
|
289
298
|
"- Send blockers back to the responsible agent.",
|
|
290
299
|
"- Send the final readiness note to the human operator.",
|
|
300
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
291
301
|
].join("\n"),
|
|
292
302
|
},
|
|
293
303
|
{
|
|
@@ -313,6 +323,7 @@ const BUILTIN_PROFILES = [
|
|
|
313
323
|
"",
|
|
314
324
|
"Handoff:",
|
|
315
325
|
"- Send the architect and builder a short ordered plan with explicit blockers.",
|
|
326
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
316
327
|
].join("\n"),
|
|
317
328
|
},
|
|
318
329
|
{
|
|
@@ -338,6 +349,7 @@ const BUILTIN_PROFILES = [
|
|
|
338
349
|
"",
|
|
339
350
|
"Handoff:",
|
|
340
351
|
"- Send a concise findings brief and source list to the next agent.",
|
|
352
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
341
353
|
].join("\n"),
|
|
342
354
|
},
|
|
343
355
|
{
|
|
@@ -363,6 +375,7 @@ const BUILTIN_PROFILES = [
|
|
|
363
375
|
"",
|
|
364
376
|
"Handoff:",
|
|
365
377
|
"- Send the prototype status, evidence, and remaining gaps to the next agent.",
|
|
378
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
|
|
366
379
|
].join("\n"),
|
|
367
380
|
},
|
|
368
381
|
];
|