agentp 2.0.0 → 2.0.1
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 +33 -5
- package/bin/agentp +226 -47
- package/bin/ocmux +431 -76
- package/docs/specification_v2.md +43 -2
- package/lib/project-state.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -214,7 +214,8 @@ The ticket is `agentp_ticket` followed by a JSON object with these fields:
|
|
|
214
214
|
- `ctime` — creation timestamp (ISO 8601). Only used to compute `elapsed`.
|
|
215
215
|
- `path` — path to the temp file holding the result.
|
|
216
216
|
- `server` — OpenCode server URL used by the deferred job.
|
|
217
|
-
- `sessionId` — OpenCode session ID used by
|
|
217
|
+
- `sessionId` — OpenCode session ID used by a normal deferred job.
|
|
218
|
+
- `sessionIds` — array of OpenCode session IDs used by a **broadcast** deferred job (replaces `sessionId`).
|
|
218
219
|
- `elapsed` — seconds since `ctime`, included only when the ticket is re-printed (not on first print).
|
|
219
220
|
- `defer` — the timeout requested at submission, included only when it was > 0.
|
|
220
221
|
- `cancelled` — always present (defaults to `false`). Set it to `true` and pipe the ticket back to cancel the running job (see below).
|
|
@@ -229,6 +230,13 @@ printf "Refactor the auth module" | agentp --defer --onlineTicket
|
|
|
229
230
|
|
|
230
231
|
Both formats are accepted when piping a ticket back to `agentp --defer`.
|
|
231
232
|
|
|
233
|
+
When `ocmux` has a broadcast selection, `agentp --defer` creates a ticket with
|
|
234
|
+
`sessionIds` and sends the prompt to every selected session. On retrieval,
|
|
235
|
+
`--qa` includes the original prompt plus one `💬 <name> [<id>]` answer section
|
|
236
|
+
per session. Individual failures/incomplete sessions are listed with an error
|
|
237
|
+
and timestamp. Broadcast tickets do **not** support queued follow-up text (a
|
|
238
|
+
follow-up is inherently ambiguous across multiple sessions).
|
|
239
|
+
|
|
232
240
|
You can also append follow-up text after a not-yet-ready ticket to queue more
|
|
233
241
|
input into the original running task, similar to typing into the OpenCode TUI
|
|
234
242
|
while the agent is busy:
|
|
@@ -328,10 +336,25 @@ Without arguments (and with a TTY), opens an **interactive session picker** for
|
|
|
328
336
|
the project found upward from `<directory>` (default: `$PWD`):
|
|
329
337
|
|
|
330
338
|
- sessions are listed most-recently-viewed first
|
|
331
|
-
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
`
|
|
339
|
+
- `/` starts an **incremental search** of the list (matches title/id,
|
|
340
|
+
case-insensitive, space-separated tokens are ANDed). While the search line is
|
|
341
|
+
active it shows `Enter: confirm · Esc: cancel` on the right: `Enter` keeps the
|
|
342
|
+
filter and returns to normal navigation, `ESC` clears it, `/` resumes editing
|
|
343
|
+
it, and `Backspace` on an already-empty search also exits it. The same `/`
|
|
344
|
+
search works in the model/agent pickers and the project switcher.
|
|
345
|
+
- `Enter` switches (menu stays open). `Space` over a different session enters
|
|
346
|
+
**broadcast mode**: Space selects/deselects sessions; `Enter` keeps the list
|
|
347
|
+
and switches normally; `ESC`/`q` cancels back to the session list and returns
|
|
348
|
+
the TUI to the stored session; deselecting down to a single session selects
|
|
349
|
+
that remaining session. New selections open in the TUI for inspection.
|
|
350
|
+
Broadcast mode also has `h` help, `d` delete, and `m` to change the model for
|
|
351
|
+
every selected session.
|
|
352
|
+
- `n` create (name input) · `r` rename (edit in place) · `R` set a reminder ·
|
|
353
|
+
`d` delete (confirm) · `a` switch agent · `m` switch model · `p` project
|
|
354
|
+
switcher · `h` help · `q` quit
|
|
355
|
+
- **`q` only quits the session picker.** In every other menu (model/agent
|
|
356
|
+
pickers, project switcher, help/input prompts) `q`/`ESC` just closes that menu
|
|
357
|
+
and returns to the previous one. **`Ctrl+C` fully exits** `ocmux` from any menu.
|
|
335
358
|
- switching updates `.ocmux.json` and relaunches the TUI on the chosen session
|
|
336
359
|
(`opencode --server <url> --session <id>`); silent on success
|
|
337
360
|
- new sessions inherit the model of the previously selected session (v2
|
|
@@ -340,6 +363,11 @@ the project found upward from `<directory>` (default: `$PWD`):
|
|
|
340
363
|
Reminders (`R`) are stored in the `.ocmux.json` `annotations` map; `agentp`
|
|
341
364
|
prepends a session's reminder to every prompt sent to it.
|
|
342
365
|
|
|
366
|
+
Broadcast selections are stored in `.ocmux.json` as `broadcast`. `agentp`
|
|
367
|
+
sends a prompt to all selected sessions (waiting for busy sessions to go idle),
|
|
368
|
+
and prints a labeled answer section for each. Errors/incomplete targets are
|
|
369
|
+
reported individually with session name, id, error, and timestamp.
|
|
370
|
+
|
|
343
371
|
Subcommands:
|
|
344
372
|
|
|
345
373
|
- **`serve [--server <url>] [--git|--GIT] [dir]`** — create a project TUI window
|
package/bin/agentp
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
selectSession,
|
|
19
19
|
sortSessionsByRecency,
|
|
20
20
|
interruptSession,
|
|
21
|
+
getActiveSessions,
|
|
21
22
|
} = require('../lib/opencode');
|
|
22
23
|
const projectState = require('../lib/project-state');
|
|
23
24
|
const ocmuxLib = require('../lib/ocmux');
|
|
@@ -168,7 +169,7 @@ function parseDeferredTicketInput(text) {
|
|
|
168
169
|
if (!data || typeof data.path !== 'string' || !data.path) return null;
|
|
169
170
|
const rest = afterPrefix.slice(jsonEnd);
|
|
170
171
|
const followupText = rest.trim() ? stripFollowupRow(rest.trimStart()) : '';
|
|
171
|
-
|
|
172
|
+
const ticket = {
|
|
172
173
|
ctime: typeof data.ctime === 'string' ? data.ctime : null,
|
|
173
174
|
path: data.path,
|
|
174
175
|
defer: typeof data.defer === 'number' && data.defer > 0 ? data.defer : 0,
|
|
@@ -177,6 +178,10 @@ function parseDeferredTicketInput(text) {
|
|
|
177
178
|
cancelled: data.cancelled === true,
|
|
178
179
|
followupText,
|
|
179
180
|
};
|
|
181
|
+
if (Array.isArray(data.sessionIds)) {
|
|
182
|
+
ticket.sessionIds = data.sessionIds.filter(x => typeof x === 'string' && x);
|
|
183
|
+
}
|
|
184
|
+
return ticket;
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
function parseDeferredTicket(text) {
|
|
@@ -193,6 +198,7 @@ function formatTicket(ticket, elapsed, compact) {
|
|
|
193
198
|
const fields = { ctime: ticket.ctime, path: ticket.path };
|
|
194
199
|
if (ticket.server) fields.server = ticket.server;
|
|
195
200
|
if (ticket.sessionId) fields.sessionId = ticket.sessionId;
|
|
201
|
+
if (Array.isArray(ticket.sessionIds) && ticket.sessionIds.length > 0) fields.sessionIds = ticket.sessionIds;
|
|
196
202
|
if (elapsed !== undefined) fields.elapsed = elapsed;
|
|
197
203
|
if (ticket.defer > 0) fields.defer = ticket.defer;
|
|
198
204
|
// Always present so it can be flipped to true and piped back to cancel.
|
|
@@ -204,8 +210,12 @@ function formatTicket(ticket, elapsed, compact) {
|
|
|
204
210
|
// the ticket. Prints a message and exits 0.
|
|
205
211
|
async function cancelTicket(ticket) {
|
|
206
212
|
let interrupted = false;
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
const ids = Array.isArray(ticket.sessionIds) && ticket.sessionIds.length > 0
|
|
214
|
+
? ticket.sessionIds
|
|
215
|
+
: (ticket.sessionId ? [ticket.sessionId] : []);
|
|
216
|
+
if (ticket.server && ids.length > 0) {
|
|
217
|
+
const interruptedAll = await Promise.all(ids.map(id => interruptSession(ticket.server, id)));
|
|
218
|
+
interrupted = interruptedAll.some(Boolean);
|
|
209
219
|
}
|
|
210
220
|
try { fs.unlinkSync(ticket.path); } catch {}
|
|
211
221
|
try { fs.unlinkSync(ticket.path + '.lock'); } catch {}
|
|
@@ -269,7 +279,9 @@ async function resolveTargetSession(serverBase, sessionName, newSession, opts =
|
|
|
269
279
|
}
|
|
270
280
|
|
|
271
281
|
if (sessions && sessions.length > 0) {
|
|
272
|
-
|
|
282
|
+
// Only "main" sessions (no parentID) are considered as implicit targets —
|
|
283
|
+
// child/subagent sessions are work products, not conversation targets.
|
|
284
|
+
const sorted = sortSessionsByRecency(sessions).filter(s => s.id && !s.parentID);
|
|
273
285
|
if (sorted.length > 0) return sorted[0].id;
|
|
274
286
|
}
|
|
275
287
|
|
|
@@ -414,6 +426,114 @@ function outputReadyResult(filePath, result, qaMode, unusedFollowupText, header)
|
|
|
414
426
|
process.exit(0);
|
|
415
427
|
}
|
|
416
428
|
|
|
429
|
+
// ── Broadcast (ocmux "Space" multi-session send) ───────────────────
|
|
430
|
+
|
|
431
|
+
// One answer section: a heading with the session name and a separator below it.
|
|
432
|
+
function buildBroadcastSection(title, id, answer) {
|
|
433
|
+
let s = `\n💬 ${title} [${id}]\n${'━'.repeat(40)}\n`;
|
|
434
|
+
s += answer;
|
|
435
|
+
if (!answer.endsWith('\n')) s += '\n';
|
|
436
|
+
return s;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Send a prompt to every session in `ids` and collect the answers.
|
|
440
|
+
// deps.getActiveSessions(server) -> Set of busy session ids
|
|
441
|
+
// deps.getSession(server, id) -> session object (title)
|
|
442
|
+
// deps.sendToSession(server, id, text) -> answer text
|
|
443
|
+
// deps.readBroadcast() -> current .ocmux.json broadcast list (null/[] = cancelled)
|
|
444
|
+
// Waits for each session to become idle before sending; stops as soon as the
|
|
445
|
+
// broadcast list disappears (ocmux exited broadcast mode early).
|
|
446
|
+
async function runBroadcastSend(server, ids, prompt, deps) {
|
|
447
|
+
const { getActiveSessions, getSession, sendToSession, readBroadcast, getPrompt, pollMs = 1000, busyTimeoutMs = 15 * 60 * 1000 } = deps;
|
|
448
|
+
const initial = new Set(ids.map(String));
|
|
449
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
450
|
+
const cancelled = () => {
|
|
451
|
+
try { return !isBroadcastActive(readBroadcast(), initial); } catch { return false; }
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// Race a sent session's answer against ocmux leaving broadcast mode. We do
|
|
455
|
+
// not interrupt the model: we merely stop waiting/discard its later answer,
|
|
456
|
+
// exactly as the UI contract promises.
|
|
457
|
+
const awaitAnswerOrCancel = (promise) => new Promise((resolve) => {
|
|
458
|
+
let done = false;
|
|
459
|
+
const finish = (value) => {
|
|
460
|
+
if (done) return;
|
|
461
|
+
done = true;
|
|
462
|
+
clearInterval(timer);
|
|
463
|
+
resolve(value);
|
|
464
|
+
};
|
|
465
|
+
const timer = setInterval(() => {
|
|
466
|
+
if (cancelled()) finish({ cancelled: true });
|
|
467
|
+
}, pollMs);
|
|
468
|
+
promise.then(answer => finish({ answer }), err => finish({ error: err }));
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const runOne = async (id) => {
|
|
472
|
+
let title = id;
|
|
473
|
+
try {
|
|
474
|
+
const s = await getSession(server, id);
|
|
475
|
+
title = (s && (s.title || s.id)) || id;
|
|
476
|
+
} catch {
|
|
477
|
+
return { missing: { id, title, error: 'Unable to read session', timestamp: new Date().toISOString() } };
|
|
478
|
+
}
|
|
479
|
+
// Wait for the session to become idle (skip prompts into a busy session).
|
|
480
|
+
const deadline = Date.now() + busyTimeoutMs;
|
|
481
|
+
let busy = true;
|
|
482
|
+
while (busy && Date.now() < deadline) {
|
|
483
|
+
if (cancelled()) {
|
|
484
|
+
return { missing: { id, title, error: 'Broadcast cancelled while waiting for idle', timestamp: new Date().toISOString() } };
|
|
485
|
+
}
|
|
486
|
+
const active = await getActiveSessions(server);
|
|
487
|
+
if (!active.has(String(id))) busy = false;
|
|
488
|
+
else await sleep(pollMs);
|
|
489
|
+
}
|
|
490
|
+
if (busy) {
|
|
491
|
+
return { missing: { id, title, error: 'Session stayed busy until timeout', timestamp: new Date().toISOString() } };
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const result = await awaitAnswerOrCancel(sendToSession(server, id, getPrompt ? getPrompt(id, prompt) : prompt));
|
|
495
|
+
if (result.cancelled) {
|
|
496
|
+
return { missing: { id, title, error: 'Broadcast cancelled before this session replied', timestamp: new Date().toISOString() } };
|
|
497
|
+
}
|
|
498
|
+
if (result.error) {
|
|
499
|
+
return { missing: { id, title, error: result.error.message || String(result.error), timestamp: new Date().toISOString() } };
|
|
500
|
+
}
|
|
501
|
+
return { result: { id, title, answer: result.answer } };
|
|
502
|
+
} catch (err) {
|
|
503
|
+
return { missing: { id, title, error: err.message || String(err), timestamp: new Date().toISOString() } };
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Run independently so one busy/model-limited session never prevents all
|
|
508
|
+
// other selected sessions from receiving the prompt.
|
|
509
|
+
const settled = await Promise.all(ids.map(runOne));
|
|
510
|
+
const results = settled.filter(x => x.result).map(x => x.result);
|
|
511
|
+
const missing = settled.filter(x => x.missing).map(x => x.missing);
|
|
512
|
+
return { results, missing, requested: ids.length };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// A broadcast list is still active while it is an array that contains every
|
|
516
|
+
// session we were asked to reach.
|
|
517
|
+
function isBroadcastActive(broadcast, initial) {
|
|
518
|
+
return Array.isArray(broadcast)
|
|
519
|
+
&& broadcast.length >= 2
|
|
520
|
+
&& [...initial].every((id) => broadcast.includes(id));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Assemble the final broadcast output: heading per session + missing note.
|
|
524
|
+
function formatBroadcastResults({ results, missing, requested }) {
|
|
525
|
+
let out = `🌐 Broadcast to ${requested} sessions\n`;
|
|
526
|
+
for (const r of results) out += buildBroadcastSection(r.title, r.id, r.answer);
|
|
527
|
+
if (missing.length > 0) {
|
|
528
|
+
out += '\n⚠️ Prompt not completed in the following sessions:\n';
|
|
529
|
+
for (const m of missing) {
|
|
530
|
+
out += `- 💬 ${m.title} [${m.id}]\n - Error: ${m.error || 'Unknown'}\n`;
|
|
531
|
+
if (m.timestamp) out += ` - At: ${m.timestamp}\n`;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
417
537
|
// Structured header (project path + session id/title) prepended to --qa output,
|
|
418
538
|
// so a piped answer is self-describing about where it came from.
|
|
419
539
|
async function buildSessionHeader(server, sessionId) {
|
|
@@ -434,7 +554,7 @@ async function buildSessionHeader(server, sessionId) {
|
|
|
434
554
|
|
|
435
555
|
// Header for deferred-ticket retrieval (ticket carries server + sessionId).
|
|
436
556
|
async function ticketHeader(ticket, qaMode) {
|
|
437
|
-
if (qaMode && ticket && ticket.server && ticket.sessionId) {
|
|
557
|
+
if (qaMode && ticket && ticket.server && ticket.sessionId && !(Array.isArray(ticket.sessionIds) && ticket.sessionIds.length >= 2)) {
|
|
438
558
|
try { return await buildSessionHeader(ticket.server, ticket.sessionId); } catch {}
|
|
439
559
|
}
|
|
440
560
|
return '';
|
|
@@ -457,7 +577,7 @@ const FINAL_ROW = ' —————————————————\n';
|
|
|
457
577
|
// Write the prompt to a temp file, lock the output file, and spawn a detached
|
|
458
578
|
// `--defer-child` process that runs the prompt and writes the answer. Returns
|
|
459
579
|
// the ticket describing this deferred run.
|
|
460
|
-
function spawnDeferredChild({ promptText, serverBase, sessionName, newSession, qaMode, tgMode, flushMode, targetSessionId, deferSeconds }) {
|
|
580
|
+
function spawnDeferredChild({ promptText, serverBase, sessionName, newSession, qaMode, tgMode, flushMode, targetSessionId, targetSessionIds, deferSeconds }) {
|
|
461
581
|
const tempPath = generateTempFilename();
|
|
462
582
|
const promptTempPath = tempPath + '.prompt';
|
|
463
583
|
|
|
@@ -471,6 +591,7 @@ function spawnDeferredChild({ promptText, serverBase, sessionName, newSession, q
|
|
|
471
591
|
defer: deferSeconds || 0,
|
|
472
592
|
server: serverBase,
|
|
473
593
|
sessionId: targetSessionId,
|
|
594
|
+
sessionIds: targetSessionIds,
|
|
474
595
|
cancelled: false,
|
|
475
596
|
};
|
|
476
597
|
|
|
@@ -488,7 +609,10 @@ function spawnDeferredChild({ promptText, serverBase, sessionName, newSession, q
|
|
|
488
609
|
childArgs.push('--session', sessionName);
|
|
489
610
|
if (newSession) childArgs.push('--new');
|
|
490
611
|
}
|
|
491
|
-
childArgs.push('--resolved-session-id', targetSessionId);
|
|
612
|
+
if (targetSessionId) childArgs.push('--resolved-session-id', targetSessionId);
|
|
613
|
+
if (Array.isArray(targetSessionIds) && targetSessionIds.length > 0) {
|
|
614
|
+
childArgs.push('--resolved-session-ids', JSON.stringify(targetSessionIds));
|
|
615
|
+
}
|
|
492
616
|
childArgs.push(serverBase);
|
|
493
617
|
|
|
494
618
|
const child = child_process.spawn(process.execPath, childArgs, {
|
|
@@ -522,6 +646,7 @@ async function main() {
|
|
|
522
646
|
let outputFile = null;
|
|
523
647
|
let sessionName = null;
|
|
524
648
|
let resolvedSessionId = null;
|
|
649
|
+
let resolvedSessionIds = null;
|
|
525
650
|
let newSession = false;
|
|
526
651
|
// tgMode: false = disabled, 'auto' = auto-detect, true = explicit --tg
|
|
527
652
|
let tgMode = null;
|
|
@@ -624,6 +749,16 @@ async function main() {
|
|
|
624
749
|
console.error('Error: --resolved-session-id requires a session id');
|
|
625
750
|
process.exit(1);
|
|
626
751
|
}
|
|
752
|
+
} else if (args[i] === '--resolved-session-ids') {
|
|
753
|
+
i++;
|
|
754
|
+
try {
|
|
755
|
+
const ids = JSON.parse(args[i]);
|
|
756
|
+
if (!Array.isArray(ids) || ids.length < 2 || ids.some(id => typeof id !== 'string' || !id)) throw new Error('bad ids');
|
|
757
|
+
resolvedSessionIds = ids;
|
|
758
|
+
} catch {
|
|
759
|
+
console.error('Error: --resolved-session-ids requires a JSON array of at least two session ids');
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
627
762
|
} else if (args[i] === '--new') {
|
|
628
763
|
newSession = true;
|
|
629
764
|
} else if (args[i] === '--getLast') {
|
|
@@ -844,15 +979,22 @@ async function main() {
|
|
|
844
979
|
}
|
|
845
980
|
|
|
846
981
|
// This is a regular prompt — create deferred execution
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
982
|
+
const broadcastIds = (resolvedSessionIds && resolvedSessionIds.length >= 2)
|
|
983
|
+
? resolvedSessionIds
|
|
984
|
+
: ((ctx && ctx.state && Array.isArray(ctx.state.broadcast) && ctx.state.broadcast.length >= 2)
|
|
985
|
+
? ctx.state.broadcast
|
|
986
|
+
: null);
|
|
987
|
+
let targetSessionId = null;
|
|
988
|
+
if (!broadcastIds) {
|
|
989
|
+
try {
|
|
990
|
+
targetSessionId = await resolveTargetSession(serverBase, sessionName, newSession, {
|
|
991
|
+
directory: projectDir,
|
|
992
|
+
preferredSession,
|
|
993
|
+
});
|
|
994
|
+
} catch (err) {
|
|
995
|
+
console.error(err.message);
|
|
996
|
+
process.exit(1);
|
|
997
|
+
}
|
|
856
998
|
}
|
|
857
999
|
|
|
858
1000
|
const newTicket = spawnDeferredChild({
|
|
@@ -864,6 +1006,7 @@ async function main() {
|
|
|
864
1006
|
tgMode,
|
|
865
1007
|
flushMode,
|
|
866
1008
|
targetSessionId,
|
|
1009
|
+
targetSessionIds: broadcastIds,
|
|
867
1010
|
deferSeconds,
|
|
868
1011
|
});
|
|
869
1012
|
|
|
@@ -938,15 +1081,20 @@ async function main() {
|
|
|
938
1081
|
}
|
|
939
1082
|
|
|
940
1083
|
// Regular prompt: submit via a detached child and wait for the answer.
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1084
|
+
const broadcastIds = (ctx && ctx.state && Array.isArray(ctx.state.broadcast) && ctx.state.broadcast.length >= 2)
|
|
1085
|
+
? ctx.state.broadcast
|
|
1086
|
+
: null;
|
|
1087
|
+
let targetSessionId = null;
|
|
1088
|
+
if (!broadcastIds) {
|
|
1089
|
+
try {
|
|
1090
|
+
targetSessionId = await resolveTargetSession(serverBase, sessionName, newSession, {
|
|
1091
|
+
directory: projectDir,
|
|
1092
|
+
preferredSession,
|
|
1093
|
+
});
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
console.error(err.message);
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
950
1098
|
}
|
|
951
1099
|
|
|
952
1100
|
const newTicket = spawnDeferredChild({
|
|
@@ -958,6 +1106,7 @@ async function main() {
|
|
|
958
1106
|
tgMode,
|
|
959
1107
|
flushMode,
|
|
960
1108
|
targetSessionId,
|
|
1109
|
+
targetSessionIds: broadcastIds,
|
|
961
1110
|
deferSeconds: 0,
|
|
962
1111
|
});
|
|
963
1112
|
|
|
@@ -993,34 +1142,64 @@ async function main() {
|
|
|
993
1142
|
// Wrap main processing in try-catch for deferChildMode to write errors to output file
|
|
994
1143
|
let tgError = null;
|
|
995
1144
|
async function runMain() {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1145
|
+
// Broadcast mode: ocmux's Space key selected multiple sessions in
|
|
1146
|
+
// .ocmux.json — send the prompt to all of them.
|
|
1147
|
+
const broadcastIds = (ctx && ctx.state && Array.isArray(ctx.state.broadcast) && ctx.state.broadcast.length >= 2)
|
|
1148
|
+
? ctx.state.broadcast
|
|
1149
|
+
: null;
|
|
1000
1150
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1151
|
+
let sessionId = null;
|
|
1152
|
+
let answer;
|
|
1153
|
+
if (broadcastIds) {
|
|
1154
|
+
try { if (projectDir) ocmuxLib.focusWindowByDir(projectDir); } catch {}
|
|
1155
|
+
const bres = await runBroadcastSend(serverBase, broadcastIds, actualPromptText, {
|
|
1156
|
+
getActiveSessions,
|
|
1157
|
+
getSession,
|
|
1158
|
+
sendToSession,
|
|
1159
|
+
getPrompt: (id, text) => {
|
|
1160
|
+
try {
|
|
1161
|
+
const ann = ctx && ctx.statefile && projectState.readAnnotations(ctx.statefile)[id];
|
|
1162
|
+
return ann ? ann + '\n\n' + text : text;
|
|
1163
|
+
} catch {
|
|
1164
|
+
return text;
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
readBroadcast: () => (ctx && ctx.statefile ? projectState.readProjectState(ctx.statefile).broadcast : null),
|
|
1168
|
+
});
|
|
1169
|
+
answer = formatBroadcastResults(bres);
|
|
1170
|
+
} else {
|
|
1171
|
+
sessionId = resolvedSessionId || await resolveTargetSession(serverBase, sessionName, newSession, {
|
|
1172
|
+
directory: projectDir,
|
|
1173
|
+
preferredSession,
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// v2: focus the target project's TUI window so the user sees the prompt
|
|
1177
|
+
// stream, and prepend the session's annotation (if any) to the prompt.
|
|
1178
|
+
let prompt = actualPromptText;
|
|
1179
|
+
try {
|
|
1180
|
+
if (ctx && ctx.statefile) {
|
|
1181
|
+
const ann = projectState.readAnnotations(ctx.statefile)[sessionId];
|
|
1182
|
+
if (ann) prompt = ann + '\n\n' + actualPromptText;
|
|
1183
|
+
}
|
|
1184
|
+
if (projectDir) ocmuxLib.focusWindowByDir(projectDir);
|
|
1185
|
+
} catch {
|
|
1186
|
+
// tmux focus is best-effort; never fail the prompt because of it
|
|
1008
1187
|
}
|
|
1009
|
-
if (projectDir) ocmuxLib.focusWindowByDir(projectDir);
|
|
1010
|
-
} catch {
|
|
1011
|
-
// tmux focus is best-effort; never fail the prompt because of it
|
|
1012
|
-
}
|
|
1013
1188
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1189
|
+
try {
|
|
1190
|
+
answer = await sendToSession(serverBase, sessionId, prompt);
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
throw new Error(`Error sending prompt: ${err.message}`);
|
|
1193
|
+
}
|
|
1019
1194
|
}
|
|
1020
1195
|
|
|
1021
1196
|
// Build the message to send to Telegram (QA pair with rulers)
|
|
1022
1197
|
let tgMessage = answer;
|
|
1023
|
-
if (qaMode) {
|
|
1198
|
+
if (qaMode && broadcastIds) {
|
|
1199
|
+
tgMessage = HUMAN_ROW + actualPromptText + AGENT_ROW + answer;
|
|
1200
|
+
if (!answer.endsWith('\n')) tgMessage += '\n';
|
|
1201
|
+
tgMessage += FINAL_ROW;
|
|
1202
|
+
} else if (qaMode && !broadcastIds) {
|
|
1024
1203
|
let header = '';
|
|
1025
1204
|
try { header = await buildSessionHeader(serverBase, sessionId); } catch {}
|
|
1026
1205
|
tgMessage = header + HUMAN_ROW + actualPromptText + AGENT_ROW + answer;
|
|
@@ -1105,4 +1284,4 @@ if (require.main === module) {
|
|
|
1105
1284
|
});
|
|
1106
1285
|
}
|
|
1107
1286
|
|
|
1108
|
-
module.exports = { main, notifyAgentpGateway, readAllStdin, parseDeferredTicket, parseDeferredTicketInput, formatTicket, waitForDeferredResult, waitForDeferredResultForever };
|
|
1287
|
+
module.exports = { main, notifyAgentpGateway, readAllStdin, parseDeferredTicket, parseDeferredTicketInput, formatTicket, waitForDeferredResult, waitForDeferredResultForever, runBroadcastSend, buildBroadcastSection, formatBroadcastResults, isBroadcastActive };
|