@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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 (217) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/cli.js +5790 -5731
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/cli-commands.d.ts +12 -0
  7. package/dist/types/commands/launch.d.ts +4 -0
  8. package/dist/types/config/api-key-resolver.d.ts +3 -0
  9. package/dist/types/config/keybindings.d.ts +6 -1
  10. package/dist/types/config/model-registry.d.ts +1 -0
  11. package/dist/types/config/model-resolver.d.ts +18 -0
  12. package/dist/types/config/settings-schema.d.ts +85 -34
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  15. package/dist/types/eval/py/executor.d.ts +5 -0
  16. package/dist/types/eval/py/kernel.d.ts +6 -1
  17. package/dist/types/eval/py/runtime.d.ts +9 -0
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/export/html/template.generated.d.ts +1 -1
  20. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/extensibility/shared-events.d.ts +2 -2
  24. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  25. package/dist/types/internal-urls/index.d.ts +1 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/irc/bus.d.ts +66 -0
  28. package/dist/types/memory-backend/index.d.ts +1 -0
  29. package/dist/types/memory-backend/runtime.d.ts +4 -0
  30. package/dist/types/memory-backend/types.d.ts +66 -1
  31. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  32. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  34. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  35. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  36. package/dist/types/modes/components/welcome.d.ts +3 -9
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  38. package/dist/types/modes/index.d.ts +3 -3
  39. package/dist/types/modes/interactive-mode.d.ts +10 -4
  40. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  41. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  42. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  43. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  44. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  45. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  46. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  47. package/dist/types/modes/theme/theme.d.ts +2 -1
  48. package/dist/types/modes/types.d.ts +5 -2
  49. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  50. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  51. package/dist/types/registry/agent-registry.d.ts +16 -5
  52. package/dist/types/secrets/index.d.ts +1 -1
  53. package/dist/types/secrets/obfuscator.d.ts +8 -2
  54. package/dist/types/session/agent-session.d.ts +49 -32
  55. package/dist/types/session/messages.d.ts +2 -4
  56. package/dist/types/session/session-history-format.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +21 -3
  58. package/dist/types/session/streaming-output.d.ts +46 -0
  59. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  60. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  61. package/dist/types/slash-commands/types.d.ts +1 -1
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +12 -2
  64. package/dist/types/task/index.d.ts +13 -6
  65. package/dist/types/task/output-manager.d.ts +0 -7
  66. package/dist/types/task/repair-args.d.ts +8 -7
  67. package/dist/types/task/types.d.ts +63 -51
  68. package/dist/types/thinking.d.ts +4 -0
  69. package/dist/types/tiny/title-client.d.ts +11 -0
  70. package/dist/types/tiny/title-protocol.d.ts +1 -0
  71. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  72. package/dist/types/tools/find.d.ts +0 -11
  73. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  74. package/dist/types/tools/index.d.ts +7 -3
  75. package/dist/types/tools/irc.d.ts +76 -38
  76. package/dist/types/tools/job.d.ts +7 -1
  77. package/dist/types/utils/git.d.ts +15 -2
  78. package/dist/types/utils/title-generator.d.ts +3 -2
  79. package/examples/extensions/with-deps/package.json +1 -0
  80. package/package.json +11 -10
  81. package/scripts/bundle-dist.ts +28 -19
  82. package/src/async/index.ts +0 -1
  83. package/src/auto-thinking/classifier.ts +1 -0
  84. package/src/cli/args.ts +3 -0
  85. package/src/cli/gallery-cli.ts +1 -1
  86. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  87. package/src/cli/gallery-fixtures/types.ts +5 -0
  88. package/src/cli-commands.ts +29 -0
  89. package/src/cli.ts +28 -15
  90. package/src/commands/launch.ts +4 -0
  91. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  92. package/src/commit/model-selection.ts +3 -2
  93. package/src/config/api-key-resolver.ts +8 -6
  94. package/src/config/keybindings.ts +6 -1
  95. package/src/config/model-registry.ts +97 -30
  96. package/src/config/model-resolver.ts +60 -0
  97. package/src/config/settings-schema.ts +99 -55
  98. package/src/config/settings.ts +68 -3
  99. package/src/edit/hashline/execute.ts +39 -2
  100. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  101. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  102. package/src/eval/agent-bridge.ts +3 -16
  103. package/src/eval/completion-bridge.ts +1 -0
  104. package/src/eval/js/shared/prelude.txt +1 -1
  105. package/src/eval/py/executor.ts +29 -7
  106. package/src/eval/py/index.ts +6 -1
  107. package/src/eval/py/kernel.ts +31 -11
  108. package/src/eval/py/prelude.py +5 -6
  109. package/src/eval/py/runtime.ts +37 -0
  110. package/src/exec/bash-executor.ts +82 -3
  111. package/src/export/html/template.generated.ts +1 -1
  112. package/src/export/html/template.js +38 -13
  113. package/src/extensibility/custom-tools/types.ts +2 -2
  114. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  115. package/src/extensibility/extensions/runner.ts +6 -1
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/shared-events.ts +2 -2
  118. package/src/hindsight/bank.ts +17 -2
  119. package/src/internal-urls/docs-index.generated.ts +11 -11
  120. package/src/internal-urls/history-protocol.ts +113 -0
  121. package/src/internal-urls/index.ts +1 -0
  122. package/src/internal-urls/router.ts +3 -1
  123. package/src/internal-urls/types.ts +1 -1
  124. package/src/irc/bus.ts +292 -0
  125. package/src/main.ts +26 -66
  126. package/src/memories/index.ts +2 -0
  127. package/src/memory-backend/index.ts +1 -0
  128. package/src/memory-backend/local-backend.ts +9 -0
  129. package/src/memory-backend/off-backend.ts +9 -0
  130. package/src/memory-backend/runtime.ts +66 -0
  131. package/src/memory-backend/types.ts +81 -1
  132. package/src/mnemopi/backend.ts +151 -4
  133. package/src/modes/acp/acp-agent.ts +119 -11
  134. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  135. package/src/modes/components/assistant-message.ts +19 -21
  136. package/src/modes/components/compaction-summary-message.ts +68 -32
  137. package/src/modes/components/custom-editor.ts +10 -0
  138. package/src/modes/components/footer.ts +3 -1
  139. package/src/modes/components/status-line/component.ts +118 -34
  140. package/src/modes/components/tool-execution.ts +31 -1
  141. package/src/modes/components/ttsr-notification.ts +72 -30
  142. package/src/modes/components/welcome.ts +9 -33
  143. package/src/modes/controllers/command-controller.ts +1 -1
  144. package/src/modes/controllers/event-controller.ts +65 -0
  145. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  146. package/src/modes/controllers/input-controller.ts +19 -2
  147. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  148. package/src/modes/controllers/selector-controller.ts +21 -17
  149. package/src/modes/index.ts +3 -21
  150. package/src/modes/interactive-mode.ts +47 -22
  151. package/src/modes/oauth-manual-input.ts +30 -3
  152. package/src/modes/rpc/rpc-client.ts +154 -3
  153. package/src/modes/rpc/rpc-mode.ts +97 -12
  154. package/src/modes/rpc/rpc-subagents.ts +265 -0
  155. package/src/modes/rpc/rpc-types.ts +81 -1
  156. package/src/modes/setup-wizard/index.ts +12 -2
  157. package/src/modes/setup-wizard/lazy.ts +16 -0
  158. package/src/modes/theme/theme.ts +18 -5
  159. package/src/modes/types.ts +5 -5
  160. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  161. package/src/modes/utils/ui-helpers.ts +51 -49
  162. package/src/prompts/system/irc-incoming.md +3 -4
  163. package/src/prompts/system/orchestrate-notice.md +2 -2
  164. package/src/prompts/system/subagent-system-prompt.md +0 -5
  165. package/src/prompts/system/system-prompt.md +1 -0
  166. package/src/prompts/system/workflow-notice.md +2 -2
  167. package/src/prompts/tools/eval.md +3 -3
  168. package/src/prompts/tools/irc.md +29 -19
  169. package/src/prompts/tools/read.md +2 -2
  170. package/src/prompts/tools/task-summary.md +5 -16
  171. package/src/prompts/tools/task.md +38 -29
  172. package/src/registry/agent-lifecycle.ts +218 -0
  173. package/src/registry/agent-registry.ts +16 -5
  174. package/src/sdk.ts +37 -10
  175. package/src/secrets/index.ts +8 -1
  176. package/src/secrets/obfuscator.ts +39 -18
  177. package/src/session/agent-session.ts +422 -291
  178. package/src/session/messages.ts +11 -78
  179. package/src/session/session-history-format.ts +246 -0
  180. package/src/session/session-manager.ts +59 -5
  181. package/src/session/streaming-output.ts +226 -10
  182. package/src/slash-commands/acp-builtins.ts +24 -0
  183. package/src/slash-commands/builtin-registry.ts +20 -0
  184. package/src/slash-commands/types.ts +1 -1
  185. package/src/system-prompt.ts +14 -0
  186. package/src/task/executor.ts +851 -461
  187. package/src/task/index.ts +721 -796
  188. package/src/task/output-manager.ts +0 -11
  189. package/src/task/render.ts +148 -63
  190. package/src/task/repair-args.ts +21 -9
  191. package/src/task/types.ts +82 -66
  192. package/src/thinking.ts +7 -0
  193. package/src/tiny/title-client.ts +34 -5
  194. package/src/tiny/title-protocol.ts +1 -1
  195. package/src/tiny/worker.ts +6 -4
  196. package/src/tools/ask.ts +4 -2
  197. package/src/tools/bash.ts +61 -10
  198. package/src/tools/browser/tab-worker.ts +26 -7
  199. package/src/tools/browser.ts +28 -1
  200. package/src/tools/find.ts +2 -27
  201. package/src/tools/grouped-file-output.ts +1 -118
  202. package/src/tools/image-gen.ts +11 -4
  203. package/src/tools/index.ts +17 -13
  204. package/src/tools/inspect-image.ts +1 -0
  205. package/src/tools/irc.ts +596 -171
  206. package/src/tools/job.ts +41 -7
  207. package/src/tools/read.ts +57 -1
  208. package/src/tools/renderers.ts +2 -0
  209. package/src/tools/resolve.ts +4 -1
  210. package/src/utils/commit-message-generator.ts +1 -0
  211. package/src/utils/git.ts +267 -13
  212. package/src/utils/title-generator.ts +24 -5
  213. package/dist/types/async/support.d.ts +0 -2
  214. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  215. package/dist/types/task/simple-mode.d.ts +0 -8
  216. package/src/async/support.ts +0 -5
  217. package/src/task/simple-mode.ts +0 -27
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Protocol handler for history:// URLs.
3
+ *
4
+ * Exposes agent transcripts as concise markdown. Live refs render from the
5
+ * in-memory message array; parked refs (session disposed, sessionFile
6
+ * retained) load read-only from the JSONL session file — no writer, no lock.
7
+ *
8
+ * URL forms:
9
+ * - history:// - Index of all registry agents (id, status, kind, last activity)
10
+ * - history://<agentId> - Concise markdown transcript of that agent
11
+ */
12
+ import type { AgentRef } from "../registry/agent-registry";
13
+ import { AgentRegistry } from "../registry/agent-registry";
14
+ import { formatSessionHistoryMarkdown } from "../session/session-history-format";
15
+ import { loadSessionMessagesReadOnly } from "../session/session-manager";
16
+ import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
17
+
18
+ /** Humanize a last-activity timestamp as `Ns/Nm/Nh/Nd ago`. */
19
+ function formatAgo(timestamp: number): string {
20
+ const diffMs = Math.max(0, Date.now() - timestamp);
21
+ const secs = Math.floor(diffMs / 1000);
22
+ if (secs < 60) return `${secs}s ago`;
23
+ const mins = Math.floor(secs / 60);
24
+ if (mins < 60) return `${mins}m ago`;
25
+ const hours = Math.floor(mins / 60);
26
+ if (hours < 24) return `${hours}h ago`;
27
+ return `${Math.floor(hours / 24)}d ago`;
28
+ }
29
+
30
+ /**
31
+ * Handler for history:// URLs.
32
+ *
33
+ * Resolves agent ids against the global AgentRegistry, serving transcripts
34
+ * for both live and parked agents.
35
+ */
36
+ export class HistoryProtocolHandler implements ProtocolHandler {
37
+ readonly scheme = "history";
38
+ readonly immutable = false;
39
+
40
+ async resolve(url: InternalUrl): Promise<InternalResource> {
41
+ const agentId = url.rawHost || url.hostname;
42
+ const registry = AgentRegistry.global();
43
+
44
+ if (!agentId) {
45
+ const content = this.#renderIndex(registry.list());
46
+ return {
47
+ url: url.href,
48
+ content,
49
+ contentType: "text/markdown",
50
+ size: Buffer.byteLength(content, "utf-8"),
51
+ };
52
+ }
53
+
54
+ let ref = registry.get(agentId);
55
+ if (!ref) {
56
+ // Case-insensitive fallback: agent ids are human-typed (e.g. AuthLoader).
57
+ const lower = agentId.toLowerCase();
58
+ ref = registry.list().find(candidate => candidate.id.toLowerCase() === lower);
59
+ }
60
+ if (!ref) {
61
+ const known = registry.list().map(candidate => candidate.id);
62
+ const knownStr = known.length > 0 ? known.join(", ") : "none";
63
+ throw new Error(`Unknown agent: ${agentId}\nKnown agents: ${knownStr}\nList all with history://`);
64
+ }
65
+
66
+ const notes: string[] = [];
67
+ let messages: unknown[];
68
+ if (ref.session) {
69
+ messages = ref.session.messages;
70
+ notes.push("Source: live session");
71
+ } else if (ref.sessionFile) {
72
+ messages = await loadSessionMessagesReadOnly(ref.sessionFile);
73
+ notes.push(`Source: session file (read-only, ${ref.status})`);
74
+ } else {
75
+ throw new Error(`Agent ${ref.id} has no transcript: session is gone and no session file was retained`);
76
+ }
77
+
78
+ const content = formatSessionHistoryMarkdown(messages, { title: `${ref.id} (${ref.status})` });
79
+ return {
80
+ url: url.href,
81
+ content,
82
+ contentType: "text/markdown",
83
+ size: Buffer.byteLength(content, "utf-8"),
84
+ sourcePath: ref.sessionFile ?? undefined,
85
+ notes,
86
+ };
87
+ }
88
+
89
+ #renderIndex(refs: AgentRef[]): string {
90
+ const lines: string[] = ["# Agents", ""];
91
+ if (refs.length === 0) {
92
+ lines.push("No agents registered.");
93
+ return `${lines.join("\n")}\n`;
94
+ }
95
+ lines.push("| id | status | kind | parent | last activity |", "|---|---|---|---|---|");
96
+ for (const ref of refs) {
97
+ lines.push(
98
+ `| ${ref.id} | ${ref.status} | ${ref.kind} | ${ref.parentId ?? "—"} | ${formatAgo(ref.lastActivity)} |`,
99
+ );
100
+ }
101
+ lines.push("", "Read a transcript with `read history://<id>`.");
102
+ return `${lines.join("\n")}\n`;
103
+ }
104
+
105
+ async complete(): Promise<UrlCompletion[]> {
106
+ return AgentRegistry.global()
107
+ .list()
108
+ .map(ref => ({
109
+ value: ref.id,
110
+ description: `${ref.status} · ${ref.kind}${ref.parentId ? ` · parent ${ref.parentId}` : ""}`,
111
+ }));
112
+ }
113
+ }
@@ -10,6 +10,7 @@
10
10
 
