@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2

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 (198) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +3556 -3504
  3. package/dist/types/advisor/advise-tool.d.ts +12 -1
  4. package/dist/types/advisor/runtime.d.ts +41 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/update-cli.d.ts +4 -1
  7. package/dist/types/cli/usage-cli.d.ts +3 -0
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/config/api-key-resolver.d.ts +2 -2
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +8 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/config/settings-schema.d.ts +10 -0
  14. package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
  15. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
  16. package/dist/types/eval/backend.d.ts +3 -3
  17. package/dist/types/eval/bridge-timeout.d.ts +9 -1
  18. package/dist/types/eval/js/context-manager.d.ts +5 -3
  19. package/dist/types/eval/js/process-entry.d.ts +6 -0
  20. package/dist/types/eval/js/worker-core.d.ts +15 -1
  21. package/dist/types/eval/py/spawn-options.d.ts +10 -0
  22. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  23. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  26. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
  27. package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
  28. package/dist/types/goals/guided-setup.d.ts +12 -0
  29. package/dist/types/internal-urls/history-protocol.d.ts +3 -2
  30. package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
  31. package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
  32. package/dist/types/main.d.ts +1 -0
  33. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  34. package/dist/types/mcp/oauth-flow.d.ts +2 -0
  35. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
  36. package/dist/types/modes/components/agent-hub.d.ts +10 -0
  37. package/dist/types/modes/components/dynamic-border.d.ts +5 -3
  38. package/dist/types/modes/components/login-dialog.d.ts +2 -0
  39. package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
  40. package/dist/types/modes/components/read-tool-group.d.ts +0 -2
  41. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  42. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  43. package/dist/types/modes/interactive-mode.d.ts +1 -0
  44. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  45. package/dist/types/modes/orchestrate.d.ts +1 -1
  46. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  47. package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
  48. package/dist/types/modes/types.d.ts +1 -0
  49. package/dist/types/modes/ultrathink.d.ts +1 -1
  50. package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
  51. package/dist/types/modes/workflow.d.ts +1 -1
  52. package/dist/types/session/agent-session.d.ts +6 -0
  53. package/dist/types/session/exit-diagnostics.d.ts +11 -0
  54. package/dist/types/session/messages.d.ts +15 -0
  55. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
  56. package/dist/types/subprocess/worker-client.d.ts +6 -0
  57. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  58. package/dist/types/tools/grep.d.ts +0 -2
  59. package/dist/types/tools/read.d.ts +0 -4
  60. package/dist/types/web/search/provider.d.ts +10 -3
  61. package/dist/types/web/search/providers/codex.d.ts +5 -4
  62. package/package.json +12 -12
  63. package/src/advisor/__tests__/advisor.test.ts +917 -42
  64. package/src/advisor/advise-tool.ts +17 -1
  65. package/src/advisor/runtime.ts +270 -67
  66. package/src/autolearn/controller.ts +15 -3
  67. package/src/autoresearch/dashboard.ts +2 -2
  68. package/src/cli/args.ts +12 -0
  69. package/src/cli/auth-broker-cli.ts +30 -11
  70. package/src/cli/auth-gateway-cli.ts +5 -1
  71. package/src/cli/config-cli.ts +15 -3
  72. package/src/cli/dry-balance-cli.ts +14 -4
  73. package/src/cli/flag-tables.ts +21 -7
  74. package/src/cli/update-cli.ts +62 -11
  75. package/src/cli/usage-cli.ts +58 -5
  76. package/src/cli/usage-error.ts +7 -0
  77. package/src/cli.ts +23 -1
  78. package/src/commands/acp.ts +11 -2
  79. package/src/commands/launch.ts +12 -3
  80. package/src/commands/token.ts +3 -1
  81. package/src/config/api-key-resolver.ts +12 -3
  82. package/src/config/config-file.ts +30 -12
  83. package/src/config/model-registry.ts +7 -7
  84. package/src/config/model-resolver.ts +21 -7
  85. package/src/config/models-config.ts +1 -1
  86. package/src/config/settings-schema.ts +10 -0
  87. package/src/cursor.ts +2 -0
  88. package/src/discovery/claude-plugins.ts +9 -3
  89. package/src/discovery/omp-plugins.ts +6 -2
  90. package/src/discovery/substitute-plugin-root.ts +32 -0
  91. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  92. package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
  93. package/src/eval/__tests__/js-context-manager.test.ts +158 -1
  94. package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
  95. package/src/eval/__tests__/prelude-agent.test.ts +20 -0
  96. package/src/eval/__tests__/process-entry-import.test.ts +27 -0
  97. package/src/eval/agent-bridge.ts +121 -116
  98. package/src/eval/backend.ts +3 -3
  99. package/src/eval/bridge-timeout.ts +20 -2
  100. package/src/eval/executor-base.ts +85 -7
  101. package/src/eval/jl/kernel.ts +2 -1
  102. package/src/eval/js/context-manager.ts +109 -32
  103. package/src/eval/js/process-entry.ts +27 -0
  104. package/src/eval/js/shared/runtime.ts +1 -1
  105. package/src/eval/js/worker-core.ts +70 -9
  106. package/src/eval/js/worker-entry.ts +1 -1
  107. package/src/eval/py/__tests__/prelude.test.ts +72 -0
  108. package/src/eval/py/kernel.ts +2 -1
  109. package/src/eval/py/prelude.py +28 -1
  110. package/src/eval/py/spawn-options.ts +13 -0
  111. package/src/eval/py/tool-bridge.ts +13 -14
  112. package/src/eval/rb/kernel.ts +2 -1
  113. package/src/exec/bash-executor.ts +30 -43
  114. package/src/extensibility/custom-tools/types.ts +3 -0
  115. package/src/extensibility/extensions/runner.ts +3 -0
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/extensions/wrapper.ts +18 -18
  118. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  119. package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
  120. package/src/extensibility/plugins/manager.ts +28 -7
  121. package/src/goals/guided-setup.ts +29 -1
  122. package/src/internal-urls/history-protocol.ts +95 -15
  123. package/src/internal-urls/memory-protocol.ts +13 -9
  124. package/src/internal-urls/registry-helpers.ts +50 -1
  125. package/src/launch/broker.ts +38 -25
  126. package/src/lsp/client.ts +7 -1
  127. package/src/main.ts +29 -0
  128. package/src/mcp/oauth-discovery.ts +20 -1
  129. package/src/mcp/oauth-flow.ts +3 -1
  130. package/src/mcp/tool-bridge.ts +57 -6
  131. package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
  132. package/src/modes/components/agent-dashboard.ts +2 -2
  133. package/src/modes/components/agent-hub.ts +15 -2
  134. package/src/modes/components/agent-transcript-viewer.ts +2 -2
  135. package/src/modes/components/chat-transcript-builder.ts +26 -4
  136. package/src/modes/components/dynamic-border.ts +9 -6
  137. package/src/modes/components/extensions/extension-list.ts +2 -2
  138. package/src/modes/components/hook-selector.ts +10 -4
  139. package/src/modes/components/login-dialog.ts +5 -0
  140. package/src/modes/components/mcp-add-wizard.ts +5 -0
  141. package/src/modes/components/plan-review-overlay.ts +11 -11
  142. package/src/modes/components/read-tool-group.ts +1 -8
  143. package/src/modes/components/status-line/component.ts +10 -1
  144. package/src/modes/components/transcript-container.ts +110 -7
  145. package/src/modes/controllers/command-controller.ts +12 -4
  146. package/src/modes/controllers/event-controller.ts +80 -15
  147. package/src/modes/controllers/input-controller.ts +4 -2
  148. package/src/modes/controllers/mcp-command-controller.ts +6 -7
  149. package/src/modes/controllers/selector-controller.ts +24 -5
  150. package/src/modes/controllers/todo-command-controller.ts +18 -14
  151. package/src/modes/interactive-mode.ts +7 -3
  152. package/src/modes/magic-keyword-boundary.ts +23 -0
  153. package/src/modes/orchestrate.ts +6 -5
  154. package/src/modes/print-mode.ts +9 -0
  155. package/src/modes/prompt-action-autocomplete.ts +6 -1
  156. package/src/modes/rpc/host-tools.ts +15 -0
  157. package/src/modes/rpc/rpc-mode.ts +123 -48
  158. package/src/modes/types.ts +1 -1
  159. package/src/modes/ultrathink.ts +6 -5
  160. package/src/modes/utils/transcript-render-helpers.ts +54 -0
  161. package/src/modes/utils/ui-helpers.ts +27 -1
  162. package/src/modes/workflow.ts +6 -5
  163. package/src/prompts/advisor/system.md +1 -0
  164. package/src/prompts/system/system-prompt.md +1 -0
  165. package/src/prompts/tools/eval.md +2 -2
  166. package/src/prompts/tools/grep.md +1 -2
  167. package/src/prompts/tools/read.md +2 -4
  168. package/src/sdk.ts +61 -34
  169. package/src/session/agent-session.ts +283 -39
  170. package/src/session/exit-diagnostics.ts +108 -0
  171. package/src/session/messages.test.ts +66 -0
  172. package/src/session/messages.ts +37 -0
  173. package/src/session/streaming-output.ts +40 -12
  174. package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
  175. package/src/slash-commands/helpers/logout.ts +23 -3
  176. package/src/slash-commands/helpers/usage-report.ts +14 -2
  177. package/src/subprocess/worker-client.ts +9 -2
  178. package/src/system-prompt.test.ts +36 -0
  179. package/src/system-prompt.ts +1 -1
  180. package/src/task/executor.ts +8 -0
  181. package/src/task/render.test.ts +36 -0
  182. package/src/task/render.ts +55 -43
  183. package/src/tools/bash-skill-urls.ts +4 -1
  184. package/src/tools/bash.ts +1 -0
  185. package/src/tools/browser/registry.ts +17 -3
  186. package/src/tools/eval.ts +14 -9
  187. package/src/tools/gh.ts +3 -1
  188. package/src/tools/grep.ts +5 -45
  189. package/src/tools/path-utils.ts +7 -1
  190. package/src/tools/read.ts +23 -74
  191. package/src/tools/write.ts +82 -9
  192. package/src/tools/yield.ts +29 -1
  193. package/src/utils/title-generator.ts +10 -6
  194. package/src/web/search/index.ts +39 -22
  195. package/src/web/search/provider.ts +33 -16
  196. package/src/web/search/providers/codex.ts +68 -21
  197. package/src/web/search/providers/perplexity-auth.ts +20 -11
  198. package/src/web/search/providers/perplexity.ts +14 -2
