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 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 the deferred job.
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
- - `Enter`/`Space` switches (menu stays open) · `n` create (name input) ·
332
- `r` rename (edit in place) · `R` set a reminder · `d` delete (confirm) ·
333
- `a` switch agent · `m` switch model · `p` project switcher · `h` help ·
334
- `q`/`Ctrl+C` quit
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
- return {
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
- if (ticket.server && ticket.sessionId) {
208
- interrupted = await interruptSession(ticket.server, ticket.sessionId);
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
- const sorted = sortSessionsByRecency(sessions).filter(s => s.id);
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
- let targetSessionId;
848
- try {
849
- targetSessionId = await resolveTargetSession(serverBase, sessionName, newSession, {
850
- directory: projectDir,
851
- preferredSession,
852
- });
853
- } catch (err) {
854
- console.error(err.message);
855
- process.exit(1);
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
- let targetSessionId;
942
- try {
943
- targetSessionId = await resolveTargetSession(serverBase, sessionName, newSession, {
944
- directory: projectDir,
945
- preferredSession,
946
- });
947
- } catch (err) {
948
- console.error(err.message);
949
- process.exit(1);
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
- const sessionId = resolvedSessionId || await resolveTargetSession(serverBase, sessionName, newSession, {
997
- directory: projectDir,
998
- preferredSession,
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
- // v2: focus the target project's TUI window so the user sees the prompt
1002
- // stream, and prepend the session's annotation (if any) to the prompt.
1003
- let prompt = actualPromptText;
1004
- try {
1005
- if (ctx && ctx.statefile) {
1006
- const ann = projectState.readAnnotations(ctx.statefile)[sessionId];
1007
- if (ann) prompt = ann + '\n\n' + actualPromptText;
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
- let answer;
1015
- try {
1016
- answer = await sendToSession(serverBase, sessionId, prompt);
1017
- } catch (err) {
1018
- throw new Error(`Error sending prompt: ${err.message}`);
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 };