@thurstonsand/pi-agent-switchboard 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/index.js +56 -57
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.0] - 2026-11-05
6
+
7
+ Schema v10: update swb and the pi line together.
8
+
9
+ ### Changed
10
+
11
+ - swb's tools and their arguments take names that say what the agent is choosing, all in snake_case: `session_reachable` is `session_list`, `session_ask` is `session_recall`, and `session_schedules` splits into `session_list_schedules` and `session_unschedule`. `session_launch`'s `goal` is `briefing`; `requestResponse` is `report_back`; a schedule's `label` is `schedule_label`; `swb_update_session`'s `force` is `cancel_turns` and `deleteWorktree` is `delete_worktree`; and the reader's `recall_read` takes `tool_output: trimmed | whole` in place of `body: preview | full`.
12
+ - The CLI follows: `swb reachable` is `swb list`, `swb schedules` is `swb list-schedules`, `--request-response` is `--report-back`, and `--label` is `--schedule-label`.
13
+ - swb's e2e runs pi-sessions 0.23.0, which takes the same names for its own tools.
14
+
15
+ ### Fixed
16
+
17
+ - A Claude Code session that snoozes itself until its next message, or archives itself, mid-turn stays that way. Its tool call could land before swb recorded the turn's start, and that record then counted as the next prompt, ending the snooze or undoing the archive.
18
+ - A working session no longer moves to Inactive when its turn outlasts `inactive_after`. Resolving a long-held attention prompt could hide the session and switch the Stage away before its reply landed, leaving that reply Unseen.
19
+ - Codex errors no longer go unmarked when its transcript writes lag behind its live status. swb records the error when Codex reports it, before settling the turn, instead of checking a history entry that may not yet contain it.
20
+ - Claude Code on a machine with managed settings takes a title set through swb with its next prompt, as everywhere else, and a conversation swb resumed in its Project root says so, to you and to the model. Its built-in `cc-plugin-sec-default` mod skipped the line's last two classic hooks, which carried both, and posted a notice for each; the line now hands them over through command hooks, which that mod leaves alone.
21
+ - In Claude Code, `session_recall` returns as soon as its reader starts, and the reader's answer arrives as a message once it's done, as a subagent's does. A Claude Code session in auto mode no longer gets the answer twice: Claude Code made the reader report through SubagentHandback, which posted the answer beside the tool's result.
22
+
5
23
  ## [0.11.0] - 2026-11-03
6
24
 
7
25
  Schema v9: update swb and the pi line together.
package/index.js CHANGED
@@ -90,7 +90,7 @@ class Db {
90
90
  }
91
91
 
92
92
  // packages/shared/src/index.ts
93
- var SCHEMA_VERSION = 9;
93
+ var SCHEMA_VERSION = 10;
94
94
  var VIEWS = ["agent", "editor", "split"];
95
95
  var HARNESSES = ["pi", "claude", "codex"];
96
96
  var SNOOZE_PRESETS = ["one_hour", "one_day", "monday", "next_message"];
@@ -309,14 +309,14 @@ function harnessName(harness) {
309
309
  return HARNESS_NAMES[harness];
310
310
  }