@@ -1,6 +1,6 @@
1
1
  import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Tool } from "@oh-my-pi/pi-ai";
3
- import { prompt } from "@oh-my-pi/pi-utils";
3
+ import { prompt, Snowflake } from "@oh-my-pi/pi-utils";
4
4
  import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit/utils";
5
5
  import guidedGoalInterviewPrompt from "../prompts/goals/guided-goal-interview.md" with { type: "text" };
6
6
  import guidedGoalSystemPrompt from "../prompts/goals/guided-goal-system.md" with { type: "text" };
@@ -37,6 +37,21 @@ export type GuidedGoalTurnResult =
37
37
  export interface GuidedGoalTurnOptions {
38
38
  messages: readonly GuidedGoalMessage[];
39
39
  signal?: AbortSignal;
40
+ /**
41
+ * Stable Codex transport session id reused across every turn of one
42
+ * interview. `handleGuidedGoalCommand` runs up to six turns; minting a fresh
43
+ * id per turn opens a new websocket-only Codex socket each time (kept in
44
+ * `providerSessionState` until session dispose), which can trip
45
+ * `websocket_connection_limit_reached` and drop back to the SSE path this
46
+ * fix avoids. Callers pass one id for the whole interview; omitted for
47
+ * one-shot callers, which mint a unique id per call.
48
+ */
49
+ sideSessionId?: string;
50
+ }
51
+
52
+ /** Mint a guided-goal Codex side-session id keyed off the main session id. */
53
+ export function newGuidedGoalSessionId(session: AgentSession): string {
54
+ return `${session.sessionId}:guided-goal:${Snowflake.next()}`;
40
55
  }
