pi-maestro-teammate 2.6.0 → 2.6.1
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/package.json +5 -1
- package/src/extension/teammate-core.ts +2 -76
- package/src/public/v1/foreground-detach.ts +90 -0
- package/src/public/v1/index.ts +1 -0
- package/src/runs/execution-infra.ts +220 -48
- package/src/runs/pi-subprocess-attempt.ts +31 -11
- package/types/extension/teammate-core.d.ts +3 -4
- package/types/public/v1/foreground-detach.d.ts +4 -0
- package/types/public/v1/index.d.ts +1 -0
- package/types/runs/execution-infra.d.ts +32 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-maestro-teammate",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Pi extension — teammate agent dispatch with DAG task graphs, RPC messaging, and compact TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
"types": "./types/public/v1/extension.d.ts",
|
|
70
70
|
"default": "./src/public/v1/extension.ts"
|
|
71
71
|
},
|
|
72
|
+
"./v1/foreground-detach": {
|
|
73
|
+
"types": "./types/public/v1/foreground-detach.d.ts",
|
|
74
|
+
"default": "./src/public/v1/foreground-detach.ts"
|
|
75
|
+
},
|
|
72
76
|
"./v1/model-routing": {
|
|
73
77
|
"types": "./types/public/v1/model-routing.d.ts",
|
|
74
78
|
"default": "./src/public/v1/model-routing.ts"
|
|
@@ -10,11 +10,11 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import { logDiagnosticError, logDiagnosticWarn } from "../shared/diagnostic-log.ts";
|
|
11
11
|
|
|
12
12
|
import { altKey } from "pi-maestro-settings-core/v1";
|
|
13
|
+
import { registerForegroundDetach, setPersistentUi } from "../public/v1/foreground-detach.ts";
|
|
13
14
|
import type {
|
|
14
15
|
ExtensionAPI,
|
|
15
16
|
ExtensionCommandContext,
|
|
16
17
|
ExtensionContext,
|
|
17
|
-
ExtensionUIContext,
|
|
18
18
|
ToolDefinition,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
@@ -896,81 +896,7 @@ export function backgroundWaitGuidance(correlationId: string): string {
|
|
|
896
896
|
*/
|
|
897
897
|
export const FOREGROUND_DETACH_HINT = `${altKey("B")} detaches a foreground call to background.`;
|
|
898
898
|
|
|
899
|
-
|
|
900
|
-
* One session-scoped Alt+B listener dispatches to the oldest active foreground
|
|
901
|
-
* owner. This makes nested calls detach layer by layer from the outermost call
|
|
902
|
-
* instead of relying on TUI listener registration order.
|
|
903
|
-
*/
|
|
904
|
-
type ForegroundDetachOwner = {
|
|
905
|
-
active: boolean;
|
|
906
|
-
detach(): void;
|
|
907
|
-
};
|
|
908
|
-
|
|
909
|
-
let persistentUi: ExtensionUIContext | undefined;
|
|
910
|
-
let persistentUiUnsubscribe: (() => void) | undefined;
|
|
911
|
-
const foregroundDetachOwners: ForegroundDetachOwner[] = [];
|
|
912
|
-
|
|
913
|
-
function uninstallForegroundDetachListener(): void {
|
|
914
|
-
const unsubscribe = persistentUiUnsubscribe;
|
|
915
|
-
persistentUiUnsubscribe = undefined;
|
|
916
|
-
unsubscribe?.();
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function installForegroundDetachListener(): void {
|
|
920
|
-
if (!persistentUi || persistentUiUnsubscribe || foregroundDetachOwners.length === 0) return;
|
|
921
|
-
persistentUiUnsubscribe = persistentUi.onTerminalInput((data: string) => {
|
|
922
|
-
if (data !== "\x1bb") return undefined;
|
|
923
|
-
const owner = foregroundDetachOwners.shift();
|
|
924
|
-
if (!owner) return undefined;
|
|
925
|
-
owner.active = false;
|
|
926
|
-
if (foregroundDetachOwners.length === 0) uninstallForegroundDetachListener();
|
|
927
|
-
owner.detach();
|
|
928
|
-
return { consume: true };
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
export function setPersistentUi(
|
|
933
|
-
ui: ExtensionUIContext | undefined,
|
|
934
|
-
resetOwners = false,
|
|
935
|
-
): void {
|
|
936
|
-
if (persistentUi !== ui || resetOwners) {
|
|
937
|
-
uninstallForegroundDetachListener();
|
|
938
|
-
persistentUi = ui;
|
|
939
|
-
}
|
|
940
|
-
if (!ui || resetOwners) {
|
|
941
|
-
for (const owner of foregroundDetachOwners) owner.active = false;
|
|
942
|
-
foregroundDetachOwners.length = 0;
|
|
943
|
-
if (!ui) return;
|
|
944
|
-
}
|
|
945
|
-
installForegroundDetachListener();
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/** Registers one foreground owner; unregister is idempotent on every race path. */
|
|
949
|
-
export function registerForegroundDetach(
|
|
950
|
-
detach: () => void,
|
|
951
|
-
ui?: ExtensionUIContext,
|
|
952
|
-
): () => void {
|
|
953
|
-
if (ui) setPersistentUi(ui);
|
|
954
|
-
const owner: ForegroundDetachOwner = { active: true, detach };
|
|
955
|
-
foregroundDetachOwners.push(owner);
|
|
956
|
-
try {
|
|
957
|
-
installForegroundDetachListener();
|
|
958
|
-
} catch (error) {
|
|
959
|
-
owner.active = false;
|
|
960
|
-
const index = foregroundDetachOwners.indexOf(owner);
|
|
961
|
-
if (index >= 0) foregroundDetachOwners.splice(index, 1);
|
|
962
|
-
if (foregroundDetachOwners.length === 0) uninstallForegroundDetachListener();
|
|
963
|
-
throw error;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
return () => {
|
|
967
|
-
if (!owner.active) return;
|
|
968
|
-
owner.active = false;
|
|
969
|
-
const index = foregroundDetachOwners.indexOf(owner);
|
|
970
|
-
if (index >= 0) foregroundDetachOwners.splice(index, 1);
|
|
971
|
-
if (foregroundDetachOwners.length === 0) uninstallForegroundDetachListener();
|
|
972
|
-
};
|
|
973
|
-
}
|
|
899
|
+
export { registerForegroundDetach, setPersistentUi };
|
|
974
900
|
|
|
975
901
|
export function foregroundWaitWindowMs(
|
|
976
902
|
tasks: ReadonlyArray<{ timeoutMs?: number }>,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/** One session-scoped Alt+B listener shared by every foreground tool owner. */
|
|
4
|
+
type ForegroundDetachOwner = {
|
|
5
|
+
active: boolean;
|
|
6
|
+
detach(): void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type ForegroundDetachRegistry = {
|
|
10
|
+
ui?: ExtensionUIContext;
|
|
11
|
+
unsubscribe?: () => void;
|
|
12
|
+
owners: ForegroundDetachOwner[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const REGISTRY_KEY = Symbol.for("pi-maestro.foreground-detach.v1");
|
|
16
|
+
const globals = globalThis as typeof globalThis & Record<symbol, unknown>;
|
|
17
|
+
|
|
18
|
+
function registry(): ForegroundDetachRegistry {
|
|
19
|
+
const existing = globals[REGISTRY_KEY];
|
|
20
|
+
if (existing && typeof existing === "object" && Array.isArray((existing as ForegroundDetachRegistry).owners)) {
|
|
21
|
+
return existing as ForegroundDetachRegistry;
|
|
22
|
+
}
|
|
23
|
+
const created: ForegroundDetachRegistry = { owners: [] };
|
|
24
|
+
globals[REGISTRY_KEY] = created;
|
|
25
|
+
return created;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function uninstallForegroundDetachListener(state: ForegroundDetachRegistry): void {
|
|
29
|
+
const unsubscribe = state.unsubscribe;
|
|
30
|
+
state.unsubscribe = undefined;
|
|
31
|
+
unsubscribe?.();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function installForegroundDetachListener(state: ForegroundDetachRegistry): void {
|
|
35
|
+
if (!state.ui || state.unsubscribe || state.owners.length === 0) return;
|
|
36
|
+
state.unsubscribe = state.ui.onTerminalInput((data: string) => {
|
|
37
|
+
if (data !== "\x1bb") return undefined;
|
|
38
|
+
const owner = state.owners.shift();
|
|
39
|
+
if (!owner) return undefined;
|
|
40
|
+
owner.active = false;
|
|
41
|
+
if (state.owners.length === 0) uninstallForegroundDetachListener(state);
|
|
42
|
+
owner.detach();
|
|
43
|
+
return { consume: true };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setPersistentUi(
|
|
48
|
+
ui: ExtensionUIContext | undefined,
|
|
49
|
+
resetOwners = false,
|
|
50
|
+
): void {
|
|
51
|
+
const state = registry();
|
|
52
|
+
if (state.ui !== ui || resetOwners) {
|
|
53
|
+
uninstallForegroundDetachListener(state);
|
|
54
|
+
state.ui = ui;
|
|
55
|
+
}
|
|
56
|
+
if (!ui || resetOwners) {
|
|
57
|
+
for (const owner of state.owners) owner.active = false;
|
|
58
|
+
state.owners.length = 0;
|
|
59
|
+
if (!ui) return;
|
|
60
|
+
}
|
|
61
|
+
installForegroundDetachListener(state);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Registers one foreground owner; unregister is idempotent on every race path. */
|
|
65
|
+
export function registerForegroundDetach(
|
|
66
|
+
detach: () => void,
|
|
67
|
+
ui?: ExtensionUIContext,
|
|
68
|
+
): () => void {
|
|
69
|
+
if (ui) setPersistentUi(ui);
|
|
70
|
+
const state = registry();
|
|
71
|
+
const owner: ForegroundDetachOwner = { active: true, detach };
|
|
72
|
+
state.owners.push(owner);
|
|
73
|
+
try {
|
|
74
|
+
installForegroundDetachListener(state);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
owner.active = false;
|
|
77
|
+
const index = state.owners.indexOf(owner);
|
|
78
|
+
if (index >= 0) state.owners.splice(index, 1);
|
|
79
|
+
if (state.owners.length === 0) uninstallForegroundDetachListener(state);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
if (!owner.active) return;
|
|
85
|
+
owner.active = false;
|
|
86
|
+
const index = state.owners.indexOf(owner);
|
|
87
|
+
if (index >= 0) state.owners.splice(index, 1);
|
|
88
|
+
if (state.owners.length === 0) uninstallForegroundDetachListener(state);
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/public/v1/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./completion-durability.ts";
|
|
|
16
16
|
export * from "./events.ts";
|
|
17
17
|
export * from "./execution.ts";
|
|
18
18
|
export * from "./extension.ts";
|
|
19
|
+
export * from "./foreground-detach.ts";
|
|
19
20
|
export * from "./mailbox.ts";
|
|
20
21
|
export * from "./model-routing.ts";
|
|
21
22
|
export * from "./monitor-window-state.ts";
|
|
@@ -1502,75 +1502,247 @@ export function normalizeTeammateParams(
|
|
|
1502
1502
|
}
|
|
1503
1503
|
|
|
1504
1504
|
// ---------------------------------------------------------------------------
|
|
1505
|
-
// AC3:
|
|
1505
|
+
// AC3: shell-free, provenance-bearing Pi launcher resolution
|
|
1506
1506
|
// ---------------------------------------------------------------------------
|
|
1507
1507
|
|
|
1508
|
-
export
|
|
1508
|
+
export type PiLaunchSource =
|
|
1509
|
+
| "override"
|
|
1510
|
+
| "path-native"
|
|
1511
|
+
| "windows-shim"
|
|
1512
|
+
| "host-entry"
|
|
1513
|
+
| "path-fallback";
|
|
1509
1514
|
|
|
1510
|
-
export
|
|
1511
|
-
|
|
1515
|
+
export interface PiLaunchSpec {
|
|
1516
|
+
command: string;
|
|
1517
|
+
argsPrefix: string[];
|
|
1518
|
+
source: PiLaunchSource;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
export interface PiSpawnCommandOptions {
|
|
1522
|
+
envBinary?: string | null;
|
|
1523
|
+
entryPoint?: string | null;
|
|
1524
|
+
platform?: NodeJS.Platform;
|
|
1525
|
+
pathValue?: string | null;
|
|
1526
|
+
argv?: readonly string[];
|
|
1527
|
+
execPath?: string;
|
|
1528
|
+
appData?: string | null;
|
|
1529
|
+
/** @internal deterministic filesystem seam for focused launcher tests. */
|
|
1530
|
+
isFile?: (candidate: string) => boolean;
|
|
1531
|
+
/** @internal deterministic package-manifest seam for focused launcher tests. */
|
|
1532
|
+
readTextFile?: (candidate: string) => string;
|
|
1533
|
+
}
|
|
1512
1534
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
return
|
|
1535
|
+
function regularFileExists(candidate: string): boolean {
|
|
1536
|
+
try {
|
|
1537
|
+
return fs.statSync(candidate).isFile();
|
|
1538
|
+
} catch {
|
|
1539
|
+
return false;
|
|
1518
1540
|
}
|
|
1541
|
+
}
|
|
1519
1542
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
if (match) {
|
|
1532
|
-
const entryPoint = path.join(npmDir, match[1]);
|
|
1533
|
-
if (fs.existsSync(entryPoint)) {
|
|
1534
|
-
resolvedPiEntryPoint = entryPoint;
|
|
1535
|
-
return resolvedPiEntryPoint;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
} catch { /* fallback */ }
|
|
1543
|
+
function pathPiCandidate(
|
|
1544
|
+
platform: NodeJS.Platform,
|
|
1545
|
+
pathValue: string | null | undefined,
|
|
1546
|
+
isFile: (candidate: string) => boolean,
|
|
1547
|
+
): Pick<PiLaunchSpec, "command" | "source"> | undefined {
|
|
1548
|
+
if (!pathValue) return undefined;
|
|
1549
|
+
const directories = pathValue.split(platform === "win32" ? ";" : ":").filter(Boolean);
|
|
1550
|
+
if (platform !== "win32") {
|
|
1551
|
+
for (const directory of directories) {
|
|
1552
|
+
const candidate = path.join(directory, "pi");
|
|
1553
|
+
if (isFile(candidate)) return { command: candidate, source: "path-native" };
|
|
1539
1554
|
}
|
|
1555
|
+
return undefined;
|
|
1540
1556
|
}
|
|
1541
1557
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1558
|
+
// Prefer a native executable anywhere on PATH before considering cmd/bat
|
|
1559
|
+
// wrappers. Ignore npm's extensionless POSIX shim: cross-spawn follows its
|
|
1560
|
+
// shebang through sh.exe, so the Pi Node process does not own the IPC channel.
|
|
1561
|
+
for (const extension of [".exe", ".com"] as const) {
|
|
1562
|
+
for (const directory of directories) {
|
|
1563
|
+
const candidate = path.join(directory, `pi${extension}`);
|
|
1564
|
+
if (isFile(candidate)) return { command: candidate, source: "path-native" };
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
for (const extension of [".cmd", ".bat"] as const) {
|
|
1568
|
+
for (const directory of directories) {
|
|
1569
|
+
const candidate = path.join(directory, `pi${extension}`);
|
|
1570
|
+
if (isFile(candidate)) return { command: candidate, source: "windows-shim" };
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return undefined;
|
|
1544
1574
|
}
|
|
1545
1575
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1576
|
+
const WINDOWS_PI_SHIM_ENTRY_PATTERN = /"?%dp0%[\\/]([^"*%\r\n]+?\.(?:js|mjs|cjs))"?/i;
|
|
1577
|
+
|
|
1578
|
+
function resolvedWindowsPiShim(
|
|
1579
|
+
shim: string,
|
|
1580
|
+
isFile: (candidate: string) => boolean,
|
|
1581
|
+
readTextFile: (candidate: string) => string,
|
|
1582
|
+
execPath: string,
|
|
1583
|
+
): PiLaunchSpec | undefined {
|
|
1584
|
+
try {
|
|
1585
|
+
const match = WINDOWS_PI_SHIM_ENTRY_PATTERN.exec(readTextFile(shim));
|
|
1586
|
+
if (!match) return undefined;
|
|
1587
|
+
const entry = path.resolve(path.dirname(shim), match[1]!.replace(/[\\/]/g, path.sep));
|
|
1588
|
+
const verified = verifiedHostPiEntry(entry, isFile, readTextFile);
|
|
1589
|
+
return verified
|
|
1590
|
+
? { command: execPath, argsPrefix: [verified], source: "windows-shim" }
|
|
1591
|
+
: undefined;
|
|
1592
|
+
} catch {
|
|
1593
|
+
return undefined;
|
|
1594
|
+
}
|
|
1550
1595
|
}
|
|
1551
1596
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1597
|
+
function verifiedHostPiEntry(
|
|
1598
|
+
entryPoint: string | null | undefined,
|
|
1599
|
+
isFile: (candidate: string) => boolean,
|
|
1600
|
+
readTextFile: (candidate: string) => string,
|
|
1601
|
+
): string | undefined {
|
|
1602
|
+
if (!entryPoint || !/\.(?:js|mjs|cjs)$/i.test(entryPoint) || !isFile(entryPoint)) return undefined;
|
|
1603
|
+
const absoluteEntry = path.resolve(entryPoint);
|
|
1604
|
+
let directory = path.dirname(absoluteEntry);
|
|
1605
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
1606
|
+
const manifestPath = path.join(directory, "package.json");
|
|
1607
|
+
if (isFile(manifestPath)) {
|
|
1608
|
+
try {
|
|
1609
|
+
const manifest = JSON.parse(readTextFile(manifestPath)) as {
|
|
1610
|
+
name?: unknown;
|
|
1611
|
+
bin?: unknown;
|
|
1612
|
+
};
|
|
1613
|
+
const acceptedPackage = manifest.name === "@earendil-works/pi-coding-agent"
|
|
1614
|
+
|| manifest.name === "@mariozechner/pi-coding-agent";
|
|
1615
|
+
const piBin = typeof manifest.bin === "object" && manifest.bin !== null
|
|
1616
|
+
? (manifest.bin as Record<string, unknown>).pi
|
|
1617
|
+
: undefined;
|
|
1618
|
+
if (acceptedPackage && typeof piBin === "string"
|
|
1619
|
+
&& path.resolve(directory, piBin) === absoluteEntry) return absoluteEntry;
|
|
1620
|
+
} catch {
|
|
1621
|
+
return undefined;
|
|
1622
|
+
}
|
|
1623
|
+
return undefined;
|
|
1624
|
+
}
|
|
1625
|
+
const parent = path.dirname(directory);
|
|
1626
|
+
if (parent === directory) break;
|
|
1627
|
+
directory = parent;
|
|
1628
|
+
}
|
|
1629
|
+
return undefined;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
export let resolvedPiEntryPoint: string | null | undefined;
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* Resolve process.argv[1] only when its owning package manifest proves it is
|
|
1636
|
+
* Pi's declared CLI. A generic .js/.mjs suffix is never evidence of identity.
|
|
1637
|
+
*/
|
|
1638
|
+
export function resolvePiEntryPoint(): string | null {
|
|
1639
|
+
if (resolvedPiEntryPoint !== undefined) return resolvedPiEntryPoint;
|
|
1640
|
+
resolvedPiEntryPoint = verifiedHostPiEntry(
|
|
1641
|
+
process.argv[1],
|
|
1642
|
+
regularFileExists,
|
|
1643
|
+
(candidate) => fs.readFileSync(candidate, "utf-8"),
|
|
1644
|
+
) ?? null;
|
|
1645
|
+
return resolvedPiEntryPoint;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
export function resolvePiLaunchSpec(options: PiSpawnCommandOptions = {}): PiLaunchSpec {
|
|
1556
1649
|
const envBinary = options.envBinary === undefined
|
|
1557
1650
|
? process.env.PI_TEAMMATE_PI_BINARY
|
|
1558
1651
|
: options.envBinary;
|
|
1559
|
-
if (envBinary) {
|
|
1560
|
-
return { command: envBinary,
|
|
1652
|
+
if (envBinary?.trim()) {
|
|
1653
|
+
return { command: envBinary, argsPrefix: [], source: "override" };
|
|
1561
1654
|
}
|
|
1562
1655
|
|
|
1563
|
-
const
|
|
1656
|
+
const platform = options.platform ?? process.platform;
|
|
1657
|
+
const isFile = options.isFile ?? regularFileExists;
|
|
1658
|
+
const readTextFile = options.readTextFile ?? ((candidate: string) => fs.readFileSync(candidate, "utf-8"));
|
|
1659
|
+
const execPath = options.execPath ?? process.execPath;
|
|
1660
|
+
const pathCandidate = pathPiCandidate(
|
|
1661
|
+
platform,
|
|
1662
|
+
options.pathValue === undefined ? process.env.PATH : options.pathValue,
|
|
1663
|
+
isFile,
|
|
1664
|
+
);
|
|
1665
|
+
if (pathCandidate?.source === "path-native") return { ...pathCandidate, argsPrefix: [] };
|
|
1666
|
+
if (pathCandidate?.source === "windows-shim") {
|
|
1667
|
+
const resolved = resolvedWindowsPiShim(pathCandidate.command, isFile, readTextFile, execPath);
|
|
1668
|
+
if (resolved) return resolved;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// APPDATA/npm is not guaranteed to be present in PATH for service/gateway
|
|
1672
|
+
// processes, so retain an explicit, verified Windows shim fallback.
|
|
1673
|
+
if (platform === "win32") {
|
|
1674
|
+
const appData = options.appData === undefined ? process.env.APPDATA : options.appData;
|
|
1675
|
+
if (appData) {
|
|
1676
|
+
const shim = path.join(appData, "npm", "pi.cmd");
|
|
1677
|
+
if (isFile(shim)) {
|
|
1678
|
+
const resolved = resolvedWindowsPiShim(shim, isFile, readTextFile, execPath);
|
|
1679
|
+
if (resolved) return resolved;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
const injectedEntry = options.entryPoint !== undefined
|
|
1684
|
+
? verifiedHostPiEntry(options.entryPoint, isFile, readTextFile)
|
|
1685
|
+
: options.argv !== undefined
|
|
1686
|
+
? verifiedHostPiEntry(options.argv[1], isFile, readTextFile)
|
|
1687
|
+
: undefined;
|
|
1688
|
+
const hostEntry = options.entryPoint === undefined && options.argv === undefined
|
|
1564
1689
|
? resolvePiEntryPoint()
|
|
1565
|
-
:
|
|
1566
|
-
if (
|
|
1567
|
-
return {
|
|
1690
|
+
: injectedEntry;
|
|
1691
|
+
if (hostEntry) {
|
|
1692
|
+
return {
|
|
1693
|
+
command: options.execPath ?? process.execPath,
|
|
1694
|
+
argsPrefix: [hostEntry],
|
|
1695
|
+
source: "host-entry",
|
|
1696
|
+
};
|
|
1568
1697
|
}
|
|
1569
1698
|
|
|
1570
|
-
//
|
|
1571
|
-
//
|
|
1572
|
-
|
|
1573
|
-
|
|
1699
|
+
// Preserve the historical ENOENT failure mode when no resolver candidate is
|
|
1700
|
+
// installed, but make that unverified fallback explicit in diagnostics.
|
|
1701
|
+
return { command: "pi", argsPrefix: [], source: "path-fallback" };
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
export function getPiSpawnCommand(
|
|
1705
|
+
args: string[],
|
|
1706
|
+
options: PiSpawnCommandOptions = {},
|
|
1707
|
+
): PiLaunchSpec & { args: string[]; shell: false } {
|
|
1708
|
+
const launch = resolvePiLaunchSpec(options);
|
|
1709
|
+
return {
|
|
1710
|
+
...launch,
|
|
1711
|
+
args: [...launch.argsPrefix, ...args],
|
|
1712
|
+
shell: false,
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
export interface PiLaunchDiagnostic {
|
|
1717
|
+
type: "teammate_pi_launch_diagnostic";
|
|
1718
|
+
source: PiLaunchSource;
|
|
1719
|
+
phase: "spawn" | "child-error" | "close";
|
|
1720
|
+
exitCode: number | null;
|
|
1721
|
+
signal: NodeJS.Signals | null;
|
|
1722
|
+
stderrTail: string;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
export function piLaunchDiagnostic(
|
|
1726
|
+
launch: Pick<PiLaunchSpec, "source">,
|
|
1727
|
+
phase: PiLaunchDiagnostic["phase"],
|
|
1728
|
+
exitCode: number | null,
|
|
1729
|
+
signal: NodeJS.Signals | null,
|
|
1730
|
+
stderr: string,
|
|
1731
|
+
): PiLaunchDiagnostic {
|
|
1732
|
+
return {
|
|
1733
|
+
type: "teammate_pi_launch_diagnostic",
|
|
1734
|
+
source: launch.source,
|
|
1735
|
+
phase,
|
|
1736
|
+
exitCode,
|
|
1737
|
+
signal,
|
|
1738
|
+
stderrTail: truncateUtf8Tail(stderr.trim(), EXECUTION_BUFFER_LIMITS.stderrBytes),
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
export function formatPiLaunchDiagnostic(diagnostic: PiLaunchDiagnostic): string {
|
|
1743
|
+
return `source=${diagnostic.source}, phase=${diagnostic.phase}, exit=${diagnostic.exitCode ?? "null"}, `
|
|
1744
|
+
+ `signal=${diagnostic.signal ?? "none"}`
|
|
1745
|
+
+ (diagnostic.stderrTail ? `\nstderr tail:\n${diagnostic.stderrTail}` : "");
|
|
1574
1746
|
}
|
|
1575
1747
|
|
|
1576
1748
|
export interface InteractiveTerminalLaunchOptions {
|
|
@@ -94,10 +94,12 @@ import {
|
|
|
94
94
|
extractPiEventError,
|
|
95
95
|
extractStructuredOutputCandidate,
|
|
96
96
|
extractTextContent,
|
|
97
|
+
formatPiLaunchDiagnostic,
|
|
97
98
|
getPiSpawnCommand,
|
|
98
99
|
getTeammateDepth,
|
|
99
100
|
getTeammateSessionRoot,
|
|
100
101
|
isPiResultReadyTurn,
|
|
102
|
+
piLaunchDiagnostic,
|
|
101
103
|
readRegularTextFile,
|
|
102
104
|
releasePublishedTurnHistory,
|
|
103
105
|
resetUsage,
|
|
@@ -876,8 +878,8 @@ export async function runSingleAttempt(
|
|
|
876
878
|
);
|
|
877
879
|
|
|
878
880
|
let useIpc = false;
|
|
881
|
+
const spawnSpec = getPiSpawnCommand(piArgs);
|
|
879
882
|
try {
|
|
880
|
-
const spawnSpec = getPiSpawnCommand(piArgs);
|
|
881
883
|
useIpc = !spawnSpec.shell;
|
|
882
884
|
const spawnOpts: Parameters<typeof crossSpawn>[2] = {
|
|
883
885
|
cwd,
|
|
@@ -892,6 +894,8 @@ export async function runSingleAttempt(
|
|
|
892
894
|
if (schemaFile) cleanupFile(schemaFile);
|
|
893
895
|
if (outputFile) cleanupFile(outputFile);
|
|
894
896
|
|
|
897
|
+
const spawnDiagnostic = piLaunchDiagnostic(spawnSpec, "spawn", null, null, "");
|
|
898
|
+
options.onChildEvent?.({ ...spawnDiagnostic, correlationId });
|
|
895
899
|
const result: SingleResult = {
|
|
896
900
|
agent: params.agent,
|
|
897
901
|
name: params.name,
|
|
@@ -901,7 +905,8 @@ export async function runSingleAttempt(
|
|
|
901
905
|
role: "system",
|
|
902
906
|
content:
|
|
903
907
|
`Failed to spawn pi subprocess (agent=${params.agent}, model=${state.resolvedModel || "unknown"}, `
|
|
904
|
-
+ `correlationId=${correlationId},
|
|
908
|
+
+ `correlationId=${correlationId}, ${formatPiLaunchDiagnostic(spawnDiagnostic)}): `
|
|
909
|
+
+ `${error instanceof Error ? error.message : String(error)}`,
|
|
905
910
|
}],
|
|
906
911
|
usage: emptyUsage(),
|
|
907
912
|
model: state.resolvedModel,
|
|
@@ -2869,24 +2874,31 @@ export async function runSingleAttempt(
|
|
|
2869
2874
|
if (finalContent && !messages.some((message) => message.content === finalContent)) {
|
|
2870
2875
|
appendDistinctAssistantMessage(messages, finalContent);
|
|
2871
2876
|
}
|
|
2872
|
-
let stderrAlreadyReported = false;
|
|
2873
2877
|
if (messages.length === 0) {
|
|
2874
2878
|
const content = state.lastContent.trim() || stderrTail || "(no output)";
|
|
2875
|
-
stderrAlreadyReported = stderrTail.length > 0 && content === stderrTail;
|
|
2876
2879
|
appendBoundedTranscriptMessage(messages, { role: "assistant", content });
|
|
2877
2880
|
}
|
|
2878
2881
|
|
|
2879
|
-
//
|
|
2880
|
-
//
|
|
2882
|
+
// Gateway results and Monitor observers consume the same bounded,
|
|
2883
|
+
// provenance-bearing diagnostic projection.
|
|
2881
2884
|
if ((code ?? 1) !== 0) {
|
|
2882
|
-
const
|
|
2885
|
+
const closeDiagnostic = piLaunchDiagnostic(
|
|
2886
|
+
spawnSpec,
|
|
2887
|
+
"close",
|
|
2888
|
+
code,
|
|
2889
|
+
signal,
|
|
2890
|
+
stderrTail,
|
|
2891
|
+
);
|
|
2892
|
+
options.onChildEvent?.({ ...closeDiagnostic, correlationId });
|
|
2893
|
+
const transcriptDiagnostic = stderrTail && messages.some((message) => message.content === stderrTail)
|
|
2894
|
+
? { ...closeDiagnostic, stderrTail: "" }
|
|
2895
|
+
: closeDiagnostic;
|
|
2883
2896
|
appendBoundedTranscriptMessage(messages, {
|
|
2884
2897
|
role: "system",
|
|
2885
2898
|
content:
|
|
2886
2899
|
`Teammate child process exited abnormally (agent=${params.agent}, `
|
|
2887
|
-
+ `correlationId=${correlationId},
|
|
2888
|
-
+ `elapsed=${Date.now() - startTime}ms, tools=${progress.toolCount})
|
|
2889
|
-
+ (detail ? `\nstderr tail:\n${truncateUtf8Tail(detail, EXECUTION_BUFFER_LIMITS.stderrBytes)}` : ""),
|
|
2900
|
+
+ `correlationId=${correlationId}, ${formatPiLaunchDiagnostic(transcriptDiagnostic)}, `
|
|
2901
|
+
+ `elapsed=${Date.now() - startTime}ms, tools=${progress.toolCount}).`,
|
|
2890
2902
|
});
|
|
2891
2903
|
}
|
|
2892
2904
|
|
|
@@ -2989,9 +3001,17 @@ export async function runSingleAttempt(
|
|
|
2989
3001
|
progress.durationMs = Date.now() - startTime;
|
|
2990
3002
|
options.onProgress?.(progress);
|
|
2991
3003
|
|
|
3004
|
+
const childErrorDiagnostic = piLaunchDiagnostic(
|
|
3005
|
+
spawnSpec,
|
|
3006
|
+
"child-error",
|
|
3007
|
+
null,
|
|
3008
|
+
child.signalCode,
|
|
3009
|
+
state.stderrBuffer,
|
|
3010
|
+
);
|
|
3011
|
+
options.onChildEvent?.({ ...childErrorDiagnostic, correlationId });
|
|
2992
3012
|
const processError =
|
|
2993
3013
|
`Teammate child process error (agent=${params.agent}, model=${state.resolvedModel || "unknown"}, `
|
|
2994
|
-
+ `correlationId=${correlationId},
|
|
3014
|
+
+ `correlationId=${correlationId}, ${formatPiLaunchDiagnostic(childErrorDiagnostic)}): ${error.message}`;
|
|
2995
3015
|
if (state.initialResultPublished) {
|
|
2996
3016
|
appendBoundedTranscriptMessage(messages, {
|
|
2997
3017
|
role: "system",
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* TUI: Alt+R mode-aware session list, widget above editor, Alt+B foreground→background detach
|
|
6
6
|
* Mode: RPC subprocess — stdin open for steer/follow_up/abort
|
|
7
7
|
*/
|
|
8
|
-
import
|
|
8
|
+
import { registerForegroundDetach, setPersistentUi } from "../public/v1/foreground-detach.ts";
|
|
9
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import type { WorkspaceSessionScan } from "../transcript/session-transcript.ts";
|
|
10
11
|
import type { RecentToolInfo } from "../shared/types.ts";
|
|
11
12
|
import { type WorkspaceBackgroundJobSnapshot, type WorkspaceOwnerState } from "./workspace-peers.ts";
|
|
@@ -122,9 +123,7 @@ export declare function backgroundWaitGuidance(correlationId: string): string;
|
|
|
122
123
|
* discoverable across the root single, root graph, and nested foreground paths.
|
|
123
124
|
*/
|
|
124
125
|
export declare const FOREGROUND_DETACH_HINT: string;
|
|
125
|
-
export
|
|
126
|
-
/** Registers one foreground owner; unregister is idempotent on every race path. */
|
|
127
|
-
export declare function registerForegroundDetach(detach: () => void, ui?: ExtensionUIContext): () => void;
|
|
126
|
+
export { registerForegroundDetach, setPersistentUi };
|
|
128
127
|
export declare function foregroundWaitWindowMs(tasks: ReadonlyArray<{
|
|
129
128
|
timeoutMs?: number;
|
|
130
129
|
}>, fallbackMs?: number): number;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export declare function setPersistentUi(ui: ExtensionUIContext | undefined, resetOwners?: boolean): void;
|
|
3
|
+
/** Registers one foreground owner; unregister is idempotent on every race path. */
|
|
4
|
+
export declare function registerForegroundDetach(detach: () => void, ui?: ExtensionUIContext): () => void;
|
|
@@ -16,6 +16,7 @@ export * from "./completion-durability.ts";
|
|
|
16
16
|
export * from "./events.ts";
|
|
17
17
|
export * from "./execution.ts";
|
|
18
18
|
export * from "./extension.ts";
|
|
19
|
+
export * from "./foreground-detach.ts";
|
|
19
20
|
export * from "./mailbox.ts";
|
|
20
21
|
export * from "./model-routing.ts";
|
|
21
22
|
export * from "./monitor-window-state.ts";
|
|
@@ -571,18 +571,46 @@ export declare function buildExpertLeaderPrompt(objective: string): string;
|
|
|
571
571
|
export declare function prepareTeammateMode(params: RunTeammateParams): RunTeammateParams;
|
|
572
572
|
/** Normalize the tasks-only public contract into executable graph tasks. */
|
|
573
573
|
export declare function normalizeTeammateParams(params: RunTeammateParams): NormalizeTeammateResult;
|
|
574
|
-
export
|
|
575
|
-
export
|
|
574
|
+
export type PiLaunchSource = "override" | "path-native" | "windows-shim" | "host-entry" | "path-fallback";
|
|
575
|
+
export interface PiLaunchSpec {
|
|
576
|
+
command: string;
|
|
577
|
+
argsPrefix: string[];
|
|
578
|
+
source: PiLaunchSource;
|
|
579
|
+
}
|
|
576
580
|
export interface PiSpawnCommandOptions {
|
|
577
581
|
envBinary?: string | null;
|
|
578
582
|
entryPoint?: string | null;
|
|
579
583
|
platform?: NodeJS.Platform;
|
|
584
|
+
pathValue?: string | null;
|
|
585
|
+
argv?: readonly string[];
|
|
586
|
+
execPath?: string;
|
|
587
|
+
appData?: string | null;
|
|
588
|
+
/** @internal deterministic filesystem seam for focused launcher tests. */
|
|
589
|
+
isFile?: (candidate: string) => boolean;
|
|
590
|
+
/** @internal deterministic package-manifest seam for focused launcher tests. */
|
|
591
|
+
readTextFile?: (candidate: string) => string;
|
|
580
592
|
}
|
|
581
|
-
export declare
|
|
582
|
-
|
|
593
|
+
export declare let resolvedPiEntryPoint: string | null | undefined;
|
|
594
|
+
/**
|
|
595
|
+
* Resolve process.argv[1] only when its owning package manifest proves it is
|
|
596
|
+
* Pi's declared CLI. A generic .js/.mjs suffix is never evidence of identity.
|
|
597
|
+
*/
|
|
598
|
+
export declare function resolvePiEntryPoint(): string | null;
|
|
599
|
+
export declare function resolvePiLaunchSpec(options?: PiSpawnCommandOptions): PiLaunchSpec;
|
|
600
|
+
export declare function getPiSpawnCommand(args: string[], options?: PiSpawnCommandOptions): PiLaunchSpec & {
|
|
583
601
|
args: string[];
|
|
584
602
|
shell: false;
|
|
585
603
|
};
|
|
604
|
+
export interface PiLaunchDiagnostic {
|
|
605
|
+
type: "teammate_pi_launch_diagnostic";
|
|
606
|
+
source: PiLaunchSource;
|
|
607
|
+
phase: "spawn" | "child-error" | "close";
|
|
608
|
+
exitCode: number | null;
|
|
609
|
+
signal: NodeJS.Signals | null;
|
|
610
|
+
stderrTail: string;
|
|
611
|
+
}
|
|
612
|
+
export declare function piLaunchDiagnostic(launch: Pick<PiLaunchSpec, "source">, phase: PiLaunchDiagnostic["phase"], exitCode: number | null, signal: NodeJS.Signals | null, stderr: string): PiLaunchDiagnostic;
|
|
613
|
+
export declare function formatPiLaunchDiagnostic(diagnostic: PiLaunchDiagnostic): string;
|
|
586
614
|
export interface InteractiveTerminalLaunchOptions {
|
|
587
615
|
platform?: NodeJS.Platform;
|
|
588
616
|
terminalCommand?: string;
|