311
311
  function pendingFor(db, targetId) {
312
- return db.all(`SELECT id, kind, source_id, target_id, body, request_response, created_at, schedule_id, scheduled_for FROM messages
312
+ return db.all(`SELECT id, kind, source_id, target_id, body, report_back, created_at, schedule_id, scheduled_for FROM messages
313
313
  WHERE target_id = ? AND delivered_at IS NULL AND cancelled_at IS NULL ORDER BY created_at, rowid`, targetId).map((row) => ({
314
314
  id: row.id,
315
315
  kind: row.kind,
316
316
  sourceId: row.source_id,
317
317
  targetId: row.target_id,
318
318
  body: row.body,
319
- requestResponse: row.request_response === 1,
319
+ reportBack: row.report_back === 1,
320
320
  createdAt: row.created_at,
321
321
  scheduleId: row.schedule_id,
322
322
  scheduledFor: row.scheduled_for
@@ -346,9 +346,9 @@ function frame(db, m, now = Date.now()) {
346
346
  if (m.scheduleId)
347
347
  when += ` (schedule: ${m.scheduleId})`;
348
348
  }
349
- const reply = m.requestResponse ? `
349
+ const reply = m.reportBack ? `
350
350
 
351
- Response requested: reply with session_send_message to session ${m.sourceId}.` : "";
351
+ Report back: reply with session_send_message to session ${m.sourceId}.` : "";
352
352
  return `Message from session "${from.title}" (session: ${m.sourceId}${who})${when}:
353
353
 
354
354
  ${m.body}${reply}`;
@@ -358,12 +358,12 @@ ${m.body}${reply}`;
358
358
  import { Type } from "typebox";
359
359
  import { Value } from "typebox/value";
360
360
  var session = Type.String({ description: "The session's id." });
361
- var requestResponse = Type.Boolean({ description: "Whether it should reply to you." });
361
+ var reportBack = Type.Boolean({ description: "Whether it should reply to you." });
362
362
  var TOOLS = {
363
- session_reachable: {
364
- label: "Reachable sessions",
363
+ session_list: {
364
+ label: "Sessions",
365
365
  description: "List every open session but this one, in any project or harness.",
366
- guidelines: ["Call session_reachable only for an id you don't already have."],
366
+ guidelines: ["Call session_list only for an id you don't already have."],
367
367
  parameters: Type.Object({}),
368
368
  readOnly: true
369
369
  },
@@ -374,13 +374,13 @@ var TOOLS = {
374
374
  parameters: Type.Object({
375
375
  session,
376
376
  message: Type.String({ description: "Self-contained: the recipient doesn't see this conversation." }),
377
- requestResponse
377
+ report_back: reportBack
378
378
  }),
379
379
  readOnly: false
380
380
  },
381
381
  session_launch: {
382
382
  label: "Launch",
383
- description: "Start a new session on one of the presets below. With at or cron and a label, as session_schedule takes them, launch later instead, or anew each time the cron fires.",
383
+ description: "Start a new session on one of the presets below. With at or cron and a schedule_label, as session_schedule takes them, launch later instead, or anew each time the cron fires.",
384
384
  guidelines: [
385
385
  "Launch a new session when the work is independent of this one, belongs to another directory, or the user asks. For part of this work you need back this turn, use your harness's own subagents, if it has them.",
386
386
  "Omit session_launch's preset fields to run what this session runs; otherwise pass the fields of the preset whose note fits the work."
@@ -390,18 +390,18 @@ var TOOLS = {
390
390
  model: Type.Optional(Type.String()),
391
391
  effort: Type.Optional(Type.String()),
392
392
  agent: Type.Optional(Type.String()),
393
- goal: Type.String({
394
- description: "Its first message: a self-contained briefing of the objective, context, constraints, and what done means."
393
+ briefing: Type.String({
394
+ description: "Its first message, self-contained: the objective, context, constraints, and what done means."
395
395
  }),
396
396
  cwd: Type.Optional(Type.String({ description: "Defaults to this session's." })),
397
397
  title: Type.String({ description: "Short, for the roster." }),
398
398
  group: Type.Optional(Type.Union([Type.String(), Type.Null()], {
399
399
  description: "Omit for this session's Group within its Project, or pass a Group's name, in any Project; null for none."
400
400
  })),
401
- requestResponse: Type.Boolean({ description: "Whether it reports back when done." }),
401
+ report_back: Type.Boolean({ description: "Whether it reports back when done." }),
402
402
  at: Type.Optional(Type.String()),
403
403
  cron: Type.Optional(Type.String()),
404
- label: Type.Optional(Type.String())
404
+ schedule_label: Type.Optional(Type.String())
405
405
  }),
406
406
  readOnly: false
407
407
  },
@@ -409,7 +409,7 @@ var TOOLS = {
409
409
  label: "Schedule",
410
410
  description: "Schedule a message, once or on a cron, to this session or another. session_launch launches on a schedule.",
411
411
  guidelines: [
412
- "Schedule only when the user asks to remind, follow up, check back, or repeat something. To wait on another session, set requestResponse on your message instead.",
412
+ "Schedule only when the user asks to remind, follow up, check back, or repeat something. To wait on another session, set report_back on your message instead.",
413
413
  "Write a scheduled message as instructions for when it fires: what to check, what to do with each outcome, and, if recurring, when to unschedule."
414
414
  ],
415
415
  parameters: Type.Object({
@@ -418,20 +418,25 @@ var TOOLS = {
418
418
  description: 'Once: an ISO 8601 time, local unless it has an offset ("2026-10-23T15:00"), or a duration from now ("PT2H").'
419
419
  })),
420
420
  cron: Type.Optional(Type.String({ description: 'Recurring: 5-field cron in local time ("57 8 * * 1-5" is weekdays at 8:57am).' })),
421
- label: Type.String({ description: 'When it runs, in plain words ("Weekdays, 8:57 AM").' }),
421
+ schedule_label: Type.String({ description: 'When it runs, in plain words ("Weekdays, 8:57 AM").' }),
422
422
  session: Type.Optional(Type.String({ description: "The recipient's id; omit for this session." })),
423
- requestResponse: Type.Optional(requestResponse),
423
+ report_back: Type.Optional(reportBack),
424
424
  snooze: Type.Optional(Type.Boolean({ description: "Snooze the recipient until it fires; only when the user asks." }))
425
425
  }),
426
426
  readOnly: false
427
427
  },
428
- session_schedules: {
428
+ session_list_schedules: {
429
429
  label: "Schedules",
430
- description: "List the schedules this session set or receives, or cancel one.",
430
+ description: "List the schedules this session set or receives.",
431
+ guidelines: [],
432
+ parameters: Type.Object({}),
433
+ readOnly: true
434
+ },
435
+ session_unschedule: {
436
+ label: "Unschedule",
437
+ description: "Cancel a pending schedule set on this session.",
431
438
  guidelines: ["Unschedule only when the user asks, or when the schedule's stated completion condition is met."],
432
- parameters: Type.Object({
433
- unschedule: Type.Optional(Type.String({ description: "A pending schedule's id, to cancel it." }))
434
- }),
439
+ parameters: Type.Object({ schedule: Type.String({ description: "The schedule's id." }) }),
435
440
  readOnly: false
436
441
  },
437
442
  swb_update_session: {
@@ -439,18 +444,18 @@ var TOOLS = {
439
444
  description: "Archive, snooze, group, retitle, or reload a session, or cancel its turn: this one unless you name another.",
440
445
  guidelines: [
441
446
  "Archive a session only when the user asks or you started it and drove it to completion. Snooze or unsnooze a session, or cancel another's turn, only when the user asks.",
442
- "Group sessions when the user asks or to keep sessions you started for one piece of work together, reusing a Group session_reachable lists.",
447
+ "Group sessions when the user asks or to keep sessions you started for one piece of work together, reusing a Group session_list lists.",
443
448
  "Retitle a session when the user asks or its title no longer fits its work.",
444
449
  "Reload a session when the user asks or after changing what it reads only at startup (AGENTS.md, CLAUDE.md, settings, extensions, plugins, skills, MCP servers)."
445
450
  ],
446
451
  parameters: Type.Object({
447
452
  session: Type.Optional(session),
448
453
  archived: Type.Optional(Type.Boolean({ description: "false unarchives." })),
449
- deleteWorktree: Type.Optional(Type.Boolean({
454
+ delete_worktree: Type.Optional(Type.Boolean({
450
455
  description: "With archived true, for this session only: delete its linked worktree, keeping the branch and conversations. With family true, archive its descendants too, but delete only this worktree. Only when the user asks. Refuses modified or untracked files, locked worktrees, scheduled launches into it, or open/live sessions using it outside the selected family. Ignored files are deleted. Conversations whose directory is gone resume in the Project root, or fail clearly if that is gone too. Call last, then end your turn."
451
456
  })),
452
457
  family: Type.Optional(Type.Boolean({ description: "With archived: every open session it started, and theirs, too." })),
453
- force: Type.Optional(Type.Boolean({ description: "With archived: also those mid-turn, blocked, cut off, or errored, cancelling their turns." })),
458
+ cancel_turns: Type.Optional(Type.Boolean({ description: "With archived: also archive those mid-turn, blocked, cut off, or errored, cancelling their turns." })),
454
459
  cancel: Type.Optional(Type.Boolean({ description: "Stop another session's running turn." })),
455
460
  group: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "The Group to move it into, in its Project; null takes it out." })),
456
461
  title: Type.Optional(Type.String()),
@@ -468,7 +473,7 @@ var TOOLS = {
468
473
  description: "Search other sessions' conversations, open or archived, subagents included. Lists matches best first, with passages.",
469
474
  guidelines: [
470
475
  'In session_search, plain words must all appear, as words or their starts; "phrases" match exactly; OR, -word, and parentheses narrow. Filters: harness:pi|claude|codex, project:NAME, file:PATH (read or changed), after: and before: (a date, an ISO 8601 time, or a duration ago like P7D), archived:yes|no. Filters alone list recent sessions.',
471
- "Question a session_search hit with session_ask."
476
+ "Question a session_search hit with session_recall."
472
477
  ],
473
478
  parameters: Type.Object({
474
479
  query: Type.String({ description: 'Such as "redirect loop" file:src/auth.ts after:P14D.' }),
@@ -476,10 +481,10 @@ var TOOLS = {
476
481
  }),
477
482
  readOnly: true
478
483
  },
479
- session_ask: {
480
- label: "Ask session",
484
+ session_recall: {
485
+ label: "Recall session",
481
486
  description: "Ask about another session's conversation, open or archived. A reader with its own context answers, keeping the transcript out of yours.",
482
- guidelines: ["Ask session_ask focused questions, not for a recap. To ask a session still at work, message it instead."],
487
+ guidelines: ["Ask session_recall focused questions, not for a recap. To ask a session still at work, message it instead."],
483
488
  parameters: Type.Object({ session, question: Type.String({ description: "Self-contained." }) }),
484
489
  readOnly: true
485
490
  },
@@ -506,15 +511,15 @@ var READER_TOOLS = {
506
511
  },
507
512
  recall_read: {
508
513
  label: "Read session",
509
- description: "Read the session's entries in order, from one ref along its branch: prompts, replies, tool calls and their results, summaries, and how a turn ended. Long results are cut in preview; full reads them whole.",
514
+ description: "Read the session's entries in order, from one ref along its branch: prompts, replies, tool calls and their results, summaries, and how a turn ended. trimmed cuts long ones short; whole reads them whole.",
510
515
  guidelines: [],
511
516
  parameters: Type.Object({
512
517
  ref: Type.Optional(Type.String({ description: "The entry to start at, or its first few characters; the session's first by default." })),
513
518
  to: Type.Optional(Type.String({ description: "The entry to stop at, later on the same branch." })),
514
519
  before: Type.Optional(Type.Integer({ minimum: 0, maximum: 100, description: "Entries to include before ref." })),
515
520
  after: Type.Optional(Type.Integer({ minimum: 0, maximum: 500, description: "Entries to read after ref; as many as fit by default." })),
516
- body: Type.Optional(Type.Enum(["preview", "full"], {
517
- description: "preview (the default) cuts tool calls and results short; full reads them whole."
521
+ tool_output: Type.Optional(Type.Enum(["trimmed", "whole"], {
522
+ description: "trimmed (the default) cuts tool calls, their results, and harness context short; whole reads them whole."
518
523
  }))
519
524
  }),
520
525
  readOnly: true
@@ -532,7 +537,7 @@ Rules:
532
537
  - End with your answer as your final message: self-contained, with the files that matter by absolute path.`;
533
538
  var ALL = { ...TOOLS, ...READER_TOOLS };
534
539
  function toolParameters(name, worktree) {
535
- return name === "swb_update_session" && !worktree ? Type.Omit(TOOLS.swb_update_session.parameters, ["deleteWorktree"]) : TOOLS[name].parameters;
540
+ return name === "swb_update_session" && !worktree ? Type.Omit(TOOLS.swb_update_session.parameters, ["delete_worktree"]) : TOOLS[name].parameters;
536
541
  }
537
542
 
538
543
  // packages/shared/src/trace.ts
@@ -856,12 +861,12 @@ ${details.body}`, 0, 0);
856
861
  };
857
862
  const self = (ctx) => ctx.sessionManager.getSessionId();
858
863
  const titleOf2 = (id) => typeof id === "string" ? titleOf(connect(), id) ?? id : "";
859
- tool("session_reachable", () => "", (_args, ctx) => swb(proc.launch.bin, ["reachable", "--from", self(ctx)]));
864
+ tool("session_list", () => "", (_args, ctx) => swb(proc.launch.bin, ["list", "--from", self(ctx)]));
860
865
  tool("session_search", (args) => String(args.query ?? ""), (args, ctx, _toolCallId, signal) => swb(proc.launch.bin, ["search", "--from", self(ctx), ...args.limit ? ["--limit", String(args.limit)] : []], args.query, signal));
861
- tool("session_ask", (args) => titleOf2(args.session), async (args, ctx, _toolCallId, signal) => {
866
+ tool("session_recall", (args) => titleOf2(args.session), async (args, ctx, _toolCallId, signal) => {
862
867
  signal?.throwIfAborted();
863
868
  if (!ctx.model)
864
- throw new Error("No active model is available for session_ask");
869
+ throw new Error("No active model is available for session_recall");
865
870
  const first = await swb(proc.launch.bin, ["recall", "begin", args.session], JSON.stringify({ question: args.question }), signal);
866
871
  const { createAgentSession, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager } = await import("@earendil-works/pi-coding-agent");
867
872
  const modelRuntime = Reflect.get(ctx.modelRegistry, "runtime");
@@ -940,17 +945,8 @@ ${details.body}`, 0, 0);
940
945
  }
941
946
  }
942
947
  });
943
- tool("session_send_message", (args) => titleOf2(args.session), (args, ctx, toolCallId) => swb(proc.launch.bin, [
944
- "send",
945
- "--from",
946
- self(ctx),
947
- "--to",
948
- args.session,
949
- "--request-id",
950
- toolCallId,
951
- ...args.requestResponse ? ["--request-response"] : []
952
- ], args.message));
953
- tool("session_launch", (args) => `${[args.harness, args.agent, args.model].filter(Boolean).join(" ") || "default"} · ${args.title}${args.label ? ` · ${args.label}` : ""}`, (args, ctx, toolCallId) => swb(proc.launch.bin, [
948
+ tool("session_send_message", (args) => titleOf2(args.session), (args, ctx, toolCallId) => swb(proc.launch.bin, ["send", "--from", self(ctx), "--to", args.session, "--request-id", toolCallId, ...args.report_back ? ["--report-back"] : []], args.message));
949
+ tool("session_launch", (args) => `${[args.harness, args.agent, args.model].filter(Boolean).join(" ") || "default"} · ${args.title}${args.schedule_label ? ` · ${args.schedule_label}` : ""}`, (args, ctx, toolCallId) => swb(proc.launch.bin, [
954
950
  "launch",
955
951
  ...["harness", "model", "effort", "agent"].flatMap((k) => args[k] ? [`--${k}`, args[k]] : []),
956
952
  "--from",
@@ -960,31 +956,34 @@ ${details.body}`, 0, 0);
960
956
  "--title",
961
957
  args.title,
962
958
  ...groupFlags(args.group),
963
- ...args.requestResponse ? ["--request-response"] : [],
964
- ...["at", "cron", "label"].flatMap((k) => args[k] !== undefined ? [`--${k}`, args[k]] : []),
959
+ ...args.report_back ? ["--report-back"] : [],
960
+ ...args.at !== undefined ? ["--at", args.at] : [],
961
+ ...args.cron !== undefined ? ["--cron", args.cron] : [],
962
+ ...args.schedule_label !== undefined ? ["--schedule-label", args.schedule_label] : [],
965
963
  "--request-id",
966
964
  toolCallId
967
- ], args.goal));
968
- tool("session_schedule", (args) => `${args.session ? titleOf2(args.session) : "this session"} · ${args.label}${args.snooze ? " · snooze" : ""}`, (args, ctx, toolCallId) => swb(proc.launch.bin, [
965
+ ], args.briefing));
966
+ tool("session_schedule", (args) => `${args.session ? titleOf2(args.session) : "this session"} · ${args.schedule_label}${args.snooze ? " · snooze" : ""}`, (args, ctx, toolCallId) => swb(proc.launch.bin, [
969
967
  "schedule",
970
968
  "--from",
971
969
  self(ctx),
972
- "--label",
973
- args.label,
970
+ "--schedule-label",
971
+ args.schedule_label,
974
972
  "--request-id",
975
973
  toolCallId,
976
974
  ...args.at !== undefined ? ["--at", args.at] : [],
977
975
  ...args.cron !== undefined ? ["--cron", args.cron] : [],
978
976
  ...args.session !== undefined ? ["--to", args.session] : [],
979
- ...args.requestResponse ? ["--request-response"] : [],
977
+ ...args.report_back ? ["--report-back"] : [],
980
978
  ...args.snooze ? ["--snooze"] : []
981
979
  ], args.message));
982
- tool("session_schedules", (args) => args.unschedule ? `unschedule ${args.unschedule}` : "", (args, ctx) => swb(proc.launch.bin, args.unschedule ? ["unschedule", args.unschedule, "--from", self(ctx)] : ["schedules", "--from", self(ctx)]));
980
+ tool("session_list_schedules", () => "", (_args, ctx) => swb(proc.launch.bin, ["list-schedules", "--from", self(ctx)]));
981
+ tool("session_unschedule", (args) => String(args.schedule ?? ""), (args, ctx) => swb(proc.launch.bin, ["unschedule", args.schedule, "--from", self(ctx)]));
983
982
  tool("swb_show_session", (args) => `${titleOf2(args.session)}${args.view ? ` · ${args.view}` : ""}`, (args, ctx) => swb(proc.launch.bin, ["show", args.session, "--from", self(ctx), ...args.view ? ["--view", args.view] : []]));
984
983
  const snoozeText = (snooze) => typeof snooze === "string" ? snooze : snooze.until;
985
984
  tool("swb_update_session", (args) => {
986
985
  const parts = [
987
- args.archived === true ? `archive${args.family ? " family" : ""}${args.force ? " · force" : ""}` : "",
986
+ args.archived === true ? `archive${args.family ? " family" : ""}${args.cancel_turns ? " · cancel turns" : ""}` : "",
988
987
  args.archived === false ? "unarchive" : "",
989
988
  args.cancel ? "cancel turn" : "",
990
989
  args.snooze !== undefined ? `snooze ${snoozeText(args.snooze)}` : "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thurstonsand/pi-agent-switchboard",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "Records managed pi sessions for Agent Switchboard (swb)",
6
6
  "license": "MIT",