11
11
  export * from "./agent-protocol";
12
12
  export * from "./artifact-protocol";
13
+ export * from "./history-protocol";
13
14
  export * from "./issue-pr-protocol";
14
15
  export * from "./json-query";
15
16
  export * from "./local-protocol";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Internal URL router for internal protocols (`agent://`, `artifact://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`).
2
+ * Internal URL router for internal protocols (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`).
3
3
  *
4
4
  * One process-global router with one handler per scheme. Access via
5
5
  * `InternalUrlRouter.instance()`. Handlers are stateless; per-session and
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { AgentProtocolHandler } from "./agent-protocol";
9
9
  import { ArtifactProtocolHandler } from "./artifact-protocol";
10
+ import { HistoryProtocolHandler } from "./history-protocol";
10
11
  import { IssueProtocolHandler, PrProtocolHandler } from "./issue-pr-protocol";
11
12
  import { LocalProtocolHandler } from "./local-protocol";
12
13
  import { McpProtocolHandler } from "./mcp-protocol";
@@ -35,6 +36,7 @@ export class InternalUrlRouter {
35
36
  this.register(new McpProtocolHandler());
36
37
  this.register(new IssueProtocolHandler());
37
38
  this.register(new PrProtocolHandler());
39
+ this.register(new HistoryProtocolHandler());
38
40
  }
39
41
 
40
42
  /** Process-global router instance. */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Types for the internal URL routing system.
3
3
  *
4
- * Internal URLs (`agent://`, `artifact://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`) are resolved by tools like read,
4
+ * Internal URLs (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`) are resolved by tools like read,
5
5
  * providing access to agent outputs and server resources without exposing filesystem paths.
6
6
  */
7
7
 
package/src/irc/bus.ts ADDED
@@ -0,0 +1,292 @@
1
+ /**
2
+ * IrcBus - Process-global mailbox bus for agent-to-agent messaging.
3
+ *
4
+ * Replaces the old auto-reply model: a `send` never blocks on the recipient
5
+ * generating anything. Delivery resolves the recipient via the global
6
+ * AgentRegistry — parked agents are revived through the
7
+ * AgentLifecycleManager, idle agents are woken with a real turn, and busy
8
+ * agents receive the message as a non-interrupting aside at the next step
9
+ * boundary (see AgentSession.deliverIrcMessage). Replies are real turns by
10
+ * the recipient, observed via `wait`.
11
+ */
12
+
13
+ import { logger, Snowflake } from "@oh-my-pi/pi-utils";
14
+ import { AgentLifecycleManager } from "../registry/agent-lifecycle";
15
+ import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
16
+ import type { CustomMessage } from "../session/messages";
17
+
18
+ export interface IrcMessage {
19
+ id: string;
20
+ /** Sender agent id. */
21
+ from: string;
22
+ /** Recipient agent id (resolved; "all" is expanded by the tool, not stored). */
23
+ to: string;
24
+ body: string;
25
+ ts: number;
26
+ /** Message id being answered. */
27
+ replyTo?: string;
28
+ }
29
+
30
+ export interface IrcDeliveryReceipt {
31
+ to: string;
32
+ outcome: "injected" | "woken" | "revived" | "failed";
33
+ error?: string;
34
+ }
35
+
36
+ interface IrcWaiter {
37
+ from?: string;
38
+ resolve: (msg: IrcMessage) => void;
39
+ cancel: () => void;
40
+ }
41
+
42
+ /** Mailbox cap per agent; oldest messages are dropped beyond it. */
43
+ const MAILBOX_CAP = 100;
44
+
45
+ export class IrcBus {
46
+ static #global: IrcBus | undefined;
47
+
48
+ static global(): IrcBus {
49
+ if (!IrcBus.#global) {
50
+ IrcBus.#global = new IrcBus();
51
+ }
52
+ return IrcBus.#global;
53
+ }
54
+
55
+ /** Reset the global bus. Test-only. */
56
+ static resetGlobalForTests(): void {
57
+ IrcBus.#global = undefined;
58
+ }
59
+
60
+ readonly #registry: AgentRegistry;
61
+ readonly #lifecycle: () => AgentLifecycleManager;
62
+ readonly #mailboxes = new Map<string, IrcMessage[]>();
63
+ readonly #waiters = new Map<string, IrcWaiter[]>();
64
+
65
+ constructor(registry: AgentRegistry = AgentRegistry.global(), lifecycle?: AgentLifecycleManager) {
66
+ this.#registry = registry;
67
+ // Lazy: the lifecycle global self-constructs against the global registry,
68
+ // so only touch it when a parked recipient actually needs reviving.
69
+ this.#lifecycle = () => lifecycle ?? AgentLifecycleManager.global();
70
+ }
71
+
72
+ /**
73
+ * Fire-and-forget delivery. Never blocks on the recipient generating
74
+ * anything: the receipt reports how the message reached the recipient
75
+ * (waiter/aside = "injected", idle wake = "woken", park revival =
76
+ * "revived"), not what they did with it.
77
+ *
78
+ * Mailbox semantics: a successfully delivered message never lingers in
79
+ * the recipient's mailbox — injection/wake puts the full body into their
80
+ * context, so buffering it too would double-deliver via a later
81
+ * `wait`/`inbox` and inflate unread counts. Only a failed live hand-off
82
+ * is buffered for the recipient to drain later.
83
+ */
84
+ async send(msg: Omit<IrcMessage, "id" | "ts">): Promise<IrcDeliveryReceipt> {
85
+ const message: IrcMessage = { ...msg, id: Snowflake.next(), ts: Date.now() };
86
+ const ref = this.#registry.get(message.to);
87
+ if (!ref || ref.status === "aborted") {
88
+ return { to: message.to, outcome: "failed", error: `Unknown or terminated agent "${message.to}".` };
89
+ }
90
+
91
+ let revived = false;
92
+ if (ref.status === "parked") {
93
+ try {
94
+ await this.#lifecycle().ensureLive(message.to);
95
+ revived = true;
96
+ } catch (error) {
97
+ return {
98
+ to: message.to,
99
+ outcome: "failed",
100
+ error: error instanceof Error ? error.message : String(error),
101
+ };
102
+ }
103
+ }
104
+
105
+ // A pending `wait` from the recipient consumes the message directly —
106
+ // it is returned from their irc tool call and never hits the inbox or
107
+ // the session injection path.
108
+ const waiter = this.#takeMatchingWaiter(message.to, message.from);
109
+ if (waiter) {
110
+ waiter.resolve(message);
111
+ this.#relayToMainUi(message);
112
+ return { to: message.to, outcome: revived ? "revived" : "injected" };
113
+ }
114
+
115
+ const session = this.#registry.get(message.to)?.session;
116
+ if (!session) {
117
+ return { to: message.to, outcome: "failed", error: `Agent "${message.to}" has no live session.` };
118
+ }
119
+
120
+ try {
121
+ const delivery = await session.deliverIrcMessage(message);
122
+ this.#relayToMainUi(message);
123
+ return { to: message.to, outcome: revived ? "revived" : delivery };
124
+ } catch (error) {
125
+ // Live hand-off failed (e.g. recipient disposed mid-shutdown): buffer
126
+ // the message so a later `wait`/`inbox` from the recipient can still
127
+ // pick it up. The receipt stays "failed" — the recipient has not
128
+ // seen it.
129
+ this.#enqueue(message);
130
+ return {
131
+ to: message.to,
132
+ outcome: "failed",
133
+ error: error instanceof Error ? error.message : String(error),
134
+ };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Block until a message for `agentId` (optionally from `filter.from`)
140
+ * arrives; consume + return it. Null on timeout (`timeoutMs <= 0` waits
141
+ * forever). Rejects when `signal` aborts. By default, already-buffered
142
+ * mail satisfies the wait before parking a future waiter; callers that
143
+ * need a strictly future reply can disable that drain.
144
+ */
145
+ async wait(
146
+ agentId: string,
147
+ filter: { from?: string },
148
+ timeoutMs: number,
149
+ signal?: AbortSignal,
150
+ options?: { drainPending?: boolean },
151
+ ): Promise<IrcMessage | null> {
152
+ if (signal?.aborted) {
153
+ throw signal.reason instanceof Error ? signal.reason : new Error("IRC wait aborted");
154
+ }
155
+
156
+ if (options?.drainPending !== false) {
157
+ // Already-pending mail satisfies the wait without parking a waiter.
158
+ const pending = this.#takeFromMailbox(agentId, filter.from);
159
+ if (pending) return pending;
160
+ }
161
+
162
+ const { promise, resolve, reject } = Promise.withResolvers<IrcMessage | null>();
163
+ let timer: NodeJS.Timeout | undefined;
164
+ let onAbort: (() => void) | undefined;
165
+
166
+ const waiter: IrcWaiter = {
167
+ from: filter.from,
168
+ resolve: msg => {
169
+ cleanup();
170
+ resolve(msg);
171
+ },
172
+ cancel: () => {
173
+ cleanup();
174
+ },
175
+ };
176
+ const cleanup = (): void => {
177
+ this.#removeWaiter(agentId, waiter);
178
+ clearTimeout(timer);
179
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
180
+ };
181
+
182
+ if (signal) {
183
+ onAbort = () => {
184
+ cleanup();
185
+ reject(signal.reason instanceof Error ? signal.reason : new Error("IRC wait aborted"));
186
+ };
187
+ signal.addEventListener("abort", onAbort, { once: true });
188
+ }
189
+ if (timeoutMs > 0) {
190
+ timer = setTimeout(() => {
191
+ cleanup();
192
+ resolve(null);
193
+ }, timeoutMs);
194
+ timer.unref?.();
195
+ }
196
+
197
+ let waiters = this.#waiters.get(agentId);
198
+ if (!waiters) {
199
+ waiters = [];
200
+ this.#waiters.set(agentId, waiters);
201
+ }
202
+ waiters.push(waiter);
203
+ return promise;
204
+ }
205
+
206
+ /** Drain (or peek) pending messages for `agentId`. */
207
+ inbox(agentId: string, opts?: { peek?: boolean }): IrcMessage[] {
208
+ const mailbox = this.#mailboxes.get(agentId);
209
+ if (!mailbox || mailbox.length === 0) return [];
210
+ if (opts?.peek) return [...mailbox];
211
+ this.#mailboxes.delete(agentId);
212
+ return mailbox;
213
+ }
214
+
215
+ unreadCount(agentId: string): number {
216
+ return this.#mailboxes.get(agentId)?.length ?? 0;
217
+ }
218
+
219
+ #enqueue(message: IrcMessage): void {
220
+ let mailbox = this.#mailboxes.get(message.to);
221
+ if (!mailbox) {
222
+ mailbox = [];
223
+ this.#mailboxes.set(message.to, mailbox);
224
+ }
225
+ mailbox.push(message);
226
+ if (mailbox.length > MAILBOX_CAP) {
227
+ const dropped = mailbox.shift();
228
+ logger.debug("IrcBus: mailbox full, dropped oldest message", {
229
+ agentId: message.to,
230
+ droppedId: dropped?.id,
231
+ droppedFrom: dropped?.from,
232
+ });
233
+ }
234
+ }
235
+
236
+ /** Resolve the OLDEST waiter for `agentId` whose from-filter accepts `from`. */
237
+ #takeMatchingWaiter(agentId: string, from: string): IrcWaiter | undefined {
238
+ const waiters = this.#waiters.get(agentId);
239
+ if (!waiters) return undefined;
240
+ const index = waiters.findIndex(waiter => !waiter.from || waiter.from === from);
241
+ if (index === -1) return undefined;
242
+ const [waiter] = waiters.splice(index, 1);
243
+ if (waiters.length === 0) this.#waiters.delete(agentId);
244
+ return waiter;
245
+ }
246
+
247
+ #removeWaiter(agentId: string, waiter: IrcWaiter): void {
248
+ const waiters = this.#waiters.get(agentId);
249
+ if (!waiters) return;
250
+ const index = waiters.indexOf(waiter);
251
+ if (index !== -1) waiters.splice(index, 1);
252
+ if (waiters.length === 0) this.#waiters.delete(agentId);
253
+ }
254
+
255
+ #takeFromMailbox(agentId: string, from?: string): IrcMessage | undefined {
256
+ const mailbox = this.#mailboxes.get(agentId);
257
+ if (!mailbox) return undefined;
258
+ const index = from ? mailbox.findIndex(msg => msg.from === from) : 0;
259
+ if (index === -1 || mailbox.length === 0) return undefined;
260
+ const [message] = mailbox.splice(index, 1);
261
+ if (mailbox.length === 0) this.#mailboxes.delete(agentId);
262
+ return message;
263
+ }
264
+
265
+ /**
266
+ * Surface agent↔agent traffic as a display-only card on the main session
267
+ * UI. Skipped when the main agent is either endpoint: as recipient its
268
+ * own `deliverIrcMessage` (or `wait` tool result) already shows the
269
+ * message, and as sender the irc send tool call already rendered the
270
+ * outbound body — relaying it again would duplicate it in the transcript.
271
+ */
272
+ #relayToMainUi(message: IrcMessage): void {
273
+ if (message.to === MAIN_AGENT_ID || message.from === MAIN_AGENT_ID) return;
274
+ const mainSession = this.#registry.get(MAIN_AGENT_ID)?.session;
275
+ if (!mainSession) return;
276
+ const record: CustomMessage = {
277
+ role: "custom",
278
+ customType: "irc:relay",
279
+ content: `[IRC \`${message.from}\` → \`${message.to}\`]\n\n${message.body}`,
280
+ display: true,
281
+ details: { from: message.from, to: message.to, body: message.body },
282
+ attribution: "agent",
283
+ timestamp: message.ts,
284
+ };
285
+ try {
286
+ mainSession.emitIrcRelayObservation(record);
287
+ } catch (error) {
288
+ // Display-only forwarding must never affect delivery semantics.
289
+ logger.debug("IrcBus: main UI relay failed", { to: message.to, error: String(error) });
290
+ }
291
+ }
292
+ }
package/src/main.ts CHANGED
@@ -51,7 +51,6 @@ import { ExtensionRunner } from "./extensibility/extensions/runner";
51
51
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
52
52
  import { scheduleMarketplaceAutoUpdate } from "./extensibility/plugins/marketplace-auto-update";