41
56
 
42
57
  function parseGuidedGoalPayload(value: unknown): GuidedGoalTurnResult {
@@ -106,6 +121,19 @@ export async function runGuidedGoalTurn(
106
121
  reasoning: toReasoningEffort(thinkingLevel),
107
122
  disableReasoning: shouldDisableReasoning(thinkingLevel),
108
123
  toolChoice: { type: "tool", name: RESPOND_TOOL_NAME },
124
+ // Route through the session's provider transport so websocket-only Codex
125
+ // models (gpt-5.6-luna/sol/terra) get a websocket session instead of
126
+ // falling back to SSE — the Codex SSE /responses endpoint does not serve
127
+ // those ids and rejects the turn with "Model not found" (#5304, same class
128
+ // as the /btw regression in #5213). The side session id is minted once per
129
+ // interview and reused across turns so a multi-question interview shares one
130
+ // Codex socket instead of opening a fresh one each turn; it stays distinct
131
+ // from the main session id so the oneshot's append-only turn state never
132
+ // pollutes the main conversation.
133
+ sessionId: options.sideSessionId ?? newGuidedGoalSessionId(session),
134
+ promptCacheKey: session.sessionId,
135
+ preferWebsockets: session.preferWebsockets,
136
+ providerSessionState: session.providerSessionState,
109
137
  },
110
138
  { telemetry: resolveTelemetry(session.agent.telemetry, session.sessionId), oneshotKind: "guided_goal_setup" },
111
139
  );
@@ -5,14 +5,22 @@
5
5
  * in-memory message array; parked refs (session disposed, sessionFile
6
6
  * retained) load read-only from the JSONL session file — no writer, no lock.
7
7
  *
8
+ * Agents that are no longer in the `AgentRegistry` — one-shot helpers
9
+ * unregistered after `finalizeSubagentLifecycle` (`keepAlive: false`, e.g. the
10
+ * `eval` `agent()` bridge), agents released via the Agent Hub / vibe kill, or
11
+ * any agent after a session resume — remain reachable: `resolve`, `complete`,
12
+ * and the index all fall back to scanning artifacts dirs for `<id>.jsonl`,
13
+ * mirroring how `agent://` reads `.md` outputs straight off disk.
14
+ *
8
15
  * URL forms:
9
- * - history:// - Index of all registry agents (id, status, kind, last activity)
16
+ * - history:// - Index of all registry + on-disk agents (id, status, kind, last activity)
10
17
  * - history://<agentId> - Concise markdown transcript of that agent
11
18
  */
12
19
  import type { AgentRef } from "../registry/agent-registry";
13
20
  import { AgentRegistry } from "../registry/agent-registry";
14
21
  import { formatSessionHistoryMarkdown } from "../session/session-history-format";
15
22
  import { loadSessionMessagesReadOnly } from "../session/session-loader";
23
+ import { sessionFilesFromDisk } from "./registry-helpers";
16
24
  import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
17
25
 
18
26
  /** Humanize a last-activity timestamp as `Ns/Nm/Nh/Nd ago`. */
@@ -27,11 +35,21 @@ function formatAgo(timestamp: number): string {
27
35
  return `${Math.floor(hours / 24)}d ago`;
28
36
  }
