@oh-my-pi/pi-coding-agent 17.0.3 → 17.0.5
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 +88 -35
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/task/types.d.ts +6 -6
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +18 -111
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/session/session-loader.ts +31 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/stt/recorder.ts +68 -55
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/types.ts +11 -8
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
package/src/dap/types.ts
CHANGED
|
@@ -484,10 +484,9 @@ export interface DapAdapterConfig {
|
|
|
484
484
|
launchDefaults?: Record<string, unknown>;
|
|
485
485
|
attachDefaults?: Record<string, unknown>;
|
|
486
486
|
/** "stdio" (default): communicate via stdin/stdout pipes.
|
|
487
|
-
* "socket": adapter
|
|
488
|
-
*
|
|
489
|
-
|
|
490
|
-
connectMode?: "stdio" | "socket";
|
|
487
|
+
* "socket": adapter-specific socket launch (currently Delve).
|
|
488
|
+
* "tcp": spawn a DAP server with `${port}` substituted in `args`, then connect to it. */
|
|
489
|
+
connectMode?: "stdio" | "socket" | "tcp";
|
|
491
490
|
/** When true, the adapter accepts a directory as the launch `program`
|
|
492
491
|
* (e.g. dlv treats it as a Go package path). When false/undefined, the
|
|
493
492
|
* debug tool rejects directory programs upfront. */
|
|
@@ -504,7 +503,7 @@ export interface DapResolvedAdapter {
|
|
|
504
503
|
rootMarkers: string[];
|
|
505
504
|
launchDefaults: Record<string, unknown>;
|
|
506
505
|
attachDefaults: Record<string, unknown>;
|
|
507
|
-
connectMode: "stdio" | "socket";
|
|
506
|
+
connectMode: "stdio" | "socket" | "tcp";
|
|
508
507
|
acceptsDirectoryProgram: boolean;
|
|
509
508
|
}
|
|
510
509
|
|
|
@@ -581,6 +580,8 @@ export interface DapSessionSummary {
|
|
|
581
580
|
outputTruncated: boolean;
|
|
582
581
|
exitCode?: number;
|
|
583
582
|
needsConfigurationDone: boolean;
|
|
583
|
+
parentSessionId?: string;
|
|
584
|
+
childSessionIds?: string[];
|
|
584
585
|
}
|
|
585
586
|
|
|
586
587
|
export interface DapContinueOutcome {
|
package/src/discovery/agents.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Agent Dirs (.agent/.agents) Provider
|
|
3
3
|
*
|
|
4
4
|
* Loads skills, rules, prompts, commands, context files, and system prompts
|
|
5
5
|
* from .agent/ and .agents/ directories at both user (~/) and project levels.
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "./helpers";
|
|
25
25
|
|
|
26
26
|
const PROVIDER_ID = "agents";
|
|
27
|
-
const DISPLAY_NAME = "
|
|
27
|
+
const DISPLAY_NAME = "Agent Dirs (.agent/.agents)";
|
|
28
28
|
const PRIORITY = 70;
|
|
29
29
|
const AGENT_DIR_CANDIDATES = [".agent", ".agents"] as const;
|
|
30
30
|
|
|
@@ -534,8 +534,9 @@ export class ExtensionRunner {
|
|
|
534
534
|
return undefined;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
-
|
|
538
|
-
|
|
537
|
+
/** Creates an extension context, optionally scoped to a provider request model. */
|
|
538
|
+
createContext(model?: Model): ExtensionContext {
|
|
539
|
+
const getModel = model ? () => model : this.#getModel;
|
|
539
540
|
return {
|
|
540
541
|
ui: this.#uiContext,
|
|
541
542
|
getContextUsage: () => this.#getContextUsageFn(),
|
|
@@ -965,8 +966,9 @@ export class ExtensionRunner {
|
|
|
965
966
|
return currentMessages;
|
|
966
967
|
}
|
|
967
968
|
|
|
968
|
-
|
|
969
|
-
|
|
969
|
+
/** Runs request payload hooks with the model used for that provider request. */
|
|
970
|
+
async emitBeforeProviderRequest(payload: unknown, model?: Model): Promise<BeforeProviderRequestEventResult> {
|
|
971
|
+
const ctx = this.createContext(model);
|
|
970
972
|
let currentPayload = payload;
|
|
971
973
|
|
|
972
974
|
for (const ext of this.extensions) {
|
|
@@ -22,8 +22,10 @@ import {
|
|
|
22
22
|
getAgentDbPath,
|
|
23
23
|
getAgentDir,
|
|
24
24
|
getProjectDir,
|
|
25
|
+
isCompiledBinary,
|
|
25
26
|
parseFrontmatter as parseOmpFrontmatter,
|
|
26
27
|
} from "@oh-my-pi/pi-utils";
|
|
28
|
+
import { getPackageDir as getOmpPackageDir } from "../config";
|
|
27
29
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
28
30
|
import { type SettingPath, Settings } from "../config/settings";
|
|
29
31
|
import { EditTool } from "../edit";
|
|
@@ -1334,6 +1336,30 @@ export function readStoredCredential(provider: string): AuthCredential | undefin
|
|
|
1334
1336
|
return storage.get(provider);
|
|
1335
1337
|
}
|
|
1336
1338
|
|
|
1339
|
+
// Pi SDK path helpers. `export * from "../index"` above only forwards
|
|
1340
|
+
// `getAgentDir`; `getProjectDir` (a `@oh-my-pi/pi-utils` helper) and
|
|
1341
|
+
// `getPackageDir` are absent from that barrel, so legacy extensions importing
|
|
1342
|
+
// either fail Bun's static export check during validation (issue #5968).
|
|
1343
|
+
export { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Coding-agent package install directory, matching pi's string-valued
|
|
1347
|
+
* `getPackageDir()` contract (extensions do `path.join(getPackageDir(), ...)`
|
|
1348
|
+
* to auto-allow bundled docs/resources).
|
|
1349
|
+
*
|
|
1350
|
+
* omp's canonical `getPackageDir()` (`../config`) returns `undefined` inside a
|
|
1351
|
+
* `bun --compile` binary — `import.meta.dir` is `/$bunfs/root` and no owning
|
|
1352
|
+
* `package.json` exists (issue #1423). Returning `undefined` there would crash
|
|
1353
|
+
* every legacy `path.join(getPackageDir(), ...)` at runtime in the shipped
|
|
1354
|
+
* binary, the primary distribution. So fall back to the executable's own
|
|
1355
|
+
* directory in compiled mode, where the binary *is* the install root. The
|
|
1356
|
+
* `PI_PACKAGE_DIR` override and dev/source/npm-dist walk-up still win via the
|
|
1357
|
+
* canonical helper.
|
|
1358
|
+
*/
|
|
1359
|
+
export function getPackageDir(): string {
|
|
1360
|
+
return getOmpPackageDir() ?? (isCompiledBinary() ? path.dirname(process.execPath) : process.cwd());
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1337
1363
|
export * from "../index";
|
|
1338
1364
|
export { formatBytes as formatSize } from "../tools/render-utils";
|
|
1339
1365
|
export { Type } from "./typebox";
|
package/src/launch/broker.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { Process, type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
|
|
6
6
|
import { isEexist, isEnoent, logger, postmortem, procmgr, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
7
|
+
import { hostHasInheritableConsole } from "../eval/py/spawn-options";
|
|
7
8
|
import { truncateHead, truncateHeadBytes, truncateTail, truncateTailBytes } from "../session/streaming-output";
|
|
8
9
|
import { workerEnvFromParent } from "../subprocess/worker-client";
|
|
9
10
|
import { daemonBrokerEndpoint } from "./paths";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
parseDaemonSpec,
|
|
25
26
|
parseDaemonWireRequest,
|
|
26
27
|
} from "./protocol";
|
|
28
|
+
import { resolveDaemonSpawnOptions } from "./spawn-options";
|
|
27
29
|
|
|
28
30
|
const DEFAULT_IDLE_GRACE_MS = 3_000;
|
|
29
31
|
const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
@@ -36,6 +38,10 @@ const PID_FILE = "broker.pid";
|
|
|
36
38
|
const META_FILE = "meta.json";
|
|
37
39
|
const LOG_FILE = "output.log";
|
|
38
40
|
const PREVIOUS_LOG_FILE = "output.previous.log";
|
|
41
|
+
const DAEMON_SPAWN_OPTIONS = resolveDaemonSpawnOptions({
|
|
42
|
+
platform: process.platform,
|
|
43
|
+
hostHasInheritableConsole: hostHasInheritableConsole(),
|
|
44
|
+
});
|
|
39
45
|
|
|
40
46
|
const SIGNAL_NUMBER: Record<DaemonSignal, number> = {
|
|
41
47
|
SIGINT: os.constants.signals.SIGINT,
|
|
@@ -537,6 +543,15 @@ class DaemonBroker {
|
|
|
537
543
|
if (error) record.log?.append(`PTY output error: ${error.message}\n`);
|
|
538
544
|
if (chunk) this.#onOutput(record, generation, chunk);
|
|
539
545
|
};
|
|
546
|
+
const started = Promise.withResolvers<number | undefined>();
|
|
547
|
+
const onStart = (error: Error | null, pid: number): void => {
|
|
548
|
+
if (error) {
|
|
549
|
+
record.log?.append(`PTY startup callback failed: ${error.message}\n`);
|
|
550
|
+
started.resolve(undefined);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
started.resolve(Number.isSafeInteger(pid) && pid > 0 ? pid : undefined);
|
|
554
|
+
};
|
|
540
555
|
let run: Promise<PtyRunResult>;
|
|
541
556
|
if (process.platform === "win32") {
|
|
542
557
|
run = session.startArgv(
|
|
@@ -546,41 +561,29 @@ class DaemonBroker {
|
|
|
546
561
|
...options,
|
|
547
562
|
},
|
|
548
563
|
onChunk,
|
|
564
|
+
onStart,
|
|
549
565
|
);
|
|
550
566
|
} else {
|
|
551
|
-
const pidPath = path.join(record.dir, "process.pid");
|
|
552
|
-
await fs.rm(pidPath, { force: true });
|
|
553
567
|
const argv = [record.spec.application, ...record.spec.args];
|
|
554
|
-
const command =
|
|
555
|
-
`printf '%s' "$$" > ${quoteShellArg(pidPath)}`,
|
|
556
|
-
`exec ${argv.map(quoteShellArg).join(" ")}`,
|
|
557
|
-
].join("; ");
|
|
568
|
+
const command = `exec ${argv.map(quoteShellArg).join(" ")}`;
|
|
558
569
|
const shell = procmgr.getShellConfig().shell;
|
|
559
|
-
run = session.start({ command, shell, ...options }, onChunk);
|
|
570
|
+
run = session.start({ command, shell, ...options }, onChunk, onStart);
|
|
560
571
|
}
|
|
561
|
-
void run
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
572
|
+
void run.then(
|
|
573
|
+
async result => {
|
|
574
|
+
await this.#onPtyExit(record, generation, result);
|
|
575
|
+
started.resolve(undefined);
|
|
576
|
+
},
|
|
577
|
+
async error => {
|
|
578
|
+
await this.#settle(record, generation, undefined, error instanceof Error ? error.message : String(error));
|
|
579
|
+
started.resolve(undefined);
|
|
580
|
+
},
|
|
581
|
+
);
|
|
566
582
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
while (Date.now() < deadline && generation === record.generation) {
|
|
572
|
-
try {
|
|
573
|
-
const pid = Number.parseInt((await pidFile.text()).trim(), 10);
|
|
574
|
-
if (Number.isSafeInteger(pid) && pid > 0) {
|
|
575
|
-
record.snapshot.pid = pid;
|
|
576
|
-
this.#persist(record);
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
} catch (error) {
|
|
580
|
-
if (!isEnoent(error)) throw error;
|
|
581
|
-
}
|
|
582
|
-
if (terminalState(record.snapshot.state)) return;
|
|
583
|
-
await Bun.sleep(20);
|
|
583
|
+
const pid = await started.promise;
|
|
584
|
+
if (pid !== undefined && generation === record.generation) {
|
|
585
|
+
record.snapshot.pid = pid;
|
|
586
|
+
this.#persist(record);
|
|
584
587
|
}
|
|
585
588
|
}
|
|
586
589
|
|
|
@@ -591,7 +594,7 @@ class DaemonBroker {
|
|
|
591
594
|
stdin: "pipe",
|
|
592
595
|
stdout: "pipe",
|
|
593
596
|
stderr: "pipe",
|
|
594
|
-
|
|
597
|
+
...DAEMON_SPAWN_OPTIONS,
|
|
595
598
|
});
|
|
596
599
|
record.process = process;
|
|
597
600
|
record.input = process.stdin;
|
|
@@ -614,7 +617,7 @@ class DaemonBroker {
|
|
|
614
617
|
cwd: record.spec.cwd,
|
|
615
618
|
env: workerEnvFromParent(record.spec.env),
|
|
616
619
|
stdio: ["ignore", output.fd, output.fd],
|
|
617
|
-
|
|
620
|
+
...DAEMON_SPAWN_OPTIONS,
|
|
618
621
|
});
|
|
619
622
|
record.process = process;
|
|
620
623
|
record.snapshot.pid = process.pid;
|
package/src/launch/client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as net from "node:net";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { isEexist, isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { hostHasInheritableConsole } from "../eval/py/spawn-options";
|
|
6
7
|
import { resolveWorkerSpawnCmd, workerEnvFromParent } from "../subprocess/worker-client";
|
|
7
8
|
import { daemonBrokerEndpoint, daemonRuntimeDir } from "./paths";
|
|
8
9
|
import {
|
|
@@ -16,10 +17,15 @@ import {
|
|
|
16
17
|
parseDaemonRpcResult,
|
|
17
18
|
parseDaemonWireResponse,
|
|
18
19
|
} from "./protocol";
|
|
20
|
+
import { resolveDaemonSpawnOptions } from "./spawn-options";
|
|
19
21
|
|
|
20
22
|
const CONNECT_TIMEOUT_MS = 10_000;
|
|
21
23
|
const CONNECT_RETRY_MS = 50;
|
|
22
24
|
const TOKEN_FILE = "broker.token";
|
|
25
|
+
const BROKER_SPAWN_OPTIONS = resolveDaemonSpawnOptions({
|
|
26
|
+
platform: process.platform,
|
|
27
|
+
hostHasInheritableConsole: hostHasInheritableConsole(),
|
|
28
|
+
});
|
|
23
29
|
|
|
24
30
|
interface PendingRequest {
|
|
25
31
|
operation: DaemonOperation;
|
|
@@ -228,7 +234,7 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
228
234
|
stdin: "ignore",
|
|
229
235
|
stdout: "ignore",
|
|
230
236
|
stderr: "ignore",
|
|
231
|
-
|
|
237
|
+
...BROKER_SPAWN_OPTIONS,
|
|
232
238
|
});
|
|
233
239
|
child.unref();
|
|
234
240
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { resolveDaemonSpawnOptions } from "./spawn-options";
|
|
3
|
+
|
|
4
|
+
describe("resolveDaemonSpawnOptions", () => {
|
|
5
|
+
it("hides Windows daemons when the host has no console", () => {
|
|
6
|
+
expect(
|
|
7
|
+
resolveDaemonSpawnOptions({
|
|
8
|
+
platform: "win32",
|
|
9
|
+
hostHasInheritableConsole: false,
|
|
10
|
+
}),
|
|
11
|
+
).toEqual({ detached: false, windowsHide: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("inherits the Windows host console instead of detaching", () => {
|
|
15
|
+
expect(
|
|
16
|
+
resolveDaemonSpawnOptions({
|
|
17
|
+
platform: "win32",
|
|
18
|
+
hostHasInheritableConsole: true,
|
|
19
|
+
}),
|
|
20
|
+
).toEqual({ detached: false, windowsHide: false });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("keeps POSIX daemons in their own session", () => {
|
|
24
|
+
expect(
|
|
25
|
+
resolveDaemonSpawnOptions({
|
|
26
|
+
platform: "linux",
|
|
27
|
+
hostHasInheritableConsole: false,
|
|
28
|
+
}),
|
|
29
|
+
).toEqual({ detached: true });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Platform-specific options for the launch broker and its non-PTY children. */
|
|
2
|
+
export interface DaemonSpawnOptions {
|
|
3
|
+
detached: boolean;
|
|
4
|
+
windowsHide?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Keep launch processes headless without discarding an inheritable Windows console. */
|
|
8
|
+
export function resolveDaemonSpawnOptions(opts: {
|
|
9
|
+
platform: NodeJS.Platform;
|
|
10
|
+
hostHasInheritableConsole: boolean;
|
|
11
|
+
}): DaemonSpawnOptions {
|
|
12
|
+
if (opts.platform !== "win32") return { detached: true };
|
|
13
|
+
return {
|
|
14
|
+
detached: false,
|
|
15
|
+
windowsHide: !opts.hostHasInheritableConsole,
|
|
16
|
+
};
|
|
17
|
+
}
|
package/src/lsp/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ptree } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { type } from "arktype";
|
|
3
|
+
import { TOOL_TIMEOUTS } from "../tools/tool-timeouts";
|
|
3
4
|
|
|
4
5
|
// =============================================================================
|
|
5
6
|
// Tool Schema
|
|
@@ -14,7 +15,10 @@ export const lspSchema = type({
|
|
|
14
15
|
query: "string?",
|
|
15
16
|
new_name: "string?",
|
|
16
17
|
apply: "boolean?",
|
|
17
|
-
timeout:
|
|
18
|
+
"timeout?": type.number
|
|
19
|
+
.atLeast(TOOL_TIMEOUTS.lsp.min)
|
|
20
|
+
.atMost(TOOL_TIMEOUTS.lsp.max)
|
|
21
|
+
.describe("Timeout in seconds (default 20; range 5–300)."),
|
|
18
22
|
payload: "string?",
|
|
19
23
|
});
|
|
20
24
|
|
package/src/main.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
logger,
|
|
18
18
|
normalizePathForComparison,
|
|
19
19
|
postmortem,
|
|
20
|
+
setInteractiveHost,
|
|
20
21
|
setProjectDir,
|
|
21
22
|
VERSION,
|
|
22
23
|
} from "@oh-my-pi/pi-utils";
|
|
@@ -56,6 +57,7 @@ import { registerDaemonProjectPresence } from "./launch/presence";
|
|
|
56
57
|
import type { MCPManager } from "./mcp";
|
|
57
58
|
import { InteractiveMode } from "./modes/interactive-mode";
|
|
58
59
|
import type { PrintModeOptions } from "./modes/print-mode";
|
|
60
|
+
import { claimRpcInput } from "./modes/rpc/rpc-input";
|
|
59
61
|
import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
|
|
60
62
|
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
|
|
61
63
|
import type { SubmittedUserInput } from "./modes/types";
|
|
@@ -97,6 +99,7 @@ type RunRpcMode = (
|
|
|
97
99
|
session: AgentSession,
|
|
98
100
|
setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
|
|
99
101
|
eventBus?: EventBus,
|
|
102
|
+
input?: ReadableStream<Uint8Array>,
|
|
100
103
|
) => Promise<never>;
|
|
101
104
|
|
|
102
105
|
export function writeStartupNotice(parsedArgs: Pick<Args, "mode">, text: string): void {
|
|
@@ -884,8 +887,12 @@ export async function buildSessionOptions(
|
|
|
884
887
|
if (resolved.warning) {
|
|
885
888
|
process.stderr.write(`${chalk.yellow(`Warning: ${resolved.warning}`)}\n`);
|
|
886
889
|
}
|
|
887
|
-
|
|
888
|
-
|
|
890
|
+
const matchedAfterMissingRolePattern = (resolved.configuredPatternIndex ?? 0) > 0;
|
|
891
|
+
if (matchedAfterMissingRolePattern) {
|
|
892
|
+
// Extensions may register an earlier configured role candidate.
|
|
893
|
+
options.modelPattern = parsed.model;
|
|
894
|
+
} else if (resolved.error) {
|
|
895
|
+
if (!parsed.provider && ((resolved.configuredPatterns?.length ?? 0) > 0 || !parsed.model.includes(":"))) {
|
|
889
896
|
// Model not found in built-in registry — defer resolution to after extensions load
|
|
890
897
|
// (extensions may register additional providers/models via registerProvider)
|
|
891
898
|
options.modelPattern = parsed.model;
|
|
@@ -1106,6 +1113,9 @@ export async function runRootCommand(
|
|
|
1106
1113
|
process.stderr.write(`${chalk.red("Error: @file arguments are not supported in RPC mode")}\n`);
|
|
1107
1114
|
process.exit(1);
|
|
1108
1115
|
}
|
|
1116
|
+
const mode = parsedArgs.mode || "text";
|
|
1117
|
+
// RPC owns stdin. Claim its singleton stream before plugin/extension discovery can load an in-process consumer.
|
|
1118
|
+
const rpcInput = mode === "rpc" || mode === "rpc-ui" ? claimRpcInput() : undefined;
|
|
1109
1119
|
|
|
1110
1120
|
// Kick off plugin-root preload in parallel with the remaining startup work.
|
|
1111
1121
|
// Awaited later (before extension/skill discovery in createAgentSession needs it).
|
|
@@ -1152,12 +1162,15 @@ export async function runRootCommand(
|
|
|
1152
1162
|
if (parsedArgs.noTitle || parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui" || parsedArgs.mode === "acp") {
|
|
1153
1163
|
Bun.env.PI_NO_TITLE = "1";
|
|
1154
1164
|
}
|
|
1155
|
-
const mode = parsedArgs.mode || "text";
|
|
1156
1165
|
const isProtocolMode = mode === "rpc" || mode === "rpc-ui" || mode === "acp";
|
|
1157
1166
|
// Protocol modes own stdin; treating it as prompt text would consume JSON-RPC frames before their transports start.
|
|
1158
1167
|
const pipedInput = isProtocolMode ? undefined : await logger.time("readPipedInput", readPipedInput);
|
|
1159
1168
|
const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
|
|
1160
1169
|
const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
|
|
1170
|
+
// Only the interactive host renders a focusable Agent Hub / subagent session
|
|
1171
|
+
// tree; declare it so headless subagent optimizations (e.g. skipping replan
|
|
1172
|
+
// title refresh) can tell a focusable process from a print/RPC/eval one.
|
|
1173
|
+
setInteractiveHost(isInteractive);
|
|
1161
1174
|
|
|
1162
1175
|
// Initialize discovery system with settings for provider persistence
|
|
1163
1176
|
logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
|
|
@@ -1509,7 +1522,7 @@ export async function runRootCommand(
|
|
|
1509
1522
|
// Branch-only protocol runner: keep RPC host code out of normal interactive startup.
|
|
1510
1523
|
const runRpcMode: RunRpcMode = (await import("./modes/rpc/rpc-mode")).runRpcMode;
|
|
1511
1524
|
stopStartupWatchdog();
|
|
1512
|
-
await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus);
|
|
1525
|
+
await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus, rpcInput);
|
|
1513
1526
|
} else if (isInteractive) {
|
|
1514
1527
|
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
|
|
1515
1528
|
const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
|
|
@@ -153,13 +153,21 @@ describe.skipIf(process.platform === "win32")("StdioTransport request write stal
|
|
|
153
153
|
}, 8000);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
// `kill(pid, 0)` succeeds for a zombie too: a grandchild whose parent (the
|
|
157
|
+
// killed leader) is gone sits as <defunct> until whatever reaps orphans
|
|
158
|
+
// (init/subreaper) gets around to it — which can lag on some hosts. A
|
|
159
|
+
// zombie already received and honored the group SIGKILL; it is just not
|
|
160
|
+
// harvested yet, so treating it as "still alive" would make the group-kill
|
|
161
|
+
// assertions below flaky rather than testing what they claim to test.
|
|
156
162
|
function processExists(pid: number): boolean {
|
|
157
163
|
try {
|
|
158
164
|
process.kill(pid, 0);
|
|
159
|
-
return true;
|
|
160
165
|
} catch {
|
|
161
166
|
return false;
|
|
162
167
|
}
|
|
168
|
+
const result = Bun.spawnSync(["ps", "-o", "stat=", "-p", String(pid)]);
|
|
169
|
+
const state = result.stdout.toString().trim();
|
|
170
|
+
return result.exitCode === 0 && state.length > 0 && !state.startsWith("Z");
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
// Regression for #5578: `close()` used a bare `this.#process.kill()` (direct
|
package/src/mnemopi/backend.ts
CHANGED
|
@@ -145,7 +145,7 @@ export const mnemopiBackend: MemoryBackend = {
|
|
|
145
145
|
state = new MnemopiSessionState({ sessionId: session.sessionId, config, session });
|
|
146
146
|
setMnemopiSessionState(session, state);
|
|
147
147
|
}
|
|
148
|
-
await state?.consolidate();
|
|
148
|
+
await state?.consolidate({ full: true });
|
|
149
149
|
} catch (error) {
|
|
150
150
|
logger.warn("Mnemopi: enqueue failed.", { error: String(error) });
|
|
151
151
|
}
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* in either process.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type
|
|
13
|
-
import { loadFastembed } from "@oh-my-pi/pi-mnemopi/core/fastembed-runtime";
|
|
12
|
+
import { defaultLocalModelInitializer, type StandardEmbeddingModel } from "@oh-my-pi/pi-mnemopi/core";
|
|
14
13
|
import type { MnemopiEmbedModelId, MnemopiEmbedTransport, MnemopiEmbedWorkerInbound } from "./embed-protocol";
|
|
15
14
|
|
|
16
15
|
interface LoadedModel {
|
|
@@ -25,12 +24,14 @@ let loaded: Promise<LoadedModel> | null = null;
|
|
|
25
24
|
let loadedKey = "";
|
|
26
25
|
|
|
27
26
|
async function loadModel(model: MnemopiEmbedModelId, cacheDir: string | undefined): Promise<LoadedModel> {
|
|
28
|
-
|
|
27
|
+
// Route through mnemopi's shared initializer so the worker inherits BOTH
|
|
28
|
+
// cache heals (sidecar re-fetch AND corrupt-model quarantine/retry) —
|
|
29
|
+
// fastembed/onnxruntime still load only in this child address space, the
|
|
30
|
+
// initializer calls loadFastembed() itself.
|
|
29
31
|
// Cast: `model` arrives as a string from the parent (resolved by
|
|
30
|
-
// mnemopi's `fastembedModelName`)
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
const instance = await FlagEmbedding.init({
|
|
32
|
+
// mnemopi's `fastembedModelName`); the parent only ever passes pre-vetted
|
|
33
|
+
// fast-* identifiers.
|
|
34
|
+
const instance = await defaultLocalModelInitializer({
|
|
34
35
|
model: model as StandardEmbeddingModel,
|
|
35
36
|
cacheDir,
|
|
36
37
|
showDownloadProgress: false,
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -454,18 +454,23 @@ export class MnemopiSessionState {
|
|
|
454
454
|
this.lastRetainedTurn = userTurns;
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
async forceRetainCurrentSession(): Promise<void> {
|
|
457
|
+
async forceRetainCurrentSession(options: { extract?: boolean } = {}): Promise<void> {
|
|
458
458
|
if (this.aliasOf) return;
|
|
459
459
|
const flat = extractMessages(this.session.sessionManager);
|
|
460
|
-
await this.retainMessages(flat, this.sessionId);
|
|
460
|
+
await this.retainMessages(flat, this.sessionId, options);
|
|
461
461
|
this.lastRetainedTurn = flat.filter(message => message.role === "user").length;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
-
async retainMessages(
|
|
464
|
+
async retainMessages(
|
|
465
|
+
messages: Array<{ role: string; content: string }>,
|
|
466
|
+
sourceId: string,
|
|
467
|
+
options: { extract?: boolean } = {},
|
|
468
|
+
): Promise<void> {
|
|
465
469
|
const { transcript, messageCount } = prepareRetentionTranscript(messages, true);
|
|
466
470
|
if (!transcript) return;
|
|
467
471
|
const { transcript: extractText } = prepareUserRetentionTranscript(messages);
|
|
468
472
|
const { transcript: embedText } = prepareEmbeddableRetentionTranscript(messages);
|
|
473
|
+
const shouldExtract = options.extract !== false && extractText !== null;
|
|
469
474
|
this.rememberInScope(transcript, {
|
|
470
475
|
source: "coding-agent-transcript",
|
|
471
476
|
importance: 0.65,
|
|
@@ -476,9 +481,9 @@ export class MnemopiSessionState {
|
|
|
476
481
|
cwd: this.session.sessionManager.getCwd(),
|
|
477
482
|
},
|
|
478
483
|
scope: "bank",
|
|
479
|
-
extract:
|
|
480
|
-
extractEntities:
|
|
481
|
-
extractText,
|
|
484
|
+
extract: shouldExtract,
|
|
485
|
+
extractEntities: shouldExtract,
|
|
486
|
+
extractText: shouldExtract ? extractText : null,
|
|
482
487
|
embedText,
|
|
483
488
|
veracity: "unknown",
|
|
484
489
|
memoryType: "episode",
|
|
@@ -530,22 +535,42 @@ export class MnemopiSessionState {
|
|
|
530
535
|
* otherwise enqueue would report success while leaving the subagent's
|
|
531
536
|
* retained memories unconsolidated until the parent eventually shuts down
|
|
532
537
|
* (PR #2327 review).
|
|
538
|
+
*
|
|
539
|
+
* @param options.full - When true, run `sleepAllSessions` on every owned bank
|
|
540
|
+
* (the full cross-session consolidation used by `/memory enqueue`). When
|
|
541
|
+
* false (the default), run only `sleep` on the current session for a
|
|
542
|
+
* lighter, bounded shutdown pass.
|
|
543
|
+
* @param options.sleep - When false, skips the bank sleep step entirely.
|
|
544
|
+
* Used on the interactive shutdown path so `dispose` does not block on
|
|
545
|
+
* synchronous consolidation of old working rows from previous sessions.
|
|
546
|
+
* @param options.extract - When false, the retained transcript is stored but
|
|
547
|
+
* no LLM fact extraction is scheduled. Used on the interactive shutdown path
|
|
548
|
+
* so `dispose` does not block on a fresh LLM round-trip.
|
|
533
549
|
*/
|
|
534
|
-
async consolidate(): Promise<void> {
|
|
535
|
-
await this.forceRetainCurrentSession();
|
|
550
|
+
async consolidate(options: { full?: boolean; extract?: boolean; sleep?: boolean } = {}): Promise<void> {
|
|
551
|
+
await this.forceRetainCurrentSession({ extract: options.extract });
|
|
536
552
|
for (const memory of this.scoped.owned) {
|
|
537
553
|
await memory.flushExtractions();
|
|
538
|
-
|
|
554
|
+
if (options.sleep === false) continue;
|
|
555
|
+
if (options.full) {
|
|
556
|
+
memory.sleepAllSessions(false);
|
|
557
|
+
} else {
|
|
558
|
+
memory.sleep(false);
|
|
559
|
+
}
|
|
539
560
|
}
|
|
540
561
|
}
|
|
541
562
|
|
|
542
563
|
/**
|
|
543
|
-
* Release the per-session resources. Defaults to running
|
|
544
|
-
* before closing handles
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
564
|
+
* Release the per-session resources. Defaults to running a lighter
|
|
565
|
+
* {@link consolidate} pass before closing handles: it retains the current
|
|
566
|
+
* transcript and flushes in-flight extractions, but skips the synchronous
|
|
567
|
+
* bank sleep so normal session shutdown returns promptly. Full promotion of
|
|
568
|
+
* working memory into long-term storage is still performed by the explicit
|
|
569
|
+
* `/memory enqueue` and backend enqueue paths. Callers that are about to
|
|
570
|
+
* delete the DB files — e.g. `mnemopiBackend.clear` — pass
|
|
571
|
+
* `{ consolidate: false }` to skip the retain/flush pass, since spending
|
|
572
|
+
* tokens on memories that will be wiped on the next line is wasted work
|
|
573
|
+
* (PR #2327 review).
|
|
549
574
|
*
|
|
550
575
|
* `timeoutMs` caps how long the consolidate await blocks the caller
|
|
551
576
|
* (the user-visible `/quit` / `/exit` shutdown path passes this so
|
|
@@ -569,9 +594,11 @@ export class MnemopiSessionState {
|
|
|
569
594
|
closeOwned();
|
|
570
595
|
return;
|
|
571
596
|
}
|
|
572
|
-
const consolidatePromise = this.consolidate().catch(
|
|
573
|
-
|
|
574
|
-
|
|
597
|
+
const consolidatePromise = this.consolidate({ full: false, extract: false, sleep: false }).catch(
|
|
598
|
+
(error: unknown) => {
|
|
599
|
+
logger.warn("Mnemopi: consolidation on dispose failed.", { error: String(error) });
|
|
600
|
+
},
|
|
601
|
+
);
|
|
575
602
|
const { timeoutMs } = options;
|
|
576
603
|
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
577
604
|
const TIMED_OUT = Symbol("mnemopi.dispose.timedOut");
|