53
53
  import type { MCPManager } from "./mcp";
54
- import { WelcomeComponent } from "./modes/components/welcome";
55
54
  import { InteractiveMode } from "./modes/interactive-mode";
56
55
  import type { PrintModeOptions } from "./modes/print-mode";
57
56
  import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
@@ -67,10 +66,10 @@ import {
67
66
  import type { AgentSession } from "./session/agent-session";
68
67
  import type { AuthStorage } from "./session/auth-storage";
69
68
  import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
70
- import { resolvePromptInput } from "./system-prompt";
69
+ import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
71
70
  import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
72
71
  import { AUTO_THINKING } from "./thinking";
73
- import { discoverStartupLspServers, type LspStartupServerInfo } from "./tools";
72
+ import type { LspStartupServerInfo } from "./tools";
74
73
  import {
75
74
  getChangelogPath,
76
75
  getNewEntries,
@@ -85,6 +84,7 @@ type RunPrintMode = (session: AgentSession, options: PrintModeOptions) => Promis
85
84
  type RunRpcMode = (
86
85
  session: AgentSession,
87
86
  setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
87
+ eventBus?: EventBus,
88
88
  ) => Promise<never>;
89
89
 
90
90
  function maybeShowStartupSplash(options: {
@@ -92,37 +92,12 @@ function maybeShowStartupSplash(options: {
92
92
  resuming: boolean;
93
93
  quiet: boolean;
94
94
  version: string;
95
- setupPending: boolean;
96
- modelName?: string;
97
- providerName?: string;
98
- lspServers?: LspStartupServerInfo[];
99
95
  }): void {
100
96
  if (!options.isInteractive) return;
101
97
  if (options.resuming || options.quiet) return;
102
98
  if ($env.PI_TIMING) return;
103
99
  if (!process.stdin.isTTY || !process.stdout.isTTY) return;
104
- // First-run launches go straight into the setup wizard, which paints its own
105
- // splash — keep the minimal two-line notice there.
106
- if (options.setupPending) {
107
- process.stdout.write(`${chalk.dim(`omp ${options.version}`)}\n${chalk.dim("Initializing session…")}\n`);
108
- return;
109
- }
110
- // Render the same welcome box the TUI paints first: recent sessions as a
111
- // loading placeholder (the fixed slot count keeps the box height stable) and
112
- // the logo held on the intro animation's first frame so the in-TUI intro
113
- // continues from the frame shown here. Clearing the screen first puts the
114
- // box at the same origin the TUI's first full paint (clearScrollback) uses,
115
- // so the live welcome replaces this frame in place without shifting.
116
- const welcome = new WelcomeComponent(
117
- options.version,
118
- options.modelName ?? "",
119
- options.providerName ?? "",
120
- null,
121
- options.lspServers ?? [],
122
- );
123
- welcome.holdIntroFirstFrame();
124
- const lines = welcome.render(process.stdout.columns || 80);
125
- process.stdout.write(`\x1b[2J\x1b[H\x1b[3J\n${lines.join("\n")}\n`);
100
+ //process.stdout.write(`${chalk.dim(`omp ${options.version}`)}\n${chalk.dim("Initializing session…")}\n`);
126
101
  }
127
102
 
128
103
  async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
@@ -155,7 +130,7 @@ const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
155
130
  "task.isolation.merge",
156
131
  "task.isolation.commits",
157
132
  "task.eager",
158
- "task.simple",
133
+ "task.batch",
159
134
  "task.maxConcurrency",
160
135
  "task.maxRecursionDepth",
161
136
  "task.disabledAgents",
@@ -370,6 +345,7 @@ async function runInteractiveMode(
370
345
  eventBus?: EventBus,
371
346
  initialMessage?: string,
372
347
  initialImages?: ImageContent[],
348
+ titleSystemPrompt?: string,
373
349
  ): Promise<void> {
374
350
  const mode = new InteractiveMode(
375
351
  session,
@@ -379,6 +355,7 @@ async function runInteractiveMode(
379
355
  lspServers,
380
356
  mcpManager,
381
357
  eventBus,
358
+ titleSystemPrompt,
382
359
  );
383
360
 
384
361
  // Cold-launch gate: the full setup wizard (every scene + the overlay and
@@ -422,7 +399,7 @@ async function runInteractiveMode(
422
399
  // Every in-process session load also uses `clearTerminalHistory`; cold launch
423
400
  // follows the same clean-cutover path instead of preserving a previous run's
424
401
  // transcript above the fresh one.
425
- mode.renderInitialMessages(undefined, { preserveExistingChat: true, clearTerminalHistory: true });
402
+ mode.renderInitialMessages({ preserveExistingChat: true, clearTerminalHistory: true });
426
403
 
427
404
  for (const notify of notifs) {
428
405
  if (!notify) {
@@ -724,7 +701,7 @@ async function buildSessionOptions(
724
701
  sessionManager: SessionManager | undefined,
725
702
  modelRegistry: ModelRegistry,
726
703
  activeSettings: Settings,
727
- ): Promise<{ options: CreateAgentSessionOptions }> {
704
+ ): Promise<{ options: CreateAgentSessionOptions; titleSystemPrompt?: string }> {
728
705
  const options: CreateAgentSessionOptions = {
729
706
  cwd: parsed.cwd ?? getProjectDir(),
730
707
  autoApprove: parsed.autoApprove ?? false,
@@ -735,6 +712,8 @@ async function buildSessionOptions(
735
712
  const resolvedSystemPrompt = await resolvePromptInput(systemPromptSource, "system prompt");
736
713
  const appendPromptSource = parsed.appendSystemPrompt ?? discoverAppendSystemPromptFile();
737
714
  const resolvedAppendPrompt = await resolvePromptInput(appendPromptSource, "append system prompt");
715
+ const titleSystemPromptSource = discoverTitleSystemPromptFile();
716
+ const titleSystemPrompt = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
738
717
 
739
718
  if (sessionManager) {
740
719
  options.sessionManager = sessionManager;
@@ -880,7 +859,7 @@ async function buildSessionOptions(
880
859
  options.additionalExtensionPaths = [];
881
860
  }
882
861
 
883
- return { options };
862
+ return { options, titleSystemPrompt };
884
863
  }
885
864
 
886
865
  interface RunRootCommandDependencies {
@@ -920,6 +899,7 @@ export async function runRootCommand(
920
899
  if (parsedArgs.listModels !== undefined) {
921
900
  const settingsInstance = await logger.time("settings:init:list-models", Settings.init, {
922
901
  cwd: getProjectDir(),
902
+ configFiles: parsedArgs.config,
923
903
  });
924
904
  await modelRegistry.refresh("online");
925
905
  const cliExtensionPaths = parsedArgs.noExtensions
@@ -983,11 +963,16 @@ export async function runRootCommand(
983
963
  }
984
964
 
985
965
  let cwd = getProjectDir();
986
- const settingsInstance = deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd }));
966
+ const settingsInstance =
967
+ deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd, configFiles: parsedArgs.config }));
987
968
  if (parsedArgs.approvalMode) {
988
969
  // Runtime override (not persisted): every settings.get("tools.approvalMode") downstream
989
970
  // sees this value. The wrapper still honours --auto-approve / --yolo on top of it.
990
971
  settingsInstance.override("tools.approvalMode", parsedArgs.approvalMode);
972
+ } else if (parsedArgs.autoApprove) {
973
+ // --auto-approve / --yolo without an explicit --approval-mode: reflect in settings so
974
+ // setup-time checks (e.g. #wrapToolForAcpPermission) also see the yolo intent.
975
+ settingsInstance.override("tools.approvalMode", "yolo");
991
976
  }
992
977
  if (parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui") {
993
978
  applyRpcDefaultSettingOverrides(settingsInstance);
@@ -1133,7 +1118,7 @@ export async function runRootCommand(
1133
1118
  clearPluginRootsCache: clearPluginRootsAndCaches,
1134
1119
  });
1135
1120
 
1136
- const { options: sessionOptions } = await logger.time(
1121
+ const { options: sessionOptions, titleSystemPrompt } = await logger.time(
1137
1122
  "buildSessionOptions",
1138
1123
  buildSessionOptions,
1139
1124
  parsedArgs,
@@ -1228,40 +1213,11 @@ export async function runRootCommand(
1228
1213
  stdinContent: pipedInput,
1229
1214
  });
1230
1215
 
1231
- // Resolve the model the session will most likely start with so the splash
1232
- // box matches the final welcome screen (the raw role selector, e.g.
1233
- // "anthropic/claude-fable-5:high", is wider than the left column and would
1234
- // collapse the box into the single-column layout).
1235
- let splashModel = sessionOptions.model;
1236
- if (!splashModel) {
1237
- const remembered = settingsInstance.getModelRole("default");
1238
- if (remembered) {
1239
- splashModel = resolveModelRoleValue(remembered, modelRegistry.getAll(), {
1240
- settings: settingsInstance,
1241
- matchPreferences: modelMatchPreferences,
1242
- modelRegistry,
1243
- }).model;
1244
- }
1245
- }
1246
- // Mirror createAgentSession's startup LSP discovery (sync and cheap: root
1247
- // markers + binary lookup) so the splash lists the same servers the live
1248
- // welcome screen will show.
1249
- const splashLspServers =
1250
- (sessionOptions.enableLsp ?? true)
1251
- ? discoverStartupLspServers(
1252
- sessionOptions.cwd ?? cwd,
1253
- settingsInstance.get("lsp.lazy") ? "available" : "connecting",
1254
- )
1255
- : [];
1256
1216
  maybeShowStartupSplash({
1257
1217
  isInteractive,
1258
1218
  resuming: Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1259
1219
  quiet: settingsInstance.get("startup.quiet"),
1260
1220
  version: VERSION,
1261
- setupPending: deps.forceSetupWizard === true || settingsInstance.get("setupVersion") < CURRENT_SETUP_VERSION,
1262
- modelName: splashModel?.name,
1263
- providerName: splashModel?.provider,
1264
- lspServers: splashLspServers,
1265
1221
  });
1266
1222
 
1267
1223
  const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } = await createSession({
@@ -1298,7 +1254,7 @@ export async function runRootCommand(
1298
1254
  // Branch-only protocol runner: keep RPC host code out of normal interactive startup.
1299
1255
  const runRpcMode: RunRpcMode = (await import("./modes/rpc/rpc-mode")).runRpcMode;
1300
1256
  stopStartupWatchdog();
1301
- await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined);
1257
+ await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus);
1302
1258
  } else if (isInteractive) {
1303
1259
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
1304
1260
  const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
@@ -1311,7 +1267,10 @@ export async function runRootCommand(
1311
1267
  return `${scopedModel.model.id}${thinkingStr}`;
1312
1268
  })
1313
1269
  .join(", ");
1314
- process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`)}\n`);
1270
+ // Routed through the TUI (not stdout): the startup capture owns the
1271
+ // terminal in raw mode here, and the TUI's first clearScrollback paint
1272
+ // would wipe a pre-TUI line anyway.
1273
+ notifs.push({ kind: "info", message: `Model scope: ${modelList} (Ctrl+P to cycle)` });
1315
1274
  }
1316
1275
 
1317
1276
  if ($env.PI_TIMING) {
@@ -1338,6 +1297,7 @@ export async function runRootCommand(
1338
1297
  eventBus,
1339
1298
  initialMessage,
1340
1299
  initialImages,
1300
+ titleSystemPrompt,
1341
1301
  );
1342
1302
  } else {
1343
1303
  // Branch-only single-shot runner: keep print-mode code out of normal interactive startup.
@@ -276,6 +276,7 @@ async function runPhase1(options: {
276
276
  apiKey: modelRegistry.resolver(phase1Model.provider, {
277
277
  sessionId: session.sessionId,
278
278
  baseUrl: phase1Model.baseUrl,
279
+ modelId: phase1Model.id,
279
280
  }),
280
281
  modelMaxTokens: computeModelTokenBudget(phase1Model, config),
281
282
  config,
@@ -436,6 +437,7 @@ async function runPhase2(options: {
436
437
  apiKey: modelRegistry.resolver(phase2Model.provider, {
437
438
  sessionId: session.sessionId,
438
439
  baseUrl: phase2Model.baseUrl,
440
+ modelId: phase2Model.id,
439
441
  }),
440
442
  metadata: session.agent?.metadataForProvider(phase2Model.provider),
441
443
  });