29
37
 
38
+ /** One row of the history index — either a registered ref or a disk-only transcript. */
39
+ interface IndexEntry {
40
+ id: string;
41
+ status: string;
42
+ kind: string;
43
+ parent: string;
44
+ lastActivity: string;
45
+ }
46
+
30
47
  /**
31
48
  * Handler for history:// URLs.
32
49
  *
33
- * Resolves agent ids against the global AgentRegistry, serving transcripts
34
- * for both live and parked agents.
50
+ * Resolves agent ids against the global AgentRegistry, then falls back to
51
+ * on-disk `.jsonl` transcripts, serving read-only history for live, parked,
52
+ * and unregistered agents alike.
35
53
  */
36
54
  export class HistoryProtocolHandler implements ProtocolHandler {
37
55
  readonly scheme = "history";
@@ -45,7 +63,7 @@ export class HistoryProtocolHandler implements ProtocolHandler {
45
63
  const visible = registry.list().filter(ref => ref.kind !== "advisor");
46
64
 
47
65
  if (!agentId) {
48
- const content = this.#renderIndex(visible);
66
+ const content = await this.#renderIndex(visible);
49
67
  return {
50
68
  url: url.href,
51
69
  content,
@@ -61,7 +79,13 @@ export class HistoryProtocolHandler implements ProtocolHandler {
61
79
  const lower = agentId.toLowerCase();
62
80
  ref = visible.find(candidate => candidate.id.toLowerCase() === lower);
63
81
  }
82
+
64
83
  if (!ref) {
84
+ // Registry miss — the agent may have been unregistered or lost on resume.
85
+ // Serve its transcript straight from disk if the session file persists.
86
+ const disk = await this.#resolveFromDisk(agentId);
87
+ if (disk) return { ...disk, url: url.href };
88
+
65
89
  const known = visible.map(candidate => candidate.id);
66
90
  const knownStr = known.length > 0 ? known.join(", ") : "none";
67
91
  throw new Error(`Unknown agent: ${agentId}\nKnown agents: ${knownStr}\nList all with history://`);
@@ -76,6 +100,10 @@ export class HistoryProtocolHandler implements ProtocolHandler {
76
100
  messages = await loadSessionMessagesReadOnly(ref.sessionFile);
77
101
  notes.push(`Source: session file (read-only, ${ref.status})`);
78
102
  } else {
103
+ // No live session and no retained sessionFile — try the disk scan before
104
+ // giving up, in case the transcript lingers under an artifacts dir.
105
+ const disk = await this.#resolveFromDisk(ref.id);
106
+ if (disk) return { ...disk, url: url.href };
79
107
  throw new Error(`Agent ${ref.id} has no transcript: session is gone and no session file was retained`);
80
108
  }
81
109
 
@@ -90,29 +118,81 @@ export class HistoryProtocolHandler implements ProtocolHandler {
90
118
  };
91
119
  }
92
120
 
93
- #renderIndex(refs: AgentRef[]): string {
121
+ /**
122
+ * Load a transcript for `agentId` from an on-disk `.jsonl` session file,
123
+ * matched case-insensitively. Returns `undefined` when no file is found.
124
+ */
125
+ async #resolveFromDisk(agentId: string): Promise<InternalResource | undefined> {
126
+ const files = await sessionFilesFromDisk();
127
+ const lower = agentId.toLowerCase();
128
+ let matchedId: string | undefined;
129
+ let sessionFile: string | undefined;
130
+ for (const [id, file] of files) {
131
+ if (id === agentId || id.toLowerCase() === lower) {
132
+ matchedId = id;
133
+ sessionFile = file;
134
+ if (id === agentId) break;
135
+ }
136
+ }
137
+ if (!matchedId || !sessionFile) return undefined;
138
+ const messages = await loadSessionMessagesReadOnly(sessionFile);
139
+ const content = formatSessionHistoryMarkdown(messages, { title: `${matchedId} (on disk)` });
140
+ return {
141
+ url: "",
142
+ content,
143
+ contentType: "text/markdown",
144
+ size: Buffer.byteLength(content, "utf-8"),
145
+ sourcePath: sessionFile,
146
+ notes: ["Source: session file (read-only, unregistered)"],
147
+ };
148
+ }
149
+
150
+ async #renderIndex(refs: AgentRef[]): Promise<string> {
151
+ const entries: IndexEntry[] = refs.map(ref => ({
152
+ id: ref.id,
153
+ status: ref.status,
154
+ kind: ref.kind,
155
+ parent: ref.parentId ?? "—",
156
+ lastActivity: formatAgo(ref.lastActivity),
157
+ }));
158
+ // Merge on-disk transcripts for agents absent from the registry.
159
+ const registered = new Set(refs.map(ref => ref.id));
160
+ const disk = await sessionFilesFromDisk();
161
+ for (const id of disk.keys()) {
162
+ if (registered.has(id)) continue;
163
+ entries.push({ id, status: "on disk", kind: "—", parent: "—", lastActivity: "—" });
164
+ }
165
+
94
166
  const lines: string[] = ["# Agents", ""];
95
- if (refs.length === 0) {
167
+ if (entries.length === 0) {
96
168
  lines.push("No agents registered.");
97
169
  return `${lines.join("\n")}\n`;
98
170
  }
99
171
  lines.push("| id | status | kind | parent | last activity |", "|---|---|---|---|---|");
100
- for (const ref of refs) {
101
- lines.push(
102
- `| ${ref.id} | ${ref.status} | ${ref.kind} | ${ref.parentId ?? "—"} | ${formatAgo(ref.lastActivity)} |`,
103
- );
172
+ for (const entry of entries) {
173
+ lines.push(`| ${entry.id} | ${entry.status} | ${entry.kind} | ${entry.parent} | ${entry.lastActivity} |`);
104
174
  }
105
175
  lines.push("", "Read a transcript with `read history://<id>`.");
106
176
  return `${lines.join("\n")}\n`;
107
177
  }
108
178
 
109
179
  async complete(): Promise<UrlCompletion[]> {
110
- return AgentRegistry.global()
111
- .list()
112
- .filter(ref => ref.kind !== "advisor")
113
- .map(ref => ({
180
+ const completions: UrlCompletion[] = [];
181
+ const seen = new Set<string>();
182
+ for (const ref of AgentRegistry.global().list()) {
183
+ if (ref.kind === "advisor") continue;
184
+ seen.add(ref.id);
185
+ completions.push({
114
186
  value: ref.id,
115
187
  description: `${ref.status} · ${ref.kind}${ref.parentId ? ` · parent ${ref.parentId}` : ""}`,
116
- }));
188
+ });
189
+ }
190
+ const disk = await sessionFilesFromDisk();
191
+ for (const id of disk.keys()) {
192
+ if (seen.has(id)) continue;
193
+ seen.add(id);
194
+ completions.push({ value: id, description: "on disk" });
195
+ }
196
+ return completions;
117
197
  }
118
198
  }
@@ -6,7 +6,7 @@ import { getMnemopiSessionState, type MnemopiScopedMemoryHit, type MnemopiSessio
6
6
  import { AgentRegistry } from "../registry/agent-registry";
7
7
  import { buildDirectoryResource } from "./filesystem-resource";
8
8
  import { validateRelativePath } from "./skill-protocol";
9
- import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
9
+ import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
10
10
 
11
11
  const DEFAULT_MEMORY_FILE = "memory_summary.md";
12
12
  const MEMORY_NAMESPACE = "root";
@@ -28,6 +28,11 @@ export function memoryRootsFromRegistry(): string[] {
28
28
  return roots;
29
29
  }
30
30
 
31
+ function memoryRootsForContext(context?: ResolveContext): string[] {
32
+ if (context?.cwd) return [getMemoryRoot(getAgentDir(), context.cwd)];
33
+ return memoryRootsFromRegistry();
34
+ }
35
+
31
36
  function ensureWithinRoot(targetPath: string, rootPath: string): void {
32
37
  if (targetPath !== rootPath && !targetPath.startsWith(`${rootPath}${path.sep}`)) {
33
38
  throw new Error("memory:// URL escapes memory root");
@@ -196,16 +201,15 @@ function renderMnemopiMemory(url: InternalUrl, hit: MnemopiScopedMemoryHit): Int
196
201
 
197
202
  /**
198
203
  * Protocol handler for memory:// URLs.
199
- *
200
- * Walks every active session's memory root. Worktree-based subagents have
201
- * their own root; first one containing the file wins. Parent and subagent
202
- * sharing a cwd see the same file regardless of order.
204
+ * Resolves file-backed roots against the calling session cwd when provided.
205
+ * Contextless callers fall back to the live-session registry for legacy
206
+ * cross-session lookups.
203
207
  */
204
208
  export class MemoryProtocolHandler implements ProtocolHandler {
205
209
  readonly scheme = "memory";
206
210
  readonly immutable = true;
207
211
 
208
- async resolve(url: InternalUrl): Promise<InternalResource> {
212
+ async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
209
213
  const namespace = url.rawHost || url.hostname;
210
214
  if (!namespace) {
211
215
  throw new Error("memory:// URL requires a namespace: memory://root or memory://<memory-id>");
@@ -230,7 +234,7 @@ export class MemoryProtocolHandler implements ProtocolHandler {
230
234
  );
231
235
  }
232
236
 
233
- const roots = memoryRootsFromRegistry();
237
+ const roots = memoryRootsForContext(context);
234
238
  if (roots.length === 0) {
235
239
  throw new Error(
236
240
  "Memory artifacts are not available for this project yet. Run a session with memories enabled first.",
@@ -259,9 +263,9 @@ export class MemoryProtocolHandler implements ProtocolHandler {
259
263
  throw new Error(`Memory file not found: ${url.href}`);
260
264
  }
261
265
 
262
- async complete(): Promise<UrlCompletion[]> {
266
+ async complete(_query?: string, context?: ResolveContext): Promise<UrlCompletion[]> {
263
267
  const completions: UrlCompletion[] = [];
264
- if (memoryRootsFromRegistry().length > 0) {
268
+ if (memoryRootsForContext(context).length > 0) {
265
269
  completions.push({ value: MEMORY_NAMESPACE, description: "Project memory summary" });
266
270
  }
267
271
  if (mnemopiSessionStatesFromRegistry().length > 0) {
@@ -2,6 +2,11 @@
2
2
  * Shared helpers for internal-url protocol handlers that resolve IDs against
3
3
  * registered agent sessions.
4
4
  */
5
+
6
+ import type { Dirent } from "node:fs";
7
+ import * as fs from "node:fs/promises";
8
+ import * as path from "node:path";
9
+ import { isEnoent } from "@oh-my-pi/pi-utils";
5
10
  import { AgentRegistry } from "../registry/agent-registry";
6
11
 
7
12
  const extraArtifactsDirs = new Set<string>();
@@ -35,9 +40,53 @@ export function artifactsDirsFromRegistry(): string[] {
35
40
  if (!dirs.includes(dir)) dirs.push(dir);
36
41
  };
37
42
  for (const ref of AgentRegistry.global().list()) {
38
- addDir(ref.session?.sessionManager.getArtifactsDir());
43
+ addDir(ref.session?.sessionManager?.getArtifactsDir());
39
44
  if (ref.sessionFile) addDir(ref.sessionFile.slice(0, -6));
40
45
  }
41
46
  for (const dir of extraArtifactsDirs) addDir(dir);
42
47
  return dirs;
43
48
  }
49
+
50
+ /**
51
+ * Recursively scan artifacts dirs for agent session transcripts, keyed by
52
+ * agent id (the `.jsonl` basename). Used by `history://` so transcripts of
53
+ * agents no longer in the registry (unregistered one-shot helpers, released
54
+ * agents, or any agent after session resume) remain reachable — mirroring how
55
+ * `agent://` reads `.md` outputs straight off disk.
56
+ *
57
+ * Layout follows `task/index.ts`: a subagent's transcript is
58
+ * `<artifactsDir>/<AgentId>.jsonl`, and its own children nest one level deeper
59
+ * under `<artifactsDir>/<AgentId>/<AgentId>.<ChildId>.jsonl`. Advisor
60
+ * transcripts (`__advisor*.jsonl`) are observability-only and excluded;
61
+ * EPERM-rewrite backups (`.bak`) are skipped. When the same id appears in
62
+ * multiple dirs, the first hit wins (registry dirs are scanned first).
63
+ */
64
+ export async function sessionFilesFromDisk(): Promise<Map<string, string>> {
65
+ const found = new Map<string, string>();
66
+ const seenDirs = new Set<string>();
67
+ const scan = async (dir: string, depth: number): Promise<void> => {
68
+ if (depth > 8 || seenDirs.has(dir)) return;
69
+ seenDirs.add(dir);
70
+ let entries: Dirent[];
71
+ try {
72
+ entries = await fs.readdir(dir, { withFileTypes: true });
73
+ } catch (err) {
74
+ if (isEnoent(err) || (err as NodeJS.ErrnoException).code === "ENOTDIR") return;
75
+ throw err;
76
+ }
77
+ for (const entry of entries) {
78
+ if (entry.isDirectory()) {
79
+ await scan(path.join(dir, entry.name), depth + 1);
80
+ continue;
81
+ }
82
+ if (!entry.isFile()) continue;
83
+ const name = entry.name;
84
+ if (!name.endsWith(".jsonl")) continue;
85
+ if (name.startsWith("__advisor")) continue;
86
+ const id = name.slice(0, -".jsonl".length);
87
+ if (!found.has(id)) found.set(id, path.join(dir, name));
88
+ }
89
+ };
90
+ for (const dir of artifactsDirsFromRegistry()) await scan(dir, 0);
91
+ return found;
92
+ }
@@ -84,9 +84,6 @@ interface DaemonLogRead {
84
84
  function quoteShellArg(value: string): string {
85
85
  return `'${value.replaceAll("'", `'\\''`)}'`;
86
86
  }
87
- function quoteCmdArg(value: string): string {
88
- return `"${value.replaceAll('"', '""')}"`;
89
- }
90
87
 
91
88
  function terminalState(state: DaemonSnapshot["state"]): boolean {
92
89
  return state === "exited" || state === "failed";
@@ -434,6 +431,13 @@ class DaemonBroker {
434
431
  if (spec.detached && spec.pty) {
435
432
  throw new Error("A detached daemon cannot allocate a PTY");
436
433
  }
434
+ if (
435
+ spec.pty &&
436
+ process.platform === "win32" &&
437
+ [".bat", ".cmd"].includes(path.extname(spec.application).toLowerCase())
438
+ ) {
439
+ throw new Error('Windows batch files require application "cmd.exe" with the batch path after "/c"');
440
+ }
437
441
  const existing = this.#records.get(spec.name);
438
442
  if (existing) await this.#refreshDetached(existing);
439
443
  if (existing && !terminalState(existing.snapshot.state)) {
@@ -520,38 +524,47 @@ class DaemonBroker {
520
524
  }
521
525
 
522
526
  async #launchPty(record: ManagedDaemon, generation: number): Promise<void> {
523
- const pidPath = path.join(record.dir, "process.pid");
524
- await fs.rm(pidPath, { force: true });
525
- const argv = [record.spec.application, ...record.spec.args];
526
- const command =
527
- process.platform === "win32"
528
- ? argv.map(quoteCmdArg).join(" ")
529
- : [`printf '%s' "$$" > ${quoteShellArg(pidPath)}`, `exec ${argv.map(quoteShellArg).join(" ")}`].join("; ");
530
527
  const session = new PtySession();
531
528
  record.pty = session;
532
- const shell = process.platform === "win32" ? process.env.COMSPEC : process.env.SHELL;
533
- void session
534
- .start(
529
+ const options = {
530
+ cwd: record.spec.cwd,
531
+ env: workerEnvFromParent({ TERM: "xterm-256color", ...record.spec.env }),
532
+ cols: DAEMON_PTY_COLUMNS,
533
+ rows: DAEMON_PTY_ROWS,
534
+ };
535
+ const onChunk = (error: Error | null, chunk: string): void => {
536
+ if (generation !== record.generation) return;
537
+ if (error) record.log?.append(`PTY output error: ${error.message}\n`);
538
+ if (chunk) this.#onOutput(record, generation, chunk);
539
+ };
540
+ let run: Promise<PtyRunResult>;
541
+ if (process.platform === "win32") {
542
+ run = session.startArgv(
535
543
  {
536
- command,
537
- cwd: record.spec.cwd,
538
- env: workerEnvFromParent({ TERM: "xterm-256color", ...record.spec.env }),
539
- cols: DAEMON_PTY_COLUMNS,
540
- rows: DAEMON_PTY_ROWS,
541
- shell,
544
+ application: record.spec.application,
545
+ args: record.spec.args,
546
+ ...options,
542
547
  },
543
- (error, chunk) => {
544
- if (generation !== record.generation) return;
545
- if (error) record.log?.append(`PTY output error: ${error.message}\n`);
546
- if (chunk) this.#onOutput(record, generation, chunk);
547
- },
548
- )
548
+ onChunk,
549
+ );
550
+ } else {
551
+ const pidPath = path.join(record.dir, "process.pid");
552
+ await fs.rm(pidPath, { force: true });
553
+ const argv = [record.spec.application, ...record.spec.args];
554
+ const command = [
555
+ `printf '%s' "$$" > ${quoteShellArg(pidPath)}`,
556
+ `exec ${argv.map(quoteShellArg).join(" ")}`,
557
+ ].join("; ");
558
+ run = session.start({ command, shell: process.env.SHELL, ...options }, onChunk);
559
+ }
560
+ void run
549
561
  .then(result => this.#onPtyExit(record, generation, result))
550
562
  .catch(error =>
551
563
  this.#settle(record, generation, undefined, error instanceof Error ? error.message : String(error)),
552
564
  );
553
565
 
554
566
  if (process.platform === "win32") return;
567
+ const pidPath = path.join(record.dir, "process.pid");
555
568
  const deadline = Date.now() + 5_000;
556
569
  const pidFile = Bun.file(pidPath);
557
570
  while (Date.now() < deadline && generation === record.generation) {
package/src/lsp/client.ts CHANGED
@@ -750,8 +750,14 @@ export async function getOrCreateClient(
750
750
 
751
751
  client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
752
752
 
753
- // Send initialized notification
753
+ // Finish the initialize handshake before publishing the client as ready.
754
754
  await sendNotification(client, "initialized", {}, signal);
755
+ await sendNotification(
756
+ client,
757
+ "workspace/didChangeConfiguration",
758
+ { settings: config.settings ?? {} },
759
+ signal,
760
+ );
755
761
 
756
762
  client.status = "ready";
757
763
  // Publish only after init succeeds: pre-init clients are reachable
package/src/main.ts CHANGED
@@ -636,6 +636,27 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
636
636
  return undefined;
637
637
  }
638
638
 
639
+ const SESSION_ID_ARG_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
640
+
641
+ export function normalizeContinueSessionArgs(parsed: Args, rawArgs?: readonly string[]): void {
642
+ if (!parsed.continue || parsed.resume || parsed.fork) return;
643
+
644
+ let message: string | undefined;
645
+ if (parsed.unrecognizedFlags.length === 0 && parsed.messages.length === 1) {
646
+ message = parsed.messages[0]?.trim();
647
+ } else if (rawArgs) {
648
+ const continueIndex = rawArgs.findIndex(arg => arg === "--continue" || arg === "-c");
649
+ message = rawArgs[continueIndex + 1]?.trim();
650
+ }
651
+ if (!message || !SESSION_ID_ARG_RE.test(message)) return;
652
+
653
+ const messageIndex = parsed.messages.indexOf(message);
654
+ if (messageIndex === -1) return;
655
+ parsed.resume = message;
656
+ parsed.continue = false;
657
+ parsed.messages.splice(messageIndex, 1);
658
+ }
659
+
639
660
  /** Resolves CLI session flags into an existing, forked, in-memory, or cancelled session manager. */
640
661
  export async function createSessionManager(
641
662
  parsed: Args,
@@ -665,6 +686,8 @@ export async function createSessionManager(
665
686
  if (parsed.noSession) {
666
687
  return SessionManager.inMemory();
667
688
  }
689
+ normalizeContinueSessionArgs(parsed);
690
+
668
691
  if (typeof parsed.resume === "string") {
669
692
  const sessionArg = parsed.resume;
670
693
  if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
@@ -1188,6 +1211,11 @@ export async function runRootCommand(
1188
1211
  );
1189
1212
  }
1190
1213
 
1214
+ // Resolve an explicit `--continue <id>` before extension flags are loaded.
1215
+ // Reading the token immediately after `--continue` distinguishes the session
1216
+ // id from UUID-shaped values owned by later extension flags.
1217
+ normalizeContinueSessionArgs(parsedArgs, rawArgs);
1218
+
1191
1219
  // Create session manager based on CLI flags. SessionResolutionError signals a
1192
1220
  // user-facing failure (unknown --resume/--fork id, non-interactive fork
1193
1221
  // prompt, --fork with --no-session): print + exit cleanly instead of letting
@@ -1386,6 +1414,7 @@ export async function runRootCommand(
1386
1414
  },
1387
1415
  };
1388
1416
  const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
1417
+ normalizeContinueSessionArgs(initialArgs, rawArgs);
1389
1418
  // Fail fast on stale/typo flags (e.g. `omp --list-models`) now that we
1390
1419
  // know the real extension flag set. Without this check the unrecognized
1391
1420
  // token gets silently consumed and any following positional leaks as the
@@ -11,10 +11,23 @@ export interface OAuthEndpoints {
11
11
  authorizationUrl: string;
12
12
  tokenUrl: string;
13
13
  clientId?: string;
14
+ /** Dynamic client registration endpoint advertised by the authorization server. */
15
+ registrationUrl?: string;
14
16
  scopes?: string;
15
17
  resource?: string;
16
18
  }
17
19
 
20
+ function readRegistrationUrl(metadata: Record<string, unknown>): string | undefined {
21
+ const value =
22
+ metadata.registration_endpoint ??
23
+ metadata.registrationEndpoint ??
24
+ metadata.registration_url ??
25
+ metadata.registrationUrl ??
26
+ metadata.registration_uri ??
27
+ metadata.registrationUri;
28
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
29
+ }
30
+
18
31
  export interface AuthDetectionResult {
19
32
  requiresAuth: boolean;
20
33
  authType?: "oauth" | "apikey" | "unknown";
@@ -102,7 +115,7 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
102
115
  (obj.resource_uri as string | undefined) ||
103
116
  (obj.resourceUri as string | undefined);
104
117
 
105
- return { authorizationUrl, tokenUrl, clientId, scopes, resource };
118
+ return { authorizationUrl, tokenUrl, registrationUrl: readRegistrationUrl(obj), clientId, scopes, resource };
106
119
  };
107
120
 
108
121
  const clientIdFromAuthUrl = (authorizationUrl: string): string | undefined => {
@@ -175,6 +188,10 @@ export function extractOAuthEndpoints(error: Error): OAuthEndpoints | null {
175
188
  return {
176
189
  authorizationUrl,
177
190
  tokenUrl,
191
+ registrationUrl:
192
+ challengeValues.get("registration_endpoint") ||
193
+ challengeValues.get("registration_url") ||
194
+ challengeValues.get("registration_uri"),
178
195
  clientId: challengeValues.get("client_id") || clientIdFromAuthUrl(authorizationUrl),
179
196
  scopes: challengeValues.get("scope") || challengeValues.get("scopes") || scopeFromAuthUrl(authorizationUrl),
180
197
  resource,
@@ -415,6 +432,7 @@ export async function discoverOAuthEndpoints(
415
432
  return {
416
433
  authorizationUrl: String(metadata.authorization_endpoint),
417
434
  tokenUrl: String(metadata.token_endpoint),
435
+ registrationUrl: readRegistrationUrl(metadata),
418
436
  clientId:
419
437
  typeof metadata.client_id === "string"
420
438
  ? metadata.client_id
@@ -438,6 +456,7 @@ export async function discoverOAuthEndpoints(
438
456
  return {
439
457
  authorizationUrl: oauthData.authorization_url || String(oauthData.authorizationUrl),
440
458
  tokenUrl: oauthData.token_url || String(oauthData.tokenUrl),
459
+ registrationUrl: readRegistrationUrl(oauthData),
441
460
  clientId:
442
461
  typeof oauthData.client_id === "string"
443
462
  ? oauthData.client_id
@@ -277,6 +277,8 @@ export interface MCPOAuthConfig {
277
277
  authorizationUrl: string;
278
278
  /** Token endpoint URL */
279
279
  tokenUrl: string;
280
+ /** Dynamic client registration endpoint advertised by the authorization server. */
281
+ registrationUrl?: string;
280
282
  /** Client ID (optional when already embedded in authorization URL) */
281
283
  clientId?: string;
282
284
  /** Client secret (optional for PKCE flows) */
@@ -561,7 +563,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
561
563
  * accept the later authorize request for the same scope set.
562
564
  */
563
565
  async #tryRegisterClient(redirectUri: string): Promise<void> {
564
- const registrationEndpoint = await this.#resolveRegistrationEndpoint();
566
+ const registrationEndpoint = this.config.registrationUrl ?? (await this.#resolveRegistrationEndpoint());
565
567
  if (!registrationEndpoint) return;
566
568
 
567
569
  try {