@oh-my-pi/pi-coding-agent 16.3.10 → 16.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/cli.js +3368 -3277
- package/dist/types/advisor/runtime.d.ts +11 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/discovery/helpers.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
- package/dist/types/modes/components/model-selector.d.ts +2 -1
- package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/modes/utils/context-usage.d.ts +0 -12
- package/dist/types/modes/workflow.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/bash-interactive.d.ts +1 -1
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +2 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +2 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +24 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/utils/local-date.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +145 -0
- package/src/advisor/runtime.ts +19 -0
- package/src/config/api-key-resolver.ts +7 -2
- package/src/config/model-discovery.ts +18 -2
- package/src/config/model-registry.ts +9 -0
- package/src/config/settings-schema.ts +11 -1
- package/src/config/settings.ts +11 -0
- package/src/discovery/builtin.ts +2 -1
- package/src/discovery/claude-plugins.ts +167 -46
- package/src/discovery/helpers.ts +16 -1
- package/src/edit/renderer.ts +20 -6
- package/src/eval/js/worker-core.ts +163 -6
- package/src/exec/bash-executor.ts +14 -9
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/registry-helpers.ts +9 -6
- package/src/modes/components/model-selector.ts +30 -6
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/components/status-line/component.ts +14 -2
- package/src/modes/controllers/command-controller.ts +13 -23
- package/src/modes/controllers/event-controller.ts +12 -12
- package/src/modes/controllers/extension-ui-controller.ts +6 -35
- package/src/modes/controllers/input-controller.ts +18 -2
- package/src/modes/controllers/mcp-command-controller.ts +10 -9
- package/src/modes/controllers/selector-controller.ts +16 -5
- package/src/modes/github-ref-autocomplete.ts +75 -0
- package/src/modes/interactive-mode.ts +54 -10
- package/src/modes/prompt-action-autocomplete.ts +35 -0
- package/src/modes/utils/context-usage.ts +58 -5
- package/src/modes/utils/hotkeys-markdown.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/modes/workflow.ts +14 -8
- package/src/prompts/system/plan-mode-active.md +5 -2
- package/src/prompts/system/system-prompt.md +1 -2
- package/src/prompts/system/title-system.md +10 -10
- package/src/prompts/system/workflow-notice.md +69 -50
- package/src/prompts/tools/bash.md +18 -7
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/read.md +4 -3
- package/src/sdk.ts +11 -0
- package/src/session/agent-session.ts +128 -14
- package/src/system-prompt.test.ts +34 -1
- package/src/system-prompt.ts +27 -10
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash-skill-urls.ts +39 -7
- package/src/tools/bash.ts +69 -39
- package/src/tools/browser/launch.ts +31 -4
- package/src/tools/grep.ts +53 -14
- package/src/tools/index.ts +11 -0
- package/src/tools/path-utils.ts +46 -1
- package/src/tools/read.ts +108 -50
- package/src/utils/local-date.ts +7 -0
- package/src/utils/open.ts +36 -10
- package/src/utils/title-generator.ts +40 -65
- package/src/prompts/system/title-system-marker.md +0 -17
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import { isMainThread } from "node:worker_threads";
|
|
2
|
+
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
1
3
|
import { ToolError } from "../../tools/tool-errors";
|
|
2
4
|
import { JsRuntime, type RuntimeHooks } from "./shared/runtime";
|
|
3
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
RunErrorPayload,
|
|
7
|
+
SessionSnapshot,
|
|
8
|
+
ToolReply,
|
|
9
|
+
Transport,
|
|
10
|
+
WorkerInbound,
|
|
11
|
+
WorkerOutbound,
|
|
12
|
+
} from "./worker-protocol";
|
|
4
13
|
|
|
5
14
|
interface PendingTool {
|
|
6
15
|
runId: string;
|
|
@@ -10,9 +19,17 @@ interface PendingTool {
|
|
|
10
19
|
|
|
11
20
|
interface ActiveRun {
|
|
12
21
|
runId: string;
|
|
22
|
+
filename: string;
|
|
13
23
|
pendingTools: Map<string, PendingTool>;
|
|
24
|
+
/** Rejections floated by this run's cell code, captured before its result was sent. */
|
|
25
|
+
floatingRejections: unknown[];
|
|
14
26
|
}
|
|
15
27
|
|
|
28
|
+
type RunResult = Extract<WorkerOutbound, { type: "result" }>;
|
|
29
|
+
|
|
30
|
+
/** Finished-cell filenames retained for attributing rejections that surface after the run settled. */
|
|
31
|
+
const RECENT_CELL_FILES_MAX = 256;
|
|
32
|
+
|
|
16
33
|
function errorPayload(error: unknown): RunErrorPayload {
|
|
17
34
|
if (error instanceof Error) {
|
|
18
35
|
return {
|
|
@@ -34,15 +51,134 @@ function errorFromPayload(payload: RunErrorPayload): Error {
|
|
|
34
51
|
return error;
|
|
35
52
|
}
|
|
36
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Fold rejections floated by cell code into the run result: an otherwise
|
|
56
|
+
* successful run fails with the first floating rejection (an unawaited promise
|
|
57
|
+
* failing is a cell failure, not a success with noise); the rest surface as
|
|
58
|
+
* output text so nothing is silently dropped.
|
|
59
|
+
*/
|
|
60
|
+
function foldFloatingRejections(active: ActiveRun, result: RunResult, hooks: RuntimeHooks): RunResult {
|
|
61
|
+
const rejections = active.floatingRejections;
|
|
62
|
+
if (rejections.length === 0) return result;
|
|
63
|
+
let folded = result;
|
|
64
|
+
let reported = rejections;
|
|
65
|
+
if (result.ok) {
|
|
66
|
+
const error = errorPayload(rejections[0]);
|
|
67
|
+
error.message = `Unhandled rejection (missing await?): ${error.message}`;
|
|
68
|
+
folded = { type: "result", runId: active.runId, ok: false, error };
|
|
69
|
+
reported = rejections.slice(1);
|
|
70
|
+
}
|
|
71
|
+
for (const reason of reported) {
|
|
72
|
+
const payload = errorPayload(reason);
|
|
73
|
+
hooks.onText(`[unhandled rejection] ${payload.name ?? "Error"}: ${payload.message}\n`);
|
|
74
|
+
}
|
|
75
|
+
return folded;
|
|
76
|
+
}
|
|
77
|
+
|
|
37
78
|
export class WorkerCore {
|
|
38
79
|
#transport: Transport;
|
|
39
80
|
#runtime: JsRuntime | null = null;
|
|
40
81
|
#runs = new Map<string, ActiveRun>();
|
|
82
|
+
#recentCellFiles = new Set<string>();
|
|
41
83
|
#unsubscribe: () => void;
|
|
84
|
+
#uninstallRejectionGuard: () => void;
|
|
42
85
|
|
|
43
86
|
constructor(transport: Transport) {
|
|
44
87
|
this.#transport = transport;
|
|
45
88
|
this.#unsubscribe = transport.onMessage(msg => this.#handle(msg));
|
|
89
|
+
this.#uninstallRejectionGuard = this.#installRejectionGuard();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Capture unhandled rejections floated by eval-cell code (unawaited async
|
|
94
|
+
* calls) so they fail the owning run instead of tearing down the worker or —
|
|
95
|
+
* via the global postmortem handler — the whole session. On the main thread
|
|
96
|
+
* (inline fallback) only cell-attributable rejections are consumed; in the
|
|
97
|
+
* dedicated worker realm a rejection during a live run is cell activity even
|
|
98
|
+
* without a usable stack, while anything else keeps its default fatality.
|
|
99
|
+
*/
|
|
100
|
+
#installRejectionGuard(): () => void {
|
|
101
|
+
if (isMainThread) {
|
|
102
|
+
return postmortem.interceptUnhandledRejections(reason => this.#consumeRejection(reason));
|
|
103
|
+
}
|
|
104
|
+
const onRejection = (reason: unknown): void => {
|
|
105
|
+
if (this.#consumeRejection(reason)) return;
|
|
106
|
+
// Not cell-attributable: restore default fatality. Rethrowing from a
|
|
107
|
+
// timer surfaces it as an uncaught exception, which reaches the host
|
|
108
|
+
// as a worker `error` event exactly like an unhandled rejection did
|
|
109
|
+
// before this listener existed.
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
throw reason;
|
|
112
|
+
}, 0);
|
|
113
|
+
};
|
|
114
|
+
process.on("unhandledRejection", onRejection);
|
|
115
|
+
return () => {
|
|
116
|
+
process.off("unhandledRejection", onRejection);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Attribute an unhandled rejection to eval-cell code. Live runs are stashed
|
|
122
|
+
* on the run (folded into its result after the settle drain); finished cells
|
|
123
|
+
* downgrade to a host-side warn log. Returns false when the rejection is not
|
|
124
|
+
* cell activity and must keep the default fatal path.
|
|
125
|
+
*/
|
|
126
|
+
#consumeRejection(reason: unknown): boolean {
|
|
127
|
+
const stack = reason instanceof Error && typeof reason.stack === "string" ? reason.stack : undefined;
|
|
128
|
+
if (stack) {
|
|
129
|
+
// The stack can name several cells (helper defined by an earlier cell,
|
|
130
|
+
// called from the live one); the outermost matching frame is the caller
|
|
131
|
+
// that owns the floating promise.
|
|
132
|
+
let owner: ActiveRun | undefined;
|
|
133
|
+
let ownerIndex = -1;
|
|
134
|
+
for (const run of this.#runs.values()) {
|
|
135
|
+
const index = stack.lastIndexOf(run.filename);
|
|
136
|
+
if (index > ownerIndex) {
|
|
137
|
+
ownerIndex = index;
|
|
138
|
+
owner = run;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (owner) {
|
|
142
|
+
owner.floatingRejections.push(reason);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
let recent: string | undefined;
|
|
146
|
+
let recentIndex = -1;
|
|
147
|
+
for (const filename of this.#recentCellFiles) {
|
|
148
|
+
const index = stack.lastIndexOf(filename);
|
|
149
|
+
if (index > recentIndex) {
|
|
150
|
+
recentIndex = index;
|
|
151
|
+
recent = filename;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (recent) {
|
|
155
|
+
this.#transport.send({
|
|
156
|
+
type: "log",
|
|
157
|
+
level: "warn",
|
|
158
|
+
msg: "Unhandled rejection from a finished eval cell (missing await?)",
|
|
159
|
+
meta: { filename: recent, error: errorPayload(reason) },
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!isMainThread && this.#runs.size > 0) {
|
|
165
|
+
// Dedicated eval worker: during a live run, a rejection without a cell
|
|
166
|
+
// frame (e.g. `Promise.reject("msg")` or a library-created reason) is
|
|
167
|
+
// still cell activity — nothing else runs user code in this realm.
|
|
168
|
+
if (this.#runs.size === 1) {
|
|
169
|
+
const only = this.#runs.values().next().value;
|
|
170
|
+
only?.floatingRejections.push(reason);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
this.#transport.send({
|
|
174
|
+
type: "log",
|
|
175
|
+
level: "warn",
|
|
176
|
+
msg: "Unhandled rejection during concurrent eval runs; cannot attribute to a cell",
|
|
177
|
+
meta: { error: errorPayload(reason) },
|
|
178
|
+
});
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
46
182
|
}
|
|
47
183
|
|
|
48
184
|
#handle(msg: WorkerInbound): void {
|
|
@@ -77,23 +213,42 @@ export class WorkerCore {
|
|
|
77
213
|
}
|
|
78
214
|
|
|
79
215
|
async #runOne(runId: string, code: string, filename: string, snapshot: SessionSnapshot): Promise<void> {
|
|
80
|
-
const
|
|
81
|
-
runtime.setCwd(snapshot.cwd);
|
|
82
|
-
const active: ActiveRun = { runId, pendingTools: new Map() };
|
|
216
|
+
const active: ActiveRun = { runId, filename, pendingTools: new Map(), floatingRejections: [] };
|
|
83
217
|
this.#runs.set(runId, active);
|
|
84
218
|
const hooks: RuntimeHooks = {
|
|
85
219
|
onText: chunk => this.#transport.send({ type: "text", runId, chunk }),
|
|
86
220
|
onDisplay: output => this.#transport.send({ type: "display", runId, output }),
|
|
87
221
|
callTool: (name, args) => this.#callTool(active, name, args),
|
|
88
222
|
};
|
|
223
|
+
let result: RunResult;
|
|
89
224
|
try {
|
|
225
|
+
const runtime = this.#ensureRuntime(snapshot);
|
|
226
|
+
runtime.setCwd(snapshot.cwd);
|
|
90
227
|
const value = await runtime.run(code, filename, hooks, { runId, cwd: snapshot.cwd });
|
|
91
228
|
runtime.displayValue(value, hooks);
|
|
92
|
-
|
|
229
|
+
result = { type: "result", runId, ok: true };
|
|
93
230
|
} catch (error) {
|
|
94
|
-
|
|
231
|
+
result = { type: "result", runId, ok: false, error: errorPayload(error) };
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
// One event-loop turn so rejections the cell already floated surface
|
|
235
|
+
// while this run can still own them (rejection callbacks run before
|
|
236
|
+
// timers fire).
|
|
237
|
+
await Bun.sleep(0);
|
|
238
|
+
result = foldFloatingRejections(active, result, hooks);
|
|
95
239
|
} finally {
|
|
96
240
|
this.#runs.delete(runId);
|
|
241
|
+
this.#rememberCellFile(filename);
|
|
242
|
+
this.#transport.send(result);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#rememberCellFile(filename: string): void {
|
|
247
|
+
this.#recentCellFiles.delete(filename);
|
|
248
|
+
this.#recentCellFiles.add(filename);
|
|
249
|
+
if (this.#recentCellFiles.size > RECENT_CELL_FILES_MAX) {
|
|
250
|
+
const oldest = this.#recentCellFiles.values().next().value;
|
|
251
|
+
if (oldest !== undefined) this.#recentCellFiles.delete(oldest);
|
|
97
252
|
}
|
|
98
253
|
}
|
|
99
254
|
|
|
@@ -127,6 +282,7 @@ export class WorkerCore {
|
|
|
127
282
|
this.#runtime?.dispose?.();
|
|
128
283
|
this.#runtime = null;
|
|
129
284
|
this.#transport.send({ type: "closed" });
|
|
285
|
+
this.#uninstallRejectionGuard();
|
|
130
286
|
this.#unsubscribe();
|
|
131
287
|
this.#transport.close();
|
|
132
288
|
}
|
|
@@ -141,6 +297,7 @@ export class WorkerCore {
|
|
|
141
297
|
this.#runs.clear();
|
|
142
298
|
this.#runtime?.dispose?.();
|
|
143
299
|
this.#runtime = null;
|
|
300
|
+
this.#uninstallRejectionGuard();
|
|
144
301
|
this.#unsubscribe();
|
|
145
302
|
try {
|
|
146
303
|
this.#transport.close();
|
|
@@ -14,6 +14,7 @@ import { buildNonInteractiveEnv } from "./non-interactive-env";
|
|
|
14
14
|
|
|
15
15
|
export interface BashExecutorOptions {
|
|
16
16
|
cwd?: string;
|
|
17
|
+
/** Milliseconds before aborting the command; 0 disables the executor deadline. */
|
|
17
18
|
timeout?: number;
|
|
18
19
|
onChunk?: (chunk: string) => void;
|
|
19
20
|
chunkThrottleMs?: number;
|
|
@@ -296,11 +297,15 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
296
297
|
|
|
297
298
|
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
298
299
|
const timeoutDeferred = Promise.withResolvers<"timeout">();
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
const requestedTimeoutMs = options?.timeout;
|
|
301
|
+
const deadlineTimeoutMs = requestedTimeoutMs === 0 ? undefined : Math.max(1_000, requestedTimeoutMs ?? 300_000);
|
|
302
|
+
const nativeTimeoutMs = requestedTimeoutMs !== undefined && requestedTimeoutMs > 0 ? requestedTimeoutMs : undefined;
|
|
303
|
+
if (deadlineTimeoutMs !== undefined) {
|
|
304
|
+
timeoutTimer = setTimeout(() => {
|
|
305
|
+
abortCurrentExecution();
|
|
306
|
+
timeoutDeferred.resolve("timeout");
|
|
307
|
+
}, deadlineTimeoutMs);
|
|
308
|
+
}
|
|
304
309
|
|
|
305
310
|
let resetSession = false;
|
|
306
311
|
|
|
@@ -311,7 +316,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
311
316
|
command: finalCommand,
|
|
312
317
|
cwd: commandCwd,
|
|
313
318
|
env: commandEnv,
|
|
314
|
-
timeoutMs:
|
|
319
|
+
timeoutMs: nativeTimeoutMs,
|
|
315
320
|
signal: runAbortController.signal,
|
|
316
321
|
},
|
|
317
322
|
(err, chunk) => {
|
|
@@ -328,7 +333,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
328
333
|
sessionEnv: shellEnv,
|
|
329
334
|
snapshotPath: snapshotPath ?? undefined,
|
|
330
335
|
minimizer,
|
|
331
|
-
timeoutMs:
|
|
336
|
+
timeoutMs: nativeTimeoutMs,
|
|
332
337
|
signal: runAbortController.signal,
|
|
333
338
|
},
|
|
334
339
|
(err, chunk) => {
|
|
@@ -359,8 +364,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
359
364
|
exitCode: undefined,
|
|
360
365
|
cancelled: true,
|
|
361
366
|
...(await sink.dump(
|
|
362
|
-
winner.kind === "timeout"
|
|
363
|
-
? `Command timed out after ${Math.round(
|
|
367
|
+
winner.kind === "timeout" && deadlineTimeoutMs !== undefined
|
|
368
|
+
? `Command timed out after ${Math.round(deadlineTimeoutMs / 1000)} seconds`
|
|
364
369
|
: "Command cancelled",
|
|
365
370
|
)),
|
|
366
371
|
};
|
|
@@ -1059,7 +1059,8 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
|
|
|
1059
1059
|
let resolved: string | null = null;
|
|
1060
1060
|
let nextFollowsBareDependencies = followBareDependencies;
|
|
1061
1061
|
if (specifier.startsWith(".")) {
|
|
1062
|
-
|
|
1062
|
+
const candidate = Bun.resolveSync(specifier, dir);
|
|
1063
|
+
resolved = hasSourceModuleExtension(candidate) ? await realpathOrSelf(candidate) : null;
|
|
1063
1064
|
} else if (specifier.startsWith("#")) {
|
|
1064
1065
|
resolved = await resolvePackageImportSpecifier(specifier, file);
|
|
1065
1066
|
} else if (
|
|
@@ -1078,7 +1079,10 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
|
|
|
1078
1079
|
dependencyExtension === ".cjs" ||
|
|
1079
1080
|
dependencyExtension === ".cts" ||
|
|
1080
1081
|
((dependencyExtension === ".js" || dependencyExtension === ".jsx") && manifest?.type !== "module");
|
|
1081
|
-
resolved =
|
|
1082
|
+
resolved =
|
|
1083
|
+
dependencyEntry && hasSourceModuleExtension(dependencyEntry) && !isCommonJsEntry
|
|
1084
|
+
? await realpathOrSelf(dependencyEntry)
|
|
1085
|
+
: null;
|
|
1082
1086
|
nextFollowsBareDependencies = false;
|
|
1083
1087
|
}
|
|
1084
1088
|
if (resolved && !modules.has(resolved)) {
|
|
@@ -196,19 +196,20 @@ export function parseMarketplaceCatalog(content: string, filePath: string): Mark
|
|
|
196
196
|
* Catalog paths tried in priority order: omp-namespaced override first, then
|
|
197
197
|
* the Claude Code-compatible fallback so existing marketplaces keep loading.
|
|
198
198
|
*/
|
|
199
|
-
const CATALOG_RELATIVE_PATHS: readonly string[] = [
|
|
200
|
-
path.join(".omp-plugin", "marketplace.json"),
|
|
201
|
-
path.join(".claude-plugin", "marketplace.json"),
|
|
202
|
-
];
|
|
199
|
+
const CATALOG_RELATIVE_PATHS: readonly string[] = [".omp-plugin/marketplace.json", ".claude-plugin/marketplace.json"];
|
|
203
200
|
|
|
204
|
-
async function readMarketplaceCatalog(
|
|
201
|
+
async function readMarketplaceCatalog(
|
|
202
|
+
root: string,
|
|
203
|
+
options: { relativeDisplayPaths?: boolean } = {},
|
|
204
|
+
): Promise<{ catalogPath: string; displayPath: string; content: string }> {
|
|
205
205
|
const tried: string[] = [];
|
|
206
206
|
for (const rel of CATALOG_RELATIVE_PATHS) {
|
|
207
|
-
const catalogPath = path.join(root, rel);
|
|
208
|
-
|
|
207
|
+
const catalogPath = path.join(root, ...rel.split("/"));
|
|
208
|
+
const displayPath = options.relativeDisplayPaths ? rel : catalogPath;
|
|
209
|
+
tried.push(displayPath);
|
|
209
210
|
try {
|
|
210
211
|
const content = await Bun.file(catalogPath).text();
|
|
211
|
-
return { catalogPath, content };
|
|
212
|
+
return { catalogPath, displayPath, content };
|
|
212
213
|
} catch (err) {
|
|
213
214
|
if (isEnoent(err)) continue;
|
|
214
215
|
throw err;
|
|
@@ -252,11 +253,11 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis
|
|
|
252
253
|
|
|
253
254
|
if (type === "github") {
|
|
254
255
|
const url = `https://github.com/${source}.git`;
|
|
255
|
-
return cloneAndReadCatalog(url, cacheDir);
|
|
256
|
+
return cloneAndReadCatalog(url, source, cacheDir);
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
if (type === "git") {
|
|
259
|
-
return cloneAndReadCatalog(source, cacheDir);
|
|
260
|
+
return cloneAndReadCatalog(source, source, cacheDir);
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
// type === "url"
|
|
@@ -284,7 +285,7 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis
|
|
|
284
285
|
* responsible for promoting the clone to its final cache location via
|
|
285
286
|
* `promoteCloneToCache` after any duplicate/drift checks pass.
|
|
286
287
|
*/
|
|
287
|
-
async function cloneAndReadCatalog(url: string, cacheDir: string): Promise<FetchResult> {
|
|
288
|
+
async function cloneAndReadCatalog(url: string, source: string, cacheDir: string): Promise<FetchResult> {
|
|
288
289
|
const tmpDir = path.join(cacheDir, `.tmp-clone-${Date.now()}`);
|
|
289
290
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
290
291
|
|
|
@@ -292,12 +293,12 @@ async function cloneAndReadCatalog(url: string, cacheDir: string): Promise<Fetch
|
|
|
292
293
|
await git.clone(url, tmpDir);
|
|
293
294
|
|
|
294
295
|
try {
|
|
295
|
-
const {
|
|
296
|
-
const catalog = parseMarketplaceCatalog(content,
|
|
296
|
+
const { displayPath, content } = await readMarketplaceCatalog(tmpDir, { relativeDisplayPaths: true });
|
|
297
|
+
const catalog = parseMarketplaceCatalog(content, displayPath);
|
|
297
298
|
return { catalog, clonePath: tmpDir };
|
|
298
299
|
} catch (err) {
|
|
299
300
|
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
300
|
-
throw new Error(`Cloned repository ${url}: ${(err as Error).message}`, { cause: err });
|
|
301
|
+
throw new Error(`Cloned repository ${url}: ${(err as Error).message} (source: ${source})`, { cause: err });
|
|
301
302
|
}
|
|
302
303
|
}
|
|
303
304
|
|
|
@@ -33,7 +33,7 @@ export interface SessionStartEvent {
|
|
|
33
33
|
export interface SessionBeforeSwitchEvent {
|
|
34
34
|
type: "session_before_switch";
|
|
35
35
|
/** Reason for the switch */
|
|
36
|
-
reason: "new" | "resume" | "fork";
|
|
36
|
+
reason: "new" | "resume" | "fork" | "handoff";
|
|
37
37
|
/** Session file we're switching to (only for "resume") */
|
|
38
38
|
targetSessionFile?: string;
|
|
39
39
|
}
|
|
@@ -42,7 +42,7 @@ export interface SessionBeforeSwitchEvent {
|
|
|
42
42
|
export interface SessionSwitchEvent {
|
|
43
43
|
type: "session_switch";
|
|
44
44
|
/** Reason for the switch */
|
|
45
|
-
reason: "new" | "resume" | "fork";
|
|
45
|
+
reason: "new" | "resume" | "fork" | "handoff";
|
|
46
46
|
/** Session file we came from */
|
|
47
47
|
previousSessionFile: string | undefined;
|
|
48
48
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { afterAll, afterEach, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { AgentRegistry } from "../../registry/agent-registry";
|
|
6
|
+
import type { AgentSession } from "../../session/agent-session";
|
|
7
|
+
import { ArtifactManager } from "../../session/artifacts";
|
|
8
|
+
import { AgentProtocolHandler } from "../agent-protocol";
|
|
9
|
+
import { resetRegisteredArtifactDirsForTests } from "../registry-helpers";
|
|
10
|
+
|
|
11
|
+
const tempDir = TempDir.createSync("omp-nested-agent-repro-");
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
AgentRegistry.resetGlobalForTests();
|
|
14
|
+
resetRegisteredArtifactDirsForTests();
|
|
15
|
+
});
|
|
16
|
+
afterAll(() => {
|
|
17
|
+
tempDir.removeSync();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("agent:// resolves a depth-2 subagent's .md output while its session is live and artifact-manager-adopted", async () => {
|
|
21
|
+
const root = tempDir.path();
|
|
22
|
+
const rootSessionFile = path.join(root, "session.jsonl");
|
|
23
|
+
const rootArtifactsDir = rootSessionFile.slice(0, -6);
|
|
24
|
+
await fs.mkdir(rootArtifactsDir, { recursive: true });
|
|
25
|
+
// Every subagent adopts the root ArtifactManager and reports its dir.
|
|
26
|
+
const sharedArtifactManager = new ArtifactManager(rootArtifactsDir);
|
|
27
|
+
|
|
28
|
+
// A depth-1 subagent's OWN children are written under its own
|
|
29
|
+
// sessionFile.slice(0, -6) (task/index.ts), i.e. one level deeper.
|
|
30
|
+
const midSessionFile = path.join(rootArtifactsDir, "CodexDeepDive.jsonl");
|
|
31
|
+
const midOwnArtifactsDir = midSessionFile.slice(0, -6);
|
|
32
|
+
await fs.mkdir(midOwnArtifactsDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
const grandchildId = "CodexDeepDive.GraphStore";
|
|
35
|
+
const grandchildSessionFile = path.join(midOwnArtifactsDir, `${grandchildId}.jsonl`);
|
|
36
|
+
await fs.writeFile(path.join(midOwnArtifactsDir, `${grandchildId}.md`), "full report content");
|
|
37
|
+
|
|
38
|
+
const fakeSession = {
|
|
39
|
+
sessionManager: { getArtifactsDir: () => sharedArtifactManager.dir },
|
|
40
|
+
} as unknown as AgentSession;
|
|
41
|
+
const registry = AgentRegistry.global();
|
|
42
|
+
registry.register({
|
|
43
|
+
id: "Main",
|
|
44
|
+
displayName: "main",
|
|
45
|
+
kind: "main",
|
|
46
|
+
session: fakeSession,
|
|
47
|
+
sessionFile: rootSessionFile,
|
|
48
|
+
});
|
|
49
|
+
registry.register({
|
|
50
|
+
id: "CodexDeepDive",
|
|
51
|
+
displayName: "sub",
|
|
52
|
+
kind: "sub",
|
|
53
|
+
parentId: "Main",
|
|
54
|
+
session: fakeSession,
|
|
55
|
+
sessionFile: midSessionFile,
|
|
56
|
+
});
|
|
57
|
+
registry.register({
|
|
58
|
+
id: grandchildId,
|
|
59
|
+
displayName: "sub",
|
|
60
|
+
kind: "sub",
|
|
61
|
+
parentId: "CodexDeepDive",
|
|
62
|
+
session: fakeSession,
|
|
63
|
+
sessionFile: grandchildSessionFile,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const resource = await new AgentProtocolHandler().resolve(new URL(`agent://${grandchildId}`) as never);
|
|
67
|
+
expect(resource.content).toBe("full report content");
|
|
68
|
+
});
|