@ai-devkit/agent-manager 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +25 -9
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/fixtures.test.js.map +1 -1
- package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -1
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -1
- package/dist/__tests__/harnesses/muse/MuseAdapter.test.js +247 -0
- package/dist/__tests__/harnesses/muse/MuseAdapter.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js +81 -0
- package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js +185 -0
- package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js.map +1 -0
- package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js +328 -0
- package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js.map +1 -0
- package/dist/__tests__/harnesses/opencode/OpenCodeSessionParser.test.js.map +1 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +42 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
- package/dist/__tests__/harnesses/runtimeProfiles.test.js +12 -0
- package/dist/__tests__/harnesses/runtimeProfiles.test.js.map +1 -1
- package/dist/__tests__/utils/process.test.js +71 -1
- package/dist/__tests__/utils/process.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +8 -1
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js +2 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/fixtures/capture.d.ts.map +1 -1
- package/dist/fixtures/capture.js +5 -4
- package/dist/fixtures/capture.js.map +1 -1
- package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinAdapter.js.map +1 -1
- package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -1
- package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -1
- package/dist/harnesses/devin/DevinSessionParser.js.map +1 -1
- package/dist/harnesses/index.d.ts.map +1 -1
- package/dist/harnesses/index.js +3 -1
- package/dist/harnesses/index.js.map +1 -1
- package/dist/harnesses/muse/MuseAdapter.d.ts +33 -0
- package/dist/harnesses/muse/MuseAdapter.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseAdapter.js +122 -0
- package/dist/harnesses/muse/MuseAdapter.js.map +1 -0
- package/dist/harnesses/muse/MuseAgentMapper.d.ts +15 -0
- package/dist/harnesses/muse/MuseAgentMapper.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseAgentMapper.js +29 -0
- package/dist/harnesses/muse/MuseAgentMapper.js.map +1 -0
- package/dist/harnesses/muse/MuseSessionLocator.d.ts +61 -0
- package/dist/harnesses/muse/MuseSessionLocator.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseSessionLocator.js +233 -0
- package/dist/harnesses/muse/MuseSessionLocator.js.map +1 -0
- package/dist/harnesses/muse/MuseSessionParser.d.ts +77 -0
- package/dist/harnesses/muse/MuseSessionParser.d.ts.map +1 -0
- package/dist/harnesses/muse/MuseSessionParser.js +350 -0
- package/dist/harnesses/muse/MuseSessionParser.js.map +1 -0
- package/dist/harnesses/muse/readiness.d.ts +3 -0
- package/dist/harnesses/muse/readiness.d.ts.map +1 -0
- package/dist/harnesses/muse/readiness.js +37 -0
- package/dist/harnesses/muse/readiness.js.map +1 -0
- package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
- package/dist/harnesses/readiness/AgentReadiness.js +3 -1
- package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
- package/dist/harnesses/readiness/types.d.ts +1 -1
- package/dist/harnesses/readiness/types.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.js +2 -1
- package/dist/harnesses/runtimeProfiles.js.map +1 -1
- package/dist/harnesses/shared.d.ts +1 -1
- package/dist/harnesses/shared.d.ts.map +1 -1
- package/dist/harnesses/shared.js +6 -5
- package/dist/harnesses/shared.js.map +1 -1
- package/dist/utils/process.d.ts +23 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +44 -8
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +34 -8
- package/src/__tests__/fixtures.test.ts +15 -17
- package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +1 -3
- package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +3 -9
- package/src/__tests__/harnesses/muse/MuseAdapter.test.ts +197 -0
- package/src/__tests__/harnesses/muse/MuseAgentMapper.test.ts +83 -0
- package/src/__tests__/harnesses/muse/MuseSessionLocator.test.ts +177 -0
- package/src/__tests__/harnesses/muse/MuseSessionParser.test.ts +279 -0
- package/src/__tests__/harnesses/opencode/OpenCodeSessionParser.test.ts +3 -2
- package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +36 -0
- package/src/__tests__/harnesses/runtimeProfiles.test.ts +13 -0
- package/src/__tests__/utils/process.test.ts +30 -0
- package/src/adapters/AgentAdapter.ts +9 -0
- package/src/fixtures/capture.ts +32 -17
- package/src/harnesses/devin/DevinAdapter.ts +3 -15
- package/src/harnesses/devin/DevinAgentMapper.ts +1 -5
- package/src/harnesses/devin/DevinSessionParser.ts +3 -12
- package/src/harnesses/index.ts +2 -0
- package/src/harnesses/muse/MuseAdapter.ts +157 -0
- package/src/harnesses/muse/MuseAgentMapper.ts +33 -0
- package/src/harnesses/muse/MuseSessionLocator.ts +286 -0
- package/src/harnesses/muse/MuseSessionParser.ts +435 -0
- package/src/harnesses/muse/readiness.ts +38 -0
- package/src/harnesses/readiness/AgentReadiness.ts +2 -0
- package/src/harnesses/runtimeProfiles.ts +1 -0
- package/src/harnesses/shared.ts +10 -5
- package/src/utils/process.ts +61 -9
package/dist/harnesses/shared.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* matching and parsing; these cover the parts that are the same everywhere.
|
|
4
4
|
*/ import { AgentStatus } from "../adapters/AgentAdapter.js";
|
|
5
5
|
import { generateAgentName } from "../utils/matching.js";
|
|
6
|
-
import { captureProcessSnapshot, executableBasename, filterByProcessNames } from "../utils/process.js";
|
|
6
|
+
import { captureProcessSnapshot, executableBasename, filterByProcessNames, snapshotOptions } from "../utils/process.js";
|
|
7
7
|
/** A session untouched for longer than this is IDLE. */ export const IDLE_THRESHOLD_MINUTES = 5;
|
|
8
8
|
/** Maximum length of a session summary derived from a user prompt. */ export const SUMMARY_MAX_LENGTH = 120;
|
|
9
9
|
/**
|
|
@@ -11,10 +11,11 @@ import { captureProcessSnapshot, executableBasename, filterByProcessNames } from
|
|
|
11
11
|
* capture when called standalone. `relevant` also keeps helper processes
|
|
12
12
|
* (e.g. a harness's child runtime) that detection may need to walk through.
|
|
13
13
|
*/ export async function findHarnessProcesses(adapter, context) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
14
|
+
const prefixes = adapter.processNamePrefixes ? [
|
|
15
|
+
...adapter.processNamePrefixes
|
|
16
|
+
] : [];
|
|
17
|
+
const snapshot = context?.processes ?? await captureProcessSnapshot(adapter.processNames, snapshotOptions(prefixes, (process1)=>adapter.canHandle(process1)));
|
|
18
|
+
const relevant = filterByProcessNames(snapshot, adapter.processNames, prefixes);
|
|
18
19
|
const byPid = new Map();
|
|
19
20
|
for (const process1 of relevant){
|
|
20
21
|
if (!byPid.has(process1.pid) && adapter.canHandle(process1)) byPid.set(process1.pid, process1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/harnesses/shared.ts"],"sourcesContent":["/**\n * Helpers shared by the harness adapters. Each harness keeps its own session\n * matching and parsing; these cover the parts that are the same everywhere.\n */\n\nimport type {\n AgentAdapter,\n AgentDetectionContext,\n AgentInfo,\n AgentType,\n ProcessInfo,\n} from \"../adapters/AgentAdapter.js\";\nimport { AgentStatus } from \"../adapters/AgentAdapter.js\";\nimport { generateAgentName } from \"../utils/matching.js\";\nimport {\n captureProcessSnapshot,\n executableBasename,\n filterByProcessNames,\n} from \"../utils/process.js\";\n\n/** A session untouched for longer than this is IDLE. */\nexport const IDLE_THRESHOLD_MINUTES = 5;\n\n/** Maximum length of a session summary derived from a user prompt. */\nexport const SUMMARY_MAX_LENGTH = 120;\n\nexport interface HarnessProcesses {\n /** Every process whose executable is one of the adapter's `processNames`. */\n relevant: ProcessInfo[];\n /** The subset `canHandle` accepts (deduplicated by PID): the agents. */\n processes: ProcessInfo[];\n}\n\n/**\n * The adapter's processes from the manager's shared snapshot, or from its own\n * capture when called standalone. `relevant` also keeps helper processes\n * (e.g. a harness's child runtime) that detection may need to walk through.\n */\nexport async function findHarnessProcesses(\n adapter: Pick<AgentAdapter, \"canHandle\"> & {
|
|
1
|
+
{"version":3,"sources":["../../src/harnesses/shared.ts"],"sourcesContent":["/**\n * Helpers shared by the harness adapters. Each harness keeps its own session\n * matching and parsing; these cover the parts that are the same everywhere.\n */\n\nimport type {\n AgentAdapter,\n AgentDetectionContext,\n AgentInfo,\n AgentType,\n ProcessInfo,\n} from \"../adapters/AgentAdapter.js\";\nimport { AgentStatus } from \"../adapters/AgentAdapter.js\";\nimport { generateAgentName } from \"../utils/matching.js\";\nimport {\n captureProcessSnapshot,\n executableBasename,\n filterByProcessNames,\n snapshotOptions,\n} from \"../utils/process.js\";\n\n/** A session untouched for longer than this is IDLE. */\nexport const IDLE_THRESHOLD_MINUTES = 5;\n\n/** Maximum length of a session summary derived from a user prompt. */\nexport const SUMMARY_MAX_LENGTH = 120;\n\nexport interface HarnessProcesses {\n /** Every process whose executable is one of the adapter's `processNames`. */\n relevant: ProcessInfo[];\n /** The subset `canHandle` accepts (deduplicated by PID): the agents. */\n processes: ProcessInfo[];\n}\n\n/**\n * The adapter's processes from the manager's shared snapshot, or from its own\n * capture when called standalone. `relevant` also keeps helper processes\n * (e.g. a harness's child runtime) that detection may need to walk through.\n */\nexport async function findHarnessProcesses(\n adapter: Pick<AgentAdapter, \"canHandle\" | \"processNamePrefixes\"> & {\n readonly processNames: readonly string[];\n },\n context?: AgentDetectionContext,\n): Promise<HarnessProcesses> {\n const prefixes = adapter.processNamePrefixes ? [...adapter.processNamePrefixes] : [];\n const snapshot =\n context?.processes ??\n (await captureProcessSnapshot(\n adapter.processNames,\n snapshotOptions(prefixes, (process) => adapter.canHandle(process)),\n ));\n const relevant = filterByProcessNames(snapshot, adapter.processNames, prefixes);\n\n const byPid = new Map<number, ProcessInfo>();\n for (const process of relevant) {\n if (!byPid.has(process.pid) && adapter.canHandle(process)) byPid.set(process.pid, process);\n }\n return { relevant, processes: Array.from(byPid.values()) };\n}\n\n/** Whether argv[0]'s basename is `name` or `name.exe`. */\nexport function matchesExecutable(command: string, name: string): boolean {\n const base = executableBasename(command);\n return base === name || base === `${name}.exe`;\n}\n\n/**\n * The placeholder agent for a running process with no matched session:\n * RUNNING, with a `pid-<pid>` session id.\n */\nexport function processOnlyAgent(\n type: AgentType,\n processInfo: ProcessInfo,\n { summary, cwd = processInfo.cwd ?? \"\" }: { summary: string; cwd?: string },\n): AgentInfo {\n return {\n name: generateAgentName(cwd, processInfo.pid),\n type,\n status: AgentStatus.RUNNING,\n summary,\n pid: processInfo.pid,\n projectPath: cwd,\n sessionId: `pid-${processInfo.pid}`,\n lastActive: new Date(),\n };\n}\n\nexport function isIdle(lastActive: Date, now = Date.now()): boolean {\n return (now - lastActive.getTime()) / 60000 > IDLE_THRESHOLD_MINUTES;\n}\n\nexport function homeDir(): string {\n return process.env.HOME || process.env.USERPROFILE || \"\";\n}\n\n/** ISO strings, or epoch numbers in seconds or milliseconds; null when invalid. */\nexport function parseTimestamp(value: unknown): Date | null {\n if (typeof value === \"number\") {\n const date = new Date(value < 1_000_000_000_000 ? value * 1000 : value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n if (typeof value !== \"string\" || !value) return null;\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\nexport function truncate(value: string, maxLength: number): string {\n if (value.length <= maxLength) return value;\n return `${value.slice(0, maxLength - 3)}...`;\n}\n\n/** Flatten a string, or an array of `{ text }` blocks, to plain text. */\nexport function flattenTextBlocks(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n return content\n .map((block) =>\n block && typeof block === \"object\" && typeof (block as { text?: unknown }).text === \"string\"\n ? (block as { text: string }).text\n : \"\",\n )\n .join(\"\");\n}\n"],"names":["AgentStatus","generateAgentName","captureProcessSnapshot","executableBasename","filterByProcessNames","snapshotOptions","IDLE_THRESHOLD_MINUTES","SUMMARY_MAX_LENGTH","findHarnessProcesses","adapter","context","prefixes","processNamePrefixes","snapshot","processes","processNames","process","canHandle","relevant","byPid","Map","has","pid","set","Array","from","values","matchesExecutable","command","name","base","processOnlyAgent","type","processInfo","summary","cwd","status","RUNNING","projectPath","sessionId","lastActive","Date","isIdle","now","getTime","homeDir","env","HOME","USERPROFILE","parseTimestamp","value","date","Number","isNaN","truncate","maxLength","length","slice","flattenTextBlocks","content","isArray","map","block","text","join"],"mappings":"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"}
|
package/dist/utils/process.d.ts
CHANGED
|
@@ -19,7 +19,18 @@ import type { ProcessInfo } from "../adapters/AgentAdapter.js";
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function executablePath(command: string): string;
|
|
21
21
|
export declare function executableBasename(command: string): string;
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Whether argv[0]'s basename is exactly `prefix` or starts with `prefix-`.
|
|
24
|
+
*
|
|
25
|
+
* Covers versioned launchers such as `muse-bin-1.4.4-R5419.1` for prefix
|
|
26
|
+
* `muse`. This is a recall-oriented prefilter: adapters still apply
|
|
27
|
+
* `canHandle` for precise shaping, so a coincidental `prefix-*` binary
|
|
28
|
+
* cannot leak into another harness.
|
|
29
|
+
*/
|
|
30
|
+
export declare function matchesExecutablePrefix(command: string, prefixes: ReadonlySet<string>): boolean;
|
|
31
|
+
/** Whether argv[0] matches one of `names` exactly or one of `prefixes`. */
|
|
32
|
+
export declare function matchesProcessPatterns(command: string, names: ReadonlySet<string>, prefixes?: ReadonlySet<string>): boolean;
|
|
33
|
+
export declare function filterByProcessNames(processes: readonly ProcessInfo[], namePatterns: readonly string[], namePrefixes?: readonly string[]): ProcessInfo[];
|
|
23
34
|
/**
|
|
24
35
|
* List running processes matching an agent executable name.
|
|
25
36
|
*
|
|
@@ -39,8 +50,19 @@ export interface ProcessSnapshotOptions {
|
|
|
39
50
|
* only) so adapters can walk parent chains. Omitted = enrich everything.
|
|
40
51
|
*/
|
|
41
52
|
isCandidate?: (process: ProcessInfo) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Basename prefixes matched like `processNamePrefixes`: exact or
|
|
55
|
+
* `<prefix>-*`. Same validation as namePatterns.
|
|
56
|
+
*/
|
|
57
|
+
namePrefixes?: readonly string[];
|
|
42
58
|
}
|
|
43
59
|
export type ProcessSnapshotCapture = (namePatterns: readonly string[], options?: ProcessSnapshotOptions) => Promise<ProcessInfo[]>;
|
|
60
|
+
/**
|
|
61
|
+
* One construction site for snapshot options: `namePrefixes` is sent only
|
|
62
|
+
* when non-empty, so adapters without prefixes keep the exact legacy call
|
|
63
|
+
* shape that existing mock assertions pin.
|
|
64
|
+
*/
|
|
65
|
+
export declare function snapshotOptions(namePrefixes: readonly string[], isCandidate: (process: ProcessInfo) => boolean): ProcessSnapshotOptions;
|
|
44
66
|
/** Working directories of `pids` (async `lsof`); PIDs it cannot read are omitted. */
|
|
45
67
|
export declare function getProcessCwdsAsync(pids: number[]): Promise<Map<number, string>>;
|
|
46
68
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/utils/process.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAmB/D;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWtD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAkCD,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,YAAY,EAAE,SAAS,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/utils/process.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAmB/D;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWtD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAkCD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAQ/F;AAED,2EAA2E;AAC3E,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,EAC1B,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,OAAO,CAET;AAED,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,YAAY,GAAE,SAAS,MAAM,EAAO,GACnC,WAAW,EAAE,CAKf;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,EAAE,CA0CrE;AAED,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAErF,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;IAChD;;;OAGG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,MAAM,sBAAsB,GAAG,CACnC,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,OAAO,CAAC,EAAE,sBAAsB,KAC7B,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;AAE5B;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,WAAW,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,GAC7C,sBAAsB,CAIxB;AA4DD,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAEhF;AAwED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,WAAW,CAAA;CAAO,GAChC,sBAAsB,CAwDxB;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,sBAAuD,CAAC;AAE7F;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAuCvE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAiC3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAavE;AAaD,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,WAAW,EAAE,EACxB,KAAK,EAAE,WAAW,GACjB,WAAW,GAAG,SAAS,CAKzB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,WAAW,EAAE,EACxB,gBAAgB,GAAE,WAAW,EAAO,GACnC,GAAG,CAAC,MAAM,CAAC,CAqBb;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASjD"}
|
package/dist/utils/process.js
CHANGED
|
@@ -69,10 +69,30 @@ function normalizedProcessNames(namePatterns) {
|
|
|
69
69
|
const mayContinuePath = tokens.slice(1).some((token)=>PATH_SEPARATOR.test(token) && names.has(normalizeExecutableName(pathBasename(token))));
|
|
70
70
|
return mayContinuePath && names.has(normalizeExecutableName(executableBasename(command)));
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Whether argv[0]'s basename is exactly `prefix` or starts with `prefix-`.
|
|
74
|
+
*
|
|
75
|
+
* Covers versioned launchers such as `muse-bin-1.4.4-R5419.1` for prefix
|
|
76
|
+
* `muse`. This is a recall-oriented prefilter: adapters still apply
|
|
77
|
+
* `canHandle` for precise shaping, so a coincidental `prefix-*` binary
|
|
78
|
+
* cannot leak into another harness.
|
|
79
|
+
*/ export function matchesExecutablePrefix(command, prefixes) {
|
|
80
|
+
if (prefixes.size === 0) return false;
|
|
81
|
+
const base = normalizeExecutableName(pathBasename(command.trim().split(/\s+/)[0] ?? ""));
|
|
82
|
+
if (prefixes.has(base)) return true;
|
|
83
|
+
for (const prefix of prefixes){
|
|
84
|
+
if (base.startsWith(`${prefix}-`)) return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/** Whether argv[0] matches one of `names` exactly or one of `prefixes`. */ export function matchesProcessPatterns(command, names, prefixes = new Set()) {
|
|
89
|
+
return matchesExecutableName(command, names) || matchesExecutablePrefix(command, prefixes);
|
|
90
|
+
}
|
|
91
|
+
export function filterByProcessNames(processes, namePatterns, namePrefixes = []) {
|
|
73
92
|
const names = normalizedProcessNames(namePatterns);
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
const prefixes = normalizedProcessNames(namePrefixes);
|
|
94
|
+
if (names.size === 0 && prefixes.size === 0) return [];
|
|
95
|
+
return processes.filter((process)=>matchesProcessPatterns(process.command, names, prefixes));
|
|
76
96
|
}
|
|
77
97
|
/**
|
|
78
98
|
* List running processes matching an agent executable name.
|
|
@@ -122,6 +142,20 @@ export function filterByProcessNames(processes, namePatterns) {
|
|
|
122
142
|
return [];
|
|
123
143
|
}
|
|
124
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* One construction site for snapshot options: `namePrefixes` is sent only
|
|
147
|
+
* when non-empty, so adapters without prefixes keep the exact legacy call
|
|
148
|
+
* shape that existing mock assertions pin.
|
|
149
|
+
*/ export function snapshotOptions(namePrefixes, isCandidate) {
|
|
150
|
+
return namePrefixes.length > 0 ? {
|
|
151
|
+
namePrefixes: [
|
|
152
|
+
...namePrefixes
|
|
153
|
+
],
|
|
154
|
+
isCandidate
|
|
155
|
+
} : {
|
|
156
|
+
isCandidate
|
|
157
|
+
};
|
|
158
|
+
}
|
|
125
159
|
const execFileText = (file, args)=>new Promise((resolve, reject)=>{
|
|
126
160
|
execFile(file, args, {
|
|
127
161
|
encoding: "utf-8",
|
|
@@ -134,7 +168,7 @@ const execFileText = (file, args)=>new Promise((resolve, reject)=>{
|
|
|
134
168
|
resolve(stdout);
|
|
135
169
|
});
|
|
136
170
|
});
|
|
137
|
-
function parseProcessList(output, namePatterns) {
|
|
171
|
+
function parseProcessList(output, namePatterns, namePrefixes = new Set()) {
|
|
138
172
|
const allPids = new Set();
|
|
139
173
|
const matched = [];
|
|
140
174
|
for (const line of output.trim().split("\n")){
|
|
@@ -147,7 +181,7 @@ function parseProcessList(output, namePatterns) {
|
|
|
147
181
|
allPids.add(pid);
|
|
148
182
|
const tty = match[3];
|
|
149
183
|
const command = match[4];
|
|
150
|
-
if (!
|
|
184
|
+
if (!matchesProcessPatterns(command, namePatterns, namePrefixes)) continue;
|
|
151
185
|
matched.push({
|
|
152
186
|
pid,
|
|
153
187
|
ppid,
|
|
@@ -248,14 +282,16 @@ function processIdentity(process) {
|
|
|
248
282
|
return cached?.identity === processIdentity(process) ? cached.startTime : undefined;
|
|
249
283
|
};
|
|
250
284
|
return async (namePatterns, options = {})=>{
|
|
251
|
-
const
|
|
252
|
-
|
|
285
|
+
const valid = (name)=>Boolean(name) && VALID_EXECUTABLE_NAME.test(name);
|
|
286
|
+
const names = normalizedProcessNames(namePatterns.filter(valid));
|
|
287
|
+
const prefixes = normalizedProcessNames((options.namePrefixes ?? []).filter(valid));
|
|
288
|
+
if (names.size === 0 && prefixes.size === 0) return [];
|
|
253
289
|
try {
|
|
254
290
|
const output = await exec("ps", [
|
|
255
291
|
"-axo",
|
|
256
292
|
"pid=,ppid=,tty=,command="
|
|
257
293
|
]);
|
|
258
|
-
const { allPids, matched } = parseProcessList(output, names);
|
|
294
|
+
const { allPids, matched } = parseProcessList(output, names, prefixes);
|
|
259
295
|
for (const pid of startTimeCache.keys()){
|
|
260
296
|
if (!allPids.has(pid)) startTimeCache.delete(pid);
|
|
261
297
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/process.ts"],"sourcesContent":["/**\n * Process Detection Utilities\n *\n * Shared shell command wrappers for detecting and inspecting running processes.\n * Built-in refresh discovery uses captureProcessSnapshot(); synchronous helpers\n * remain exported for compatibility with existing consumers.\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { execFile, execFileSync } from \"child_process\";\nimport type { ProcessInfo } from \"../adapters/AgentAdapter.js\";\n\nconst PROCESS_EXEC_MAX_BUFFER = 10 * 1024 * 1024;\nconst VALID_EXECUTABLE_NAME = /^[a-zA-Z0-9_-]+$/;\nconst ABSOLUTE_PATH = /^(?:\\/|[a-zA-Z]:[\\\\/])/;\nconst PATH_SEPARATOR = /[\\\\/]/;\n\nfunction pathBasename(executable: string): string {\n return path.basename(executable.replace(/\\\\/g, \"/\")).toLowerCase();\n}\n\nfunction isFile(filePath: string): boolean {\n try {\n return fs.statSync(filePath, { throwIfNoEntry: false })?.isFile() ?? false;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve argv[0] from a `ps` command line.\n *\n * `ps` joins argv with spaces, so an executable installed under a directory\n * containing spaces (`/Applications/Some App.app/Contents/Resources/codex`)\n * spans several whitespace tokens. When the first token of an absolute path\n * is not itself a file, longer space-joined prefixes ending in a token with a\n * path separator are tried, and the first one that exists on disk wins.\n * Otherwise the first token is returned, so arguments are never folded into\n * the executable path unless that combined path really is a file.\n */\nexport function executablePath(command: string): string {\n const parts = command.trim().split(/(\\s+)/);\n const first = parts[0] ?? \"\";\n if (parts.length === 1 || !ABSOLUTE_PATH.test(first) || isFile(first)) return first;\n\n let prefix = first;\n for (let i = 2; i < parts.length; i += 2) {\n prefix += parts[i - 1] + parts[i];\n if (PATH_SEPARATOR.test(parts[i]) && isFile(prefix)) return prefix;\n }\n return first;\n}\n\nexport function executableBasename(command: string): string {\n return pathBasename(executablePath(command));\n}\n\nfunction normalizeExecutableName(name: string): string {\n const lower = name.toLowerCase();\n return lower.endsWith(\".exe\") ? lower.slice(0, -4) : lower;\n}\n\nfunction normalizedProcessNames(namePatterns: readonly string[]): Set<string> {\n return new Set(namePatterns.filter(Boolean).map(normalizeExecutableName));\n}\n\n/**\n * Whether argv[0] of `command` is one of `names`.\n *\n * The first token is checked with string operations only, as before. The\n * filesystem is consulted only for absolute commands where a later\n * path-like token ends in one of the names, i.e. a possible executable under\n * a directory containing spaces.\n */\nfunction matchesExecutableName(command: string, names: ReadonlySet<string>): boolean {\n const tokens = command.trim().split(/\\s+/);\n const first = tokens[0] ?? \"\";\n if (names.has(normalizeExecutableName(pathBasename(first)))) return true;\n if (!ABSOLUTE_PATH.test(first)) return false;\n\n const mayContinuePath = tokens\n .slice(1)\n .some(\n (token) =>\n PATH_SEPARATOR.test(token) && names.has(normalizeExecutableName(pathBasename(token))),\n );\n return mayContinuePath && names.has(normalizeExecutableName(executableBasename(command)));\n}\n\nexport function filterByProcessNames(\n processes: readonly ProcessInfo[],\n namePatterns: readonly string[],\n): ProcessInfo[] {\n const names = normalizedProcessNames(namePatterns);\n if (names.size === 0) return [];\n return processes.filter((process) => matchesExecutableName(process.command, names));\n}\n\n/**\n * List running processes matching an agent executable name.\n *\n * Uses `ps -axo` then filters in JS for exact executable basename match.\n * This avoids shell pipelines and string interpolation.\n *\n * Returned ProcessInfo has pid, ppid, command, tty populated.\n * cwd and startTime are NOT populated — call enrichProcesses() to fill them.\n */\nexport function listAgentProcesses(namePattern: string): ProcessInfo[] {\n // Validate pattern contains only safe characters (alphanumeric, dash, underscore)\n if (!namePattern || !VALID_EXECUTABLE_NAME.test(namePattern)) {\n return [];\n }\n\n try {\n const output = execFileSync(\"ps\", [\"-axo\", \"pid=,ppid=,tty=,command=\"], { encoding: \"utf-8\" });\n\n const names = normalizedProcessNames([namePattern]);\n const processes: ProcessInfo[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n\n const tty = match[3];\n const command = match[4];\n\n if (!matchesExecutableName(command, names)) continue;\n\n const ttyShort = tty.startsWith(\"/dev/\") ? tty.slice(5) : tty;\n\n processes.push({\n pid,\n ppid,\n command,\n cwd: \"\",\n tty: ttyShort,\n });\n }\n\n return processes;\n } catch {\n return [];\n }\n}\n\n/** Runs an executable and resolves with its stdout. Injectable for tests. */\nexport type ProcessExec = (file: string, args: readonly string[]) => Promise<string>;\n\nexport interface ProcessSnapshotOptions {\n /**\n * Decides which name-matched processes are worth enriching with cwd and\n * start time. Non-candidates are still returned (pid, ppid, tty, command\n * only) so adapters can walk parent chains. Omitted = enrich everything.\n */\n isCandidate?: (process: ProcessInfo) => boolean;\n}\n\nexport type ProcessSnapshotCapture = (\n namePatterns: readonly string[],\n options?: ProcessSnapshotOptions,\n) => Promise<ProcessInfo[]>;\n\nconst execFileText: ProcessExec = (file, args) =>\n new Promise((resolve, reject) => {\n execFile(\n file,\n args,\n { encoding: \"utf-8\", maxBuffer: PROCESS_EXEC_MAX_BUFFER },\n (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout);\n },\n );\n });\n\ninterface ParsedProcessList {\n /** Every PID in the listing, used to evict cache entries for exited processes. */\n allPids: Set<number>;\n /** Processes whose executable matches one of the requested names. */\n matched: ProcessInfo[];\n}\n\nfunction parseProcessList(output: string, namePatterns: ReadonlySet<string>): ParsedProcessList {\n const allPids = new Set<number>();\n const matched: ProcessInfo[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n allPids.add(pid);\n\n const tty = match[3];\n const command = match[4];\n if (!matchesExecutableName(command, namePatterns)) continue;\n\n matched.push({\n pid,\n ppid,\n command,\n cwd: \"\",\n tty: tty.startsWith(\"/dev/\") ? tty.slice(5) : tty,\n });\n }\n\n return { allPids, matched };\n}\n\n/** Working directories of `pids` (async `lsof`); PIDs it cannot read are omitted. */\nexport function getProcessCwdsAsync(pids: number[]): Promise<Map<number, string>> {\n return batchGetProcessCwdsAsync(execFileText, pids);\n}\n\nasync function batchGetProcessCwdsAsync(\n exec: ProcessExec,\n pids: number[],\n): Promise<Map<number, string>> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = await exec(\"lsof\", [\"-a\", \"-d\", \"cwd\", \"-Fn\", \"-p\", pids.join(\",\")]);\n let currentPid: number | null = null;\n for (const line of output.trim().split(\"\\n\")) {\n if (line.startsWith(\"p\")) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith(\"n\") && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n return result;\n } catch {\n const entries = await Promise.all(\n pids.map(async (pid) => {\n try {\n const output = await exec(\"pwdx\", [String(pid)]);\n const match = output.match(/^\\d+:\\s*(.+)$/);\n return match ? ([pid, match[1].trim()] as const) : null;\n } catch {\n return null;\n }\n }),\n );\n for (const entry of entries) {\n if (entry) result.set(entry[0], entry[1]);\n }\n return result;\n }\n}\n\nasync function batchGetProcessStartTimesAsync(\n exec: ProcessExec,\n pids: number[],\n): Promise<Map<number, Date>> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = await exec(\"ps\", [\"-o\", \"pid=,lstart=\", \"-p\", pids.join(\",\")]);\n for (const rawLine of output.split(\"\\n\")) {\n const match = rawLine.trim().match(/^(\\d+)\\s+(.+)$/);\n if (!match) continue;\n const pid = parseInt(match[1], 10);\n const date = new Date(match[2].trim());\n if (Number.isFinite(pid) && !Number.isNaN(date.getTime())) result.set(pid, date);\n }\n } catch {\n // Return partial/empty data, matching the synchronous helper.\n }\n return result;\n}\n\ninterface CachedStartTime {\n /** ppid + command seen when the start time was read; a mismatch means PID reuse. */\n identity: string;\n startTime: Date;\n}\n\nfunction processIdentity(process: ProcessInfo): string {\n return `${process.ppid ?? \"\"}\\0${process.command}`;\n}\n\n/**\n * Create a snapshot capture function with its own start-time cache.\n *\n * Start times never change for a live process, so each PID is queried with\n * `ps lstart` once and reused on later refreshes. A cache entry is dropped\n * when its PID disappears from the base listing, and ignored when the PID's\n * ppid/command no longer match (PID reuse). cwd is re-read every refresh\n * because a process can change directory.\n */\nexport function createProcessSnapshotCapture(\n deps: { exec?: ProcessExec } = {},\n): ProcessSnapshotCapture {\n const exec = deps.exec ?? execFileText;\n const startTimeCache = new Map<number, CachedStartTime>();\n\n const cachedStartTime = (process: ProcessInfo): Date | undefined => {\n const cached = startTimeCache.get(process.pid);\n return cached?.identity === processIdentity(process) ? cached.startTime : undefined;\n };\n\n return async (namePatterns, options = {}) => {\n const names = normalizedProcessNames(\n namePatterns.filter((name) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name)),\n );\n if (names.size === 0) return [];\n\n try {\n const output = await exec(\"ps\", [\"-axo\", \"pid=,ppid=,tty=,command=\"]);\n const { allPids, matched } = parseProcessList(output, names);\n\n for (const pid of startTimeCache.keys()) {\n if (!allPids.has(pid)) startTimeCache.delete(pid);\n }\n\n const { isCandidate } = options;\n const candidates = isCandidate ? matched.filter((process) => isCandidate(process)) : matched;\n const candidatePids = candidates.map((process) => process.pid);\n const uncachedPids = candidates\n .filter((process) => cachedStartTime(process) === undefined)\n .map((process) => process.pid);\n\n const [cwdMap, startTimeMap] = await Promise.all([\n batchGetProcessCwdsAsync(exec, candidatePids),\n batchGetProcessStartTimesAsync(exec, uncachedPids),\n ]);\n\n for (const process of candidates) {\n const startTime = startTimeMap.get(process.pid);\n if (startTime) {\n startTimeCache.set(process.pid, { identity: processIdentity(process), startTime });\n }\n }\n\n const candidateSet = new Set(candidatePids);\n return matched.map((process) =>\n candidateSet.has(process.pid)\n ? {\n ...process,\n cwd: cwdMap.get(process.pid) || \"\",\n startTime: cachedStartTime(process),\n }\n : process,\n );\n } catch {\n return [];\n }\n };\n}\n\n/**\n * Capture and enrich relevant processes without blocking the event loop.\n * One base process listing is shared by every requested executable name;\n * only processes accepted by `options.isCandidate` are enriched.\n */\nexport const captureProcessSnapshot: ProcessSnapshotCapture = createProcessSnapshotCapture();\n\n/**\n * Batch-get current working directories for multiple PIDs.\n *\n * Single `lsof -a -d cwd -Fn -p PID1,PID2,...` call.\n * Returns partial results — if lsof fails for one PID, others still return.\n */\nexport function batchGetProcessCwds(pids: number[]): Map<number, string> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\"lsof\", [\"-a\", \"-d\", \"cwd\", \"-Fn\", \"-p\", pids.join(\",\")], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n\n // lsof output format: p{PID}\\nn{path}\\np{PID}\\nn{path}...\n let currentPid: number | null = null;\n for (const line of output.trim().split(\"\\n\")) {\n if (line.startsWith(\"p\")) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith(\"n\") && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n } catch {\n // Try per-PID fallback with pwdx (Linux)\n for (const pid of pids) {\n try {\n const output = execFileSync(\"pwdx\", [String(pid)], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n const match = output.match(/^\\d+:\\s*(.+)$/);\n if (match) {\n result.set(pid, match[1].trim());\n }\n } catch {\n // Skip this PID\n }\n }\n }\n\n return result;\n}\n\n/**\n * Batch-get process start times for multiple PIDs.\n *\n * Single `ps -o pid=,lstart= -p PID1,PID2,...` call.\n * Uses lstart format which gives full timestamp (e.g., \"Thu Feb 5 16:00:57 2026\").\n * Returns partial results.\n */\nexport function batchGetProcessStartTimes(pids: number[]): Map<number, Date> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\"ps\", [\"-o\", \"pid=,lstart=\", \"-p\", pids.join(\",\")], {\n encoding: \"utf-8\",\n });\n\n for (const rawLine of output.split(\"\\n\")) {\n const line = rawLine.trim();\n if (!line) continue;\n\n // Format: \" PID DAY MON DD HH:MM:SS YYYY\"\n // e.g., \" 78070 Wed Mar 18 23:18:01 2026\"\n const match = line.match(/^\\s*(\\d+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const dateStr = match[2].trim();\n\n if (!Number.isFinite(pid)) continue;\n\n const date = new Date(dateStr);\n if (!Number.isNaN(date.getTime())) {\n result.set(pid, date);\n }\n }\n } catch {\n // Return whatever we have\n }\n\n return result;\n}\n\n/**\n * Enrich ProcessInfo array with cwd and startTime.\n *\n * Calls batchGetProcessCwds and batchGetProcessStartTimes in batched shell calls,\n * then populates each ProcessInfo in-place. Returns partial results —\n * if a PID fails, that process keeps empty cwd / undefined startTime.\n */\nexport function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {\n if (processes.length === 0) return processes;\n\n const pids = processes.map((p) => p.pid);\n const cwdMap = batchGetProcessCwds(pids);\n const startTimeMap = batchGetProcessStartTimes(pids);\n\n for (const proc of processes) {\n proc.cwd = cwdMap.get(proc.pid) || \"\";\n proc.startTime = startTimeMap.get(proc.pid);\n }\n\n return processes;\n}\n\nfunction isSameTerminalProcess(proc: ProcessInfo, matched: ProcessInfo): boolean {\n const sameTty = proc.tty !== \"\" && proc.tty !== \"?\" && proc.tty === matched.tty;\n const sameCwd = proc.cwd !== \"\" && proc.cwd === matched.cwd;\n\n return sameTty && (sameCwd || proc.cwd === \"\" || matched.cwd === \"\");\n}\n\nfunction matchesProcessIdentity(proc: ProcessInfo, matched: ProcessInfo): boolean {\n return proc.pid === matched.pid || isSameTerminalProcess(proc, matched);\n}\n\nexport function findWrapperProcess(\n processes: ProcessInfo[],\n child: ProcessInfo,\n): ProcessInfo | undefined {\n return processes.find(\n (proc) =>\n proc.pid !== child.pid && child.ppid === proc.pid && matchesProcessIdentity(proc, child),\n );\n}\n\n/**\n * Find parent wrapper processes that should not be reported as separate agents.\n *\n * A process is considered a wrapper when it is the parent of another candidate\n * agent process in the same terminal/worktree, or when it points at the same\n * terminal/worktree as an already session-matched process.\n */\nexport function findWrapperProcessPids(\n processes: ProcessInfo[],\n matchedProcesses: ProcessInfo[] = [],\n): Set<number> {\n const wrappers = new Set<number>();\n\n for (const child of processes) {\n const wrapper = findWrapperProcess(processes, child);\n if (wrapper) {\n wrappers.add(wrapper.pid);\n }\n }\n\n for (const proc of processes) {\n if (\n matchedProcesses.some(\n (matched) => proc.pid !== matched.pid && isSameTerminalProcess(proc, matched),\n )\n ) {\n wrappers.add(proc.pid);\n }\n }\n\n return wrappers;\n}\n\n/**\n * Get the TTY device for a specific process\n */\nexport function getProcessTty(pid: number): string {\n try {\n const output = execFileSync(\"ps\", [\"-p\", String(pid), \"-o\", \"tty=\"], { encoding: \"utf-8\" });\n\n const tty = output.trim();\n return tty.startsWith(\"/dev/\") ? tty.slice(5) : tty;\n } catch {\n return \"?\";\n }\n}\n"],"names":["fs","path","execFile","execFileSync","PROCESS_EXEC_MAX_BUFFER","VALID_EXECUTABLE_NAME","ABSOLUTE_PATH","PATH_SEPARATOR","pathBasename","executable","basename","replace","toLowerCase","isFile","filePath","statSync","throwIfNoEntry","executablePath","command","parts","trim","split","first","length","test","prefix","i","executableBasename","normalizeExecutableName","name","lower","endsWith","slice","normalizedProcessNames","namePatterns","Set","filter","Boolean","map","matchesExecutableName","names","tokens","has","mayContinuePath","some","token","filterByProcessNames","processes","size","process","listAgentProcesses","namePattern","output","encoding","line","match","pid","parseInt","ppid","Number","isNaN","tty","ttyShort","startsWith","push","cwd","execFileText","file","args","Promise","resolve","reject","maxBuffer","error","stdout","parseProcessList","allPids","matched","add","getProcessCwdsAsync","pids","batchGetProcessCwdsAsync","exec","result","Map","join","currentPid","set","entries","all","String","entry","batchGetProcessStartTimesAsync","rawLine","date","Date","isFinite","getTime","processIdentity","createProcessSnapshotCapture","deps","startTimeCache","cachedStartTime","cached","get","identity","startTime","undefined","options","keys","delete","isCandidate","candidates","candidatePids","uncachedPids","cwdMap","startTimeMap","candidateSet","captureProcessSnapshot","batchGetProcessCwds","stdio","batchGetProcessStartTimes","dateStr","enrichProcesses","p","proc","isSameTerminalProcess","sameTty","sameCwd","matchesProcessIdentity","findWrapperProcess","child","find","findWrapperProcessPids","matchedProcesses","wrappers","wrapper","getProcessTty"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/process.ts"],"sourcesContent":["/**\n * Process Detection Utilities\n *\n * Shared shell command wrappers for detecting and inspecting running processes.\n * Built-in refresh discovery uses captureProcessSnapshot(); synchronous helpers\n * remain exported for compatibility with existing consumers.\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { execFile, execFileSync } from \"child_process\";\nimport type { ProcessInfo } from \"../adapters/AgentAdapter.js\";\n\nconst PROCESS_EXEC_MAX_BUFFER = 10 * 1024 * 1024;\nconst VALID_EXECUTABLE_NAME = /^[a-zA-Z0-9_-]+$/;\nconst ABSOLUTE_PATH = /^(?:\\/|[a-zA-Z]:[\\\\/])/;\nconst PATH_SEPARATOR = /[\\\\/]/;\n\nfunction pathBasename(executable: string): string {\n return path.basename(executable.replace(/\\\\/g, \"/\")).toLowerCase();\n}\n\nfunction isFile(filePath: string): boolean {\n try {\n return fs.statSync(filePath, { throwIfNoEntry: false })?.isFile() ?? false;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve argv[0] from a `ps` command line.\n *\n * `ps` joins argv with spaces, so an executable installed under a directory\n * containing spaces (`/Applications/Some App.app/Contents/Resources/codex`)\n * spans several whitespace tokens. When the first token of an absolute path\n * is not itself a file, longer space-joined prefixes ending in a token with a\n * path separator are tried, and the first one that exists on disk wins.\n * Otherwise the first token is returned, so arguments are never folded into\n * the executable path unless that combined path really is a file.\n */\nexport function executablePath(command: string): string {\n const parts = command.trim().split(/(\\s+)/);\n const first = parts[0] ?? \"\";\n if (parts.length === 1 || !ABSOLUTE_PATH.test(first) || isFile(first)) return first;\n\n let prefix = first;\n for (let i = 2; i < parts.length; i += 2) {\n prefix += parts[i - 1] + parts[i];\n if (PATH_SEPARATOR.test(parts[i]) && isFile(prefix)) return prefix;\n }\n return first;\n}\n\nexport function executableBasename(command: string): string {\n return pathBasename(executablePath(command));\n}\n\nfunction normalizeExecutableName(name: string): string {\n const lower = name.toLowerCase();\n return lower.endsWith(\".exe\") ? lower.slice(0, -4) : lower;\n}\n\nfunction normalizedProcessNames(namePatterns: readonly string[]): Set<string> {\n return new Set(namePatterns.filter(Boolean).map(normalizeExecutableName));\n}\n\n/**\n * Whether argv[0] of `command` is one of `names`.\n *\n * The first token is checked with string operations only, as before. The\n * filesystem is consulted only for absolute commands where a later\n * path-like token ends in one of the names, i.e. a possible executable under\n * a directory containing spaces.\n */\nfunction matchesExecutableName(command: string, names: ReadonlySet<string>): boolean {\n const tokens = command.trim().split(/\\s+/);\n const first = tokens[0] ?? \"\";\n if (names.has(normalizeExecutableName(pathBasename(first)))) return true;\n if (!ABSOLUTE_PATH.test(first)) return false;\n\n const mayContinuePath = tokens\n .slice(1)\n .some(\n (token) =>\n PATH_SEPARATOR.test(token) && names.has(normalizeExecutableName(pathBasename(token))),\n );\n return mayContinuePath && names.has(normalizeExecutableName(executableBasename(command)));\n}\n\n/**\n * Whether argv[0]'s basename is exactly `prefix` or starts with `prefix-`.\n *\n * Covers versioned launchers such as `muse-bin-1.4.4-R5419.1` for prefix\n * `muse`. This is a recall-oriented prefilter: adapters still apply\n * `canHandle` for precise shaping, so a coincidental `prefix-*` binary\n * cannot leak into another harness.\n */\nexport function matchesExecutablePrefix(command: string, prefixes: ReadonlySet<string>): boolean {\n if (prefixes.size === 0) return false;\n const base = normalizeExecutableName(pathBasename(command.trim().split(/\\s+/)[0] ?? \"\"));\n if (prefixes.has(base)) return true;\n for (const prefix of prefixes) {\n if (base.startsWith(`${prefix}-`)) return true;\n }\n return false;\n}\n\n/** Whether argv[0] matches one of `names` exactly or one of `prefixes`. */\nexport function matchesProcessPatterns(\n command: string,\n names: ReadonlySet<string>,\n prefixes: ReadonlySet<string> = new Set(),\n): boolean {\n return matchesExecutableName(command, names) || matchesExecutablePrefix(command, prefixes);\n}\n\nexport function filterByProcessNames(\n processes: readonly ProcessInfo[],\n namePatterns: readonly string[],\n namePrefixes: readonly string[] = [],\n): ProcessInfo[] {\n const names = normalizedProcessNames(namePatterns);\n const prefixes = normalizedProcessNames(namePrefixes);\n if (names.size === 0 && prefixes.size === 0) return [];\n return processes.filter((process) => matchesProcessPatterns(process.command, names, prefixes));\n}\n\n/**\n * List running processes matching an agent executable name.\n *\n * Uses `ps -axo` then filters in JS for exact executable basename match.\n * This avoids shell pipelines and string interpolation.\n *\n * Returned ProcessInfo has pid, ppid, command, tty populated.\n * cwd and startTime are NOT populated — call enrichProcesses() to fill them.\n */\nexport function listAgentProcesses(namePattern: string): ProcessInfo[] {\n // Validate pattern contains only safe characters (alphanumeric, dash, underscore)\n if (!namePattern || !VALID_EXECUTABLE_NAME.test(namePattern)) {\n return [];\n }\n\n try {\n const output = execFileSync(\"ps\", [\"-axo\", \"pid=,ppid=,tty=,command=\"], { encoding: \"utf-8\" });\n\n const names = normalizedProcessNames([namePattern]);\n const processes: ProcessInfo[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n\n const tty = match[3];\n const command = match[4];\n\n if (!matchesExecutableName(command, names)) continue;\n\n const ttyShort = tty.startsWith(\"/dev/\") ? tty.slice(5) : tty;\n\n processes.push({\n pid,\n ppid,\n command,\n cwd: \"\",\n tty: ttyShort,\n });\n }\n\n return processes;\n } catch {\n return [];\n }\n}\n\n/** Runs an executable and resolves with its stdout. Injectable for tests. */\nexport type ProcessExec = (file: string, args: readonly string[]) => Promise<string>;\n\nexport interface ProcessSnapshotOptions {\n /**\n * Decides which name-matched processes are worth enriching with cwd and\n * start time. Non-candidates are still returned (pid, ppid, tty, command\n * only) so adapters can walk parent chains. Omitted = enrich everything.\n */\n isCandidate?: (process: ProcessInfo) => boolean;\n /**\n * Basename prefixes matched like `processNamePrefixes`: exact or\n * `<prefix>-*`. Same validation as namePatterns.\n */\n namePrefixes?: readonly string[];\n}\n\nexport type ProcessSnapshotCapture = (\n namePatterns: readonly string[],\n options?: ProcessSnapshotOptions,\n) => Promise<ProcessInfo[]>;\n\n/**\n * One construction site for snapshot options: `namePrefixes` is sent only\n * when non-empty, so adapters without prefixes keep the exact legacy call\n * shape that existing mock assertions pin.\n */\nexport function snapshotOptions(\n namePrefixes: readonly string[],\n isCandidate: (process: ProcessInfo) => boolean,\n): ProcessSnapshotOptions {\n return namePrefixes.length > 0\n ? { namePrefixes: [...namePrefixes], isCandidate }\n : { isCandidate };\n}\n\nconst execFileText: ProcessExec = (file, args) =>\n new Promise((resolve, reject) => {\n execFile(\n file,\n args,\n { encoding: \"utf-8\", maxBuffer: PROCESS_EXEC_MAX_BUFFER },\n (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout);\n },\n );\n });\n\ninterface ParsedProcessList {\n /** Every PID in the listing, used to evict cache entries for exited processes. */\n allPids: Set<number>;\n /** Processes whose executable matches one of the requested names. */\n matched: ProcessInfo[];\n}\n\nfunction parseProcessList(\n output: string,\n namePatterns: ReadonlySet<string>,\n namePrefixes: ReadonlySet<string> = new Set(),\n): ParsedProcessList {\n const allPids = new Set<number>();\n const matched: ProcessInfo[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n allPids.add(pid);\n\n const tty = match[3];\n const command = match[4];\n if (!matchesProcessPatterns(command, namePatterns, namePrefixes)) continue;\n\n matched.push({\n pid,\n ppid,\n command,\n cwd: \"\",\n tty: tty.startsWith(\"/dev/\") ? tty.slice(5) : tty,\n });\n }\n\n return { allPids, matched };\n}\n\n/** Working directories of `pids` (async `lsof`); PIDs it cannot read are omitted. */\nexport function getProcessCwdsAsync(pids: number[]): Promise<Map<number, string>> {\n return batchGetProcessCwdsAsync(execFileText, pids);\n}\n\nasync function batchGetProcessCwdsAsync(\n exec: ProcessExec,\n pids: number[],\n): Promise<Map<number, string>> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = await exec(\"lsof\", [\"-a\", \"-d\", \"cwd\", \"-Fn\", \"-p\", pids.join(\",\")]);\n let currentPid: number | null = null;\n for (const line of output.trim().split(\"\\n\")) {\n if (line.startsWith(\"p\")) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith(\"n\") && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n return result;\n } catch {\n const entries = await Promise.all(\n pids.map(async (pid) => {\n try {\n const output = await exec(\"pwdx\", [String(pid)]);\n const match = output.match(/^\\d+:\\s*(.+)$/);\n return match ? ([pid, match[1].trim()] as const) : null;\n } catch {\n return null;\n }\n }),\n );\n for (const entry of entries) {\n if (entry) result.set(entry[0], entry[1]);\n }\n return result;\n }\n}\n\nasync function batchGetProcessStartTimesAsync(\n exec: ProcessExec,\n pids: number[],\n): Promise<Map<number, Date>> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = await exec(\"ps\", [\"-o\", \"pid=,lstart=\", \"-p\", pids.join(\",\")]);\n for (const rawLine of output.split(\"\\n\")) {\n const match = rawLine.trim().match(/^(\\d+)\\s+(.+)$/);\n if (!match) continue;\n const pid = parseInt(match[1], 10);\n const date = new Date(match[2].trim());\n if (Number.isFinite(pid) && !Number.isNaN(date.getTime())) result.set(pid, date);\n }\n } catch {\n // Return partial/empty data, matching the synchronous helper.\n }\n return result;\n}\n\ninterface CachedStartTime {\n /** ppid + command seen when the start time was read; a mismatch means PID reuse. */\n identity: string;\n startTime: Date;\n}\n\nfunction processIdentity(process: ProcessInfo): string {\n return `${process.ppid ?? \"\"}\\0${process.command}`;\n}\n\n/**\n * Create a snapshot capture function with its own start-time cache.\n *\n * Start times never change for a live process, so each PID is queried with\n * `ps lstart` once and reused on later refreshes. A cache entry is dropped\n * when its PID disappears from the base listing, and ignored when the PID's\n * ppid/command no longer match (PID reuse). cwd is re-read every refresh\n * because a process can change directory.\n */\nexport function createProcessSnapshotCapture(\n deps: { exec?: ProcessExec } = {},\n): ProcessSnapshotCapture {\n const exec = deps.exec ?? execFileText;\n const startTimeCache = new Map<number, CachedStartTime>();\n\n const cachedStartTime = (process: ProcessInfo): Date | undefined => {\n const cached = startTimeCache.get(process.pid);\n return cached?.identity === processIdentity(process) ? cached.startTime : undefined;\n };\n\n return async (namePatterns, options = {}) => {\n const valid = (name: string) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name);\n const names = normalizedProcessNames(namePatterns.filter(valid));\n const prefixes = normalizedProcessNames((options.namePrefixes ?? []).filter(valid));\n if (names.size === 0 && prefixes.size === 0) return [];\n\n try {\n const output = await exec(\"ps\", [\"-axo\", \"pid=,ppid=,tty=,command=\"]);\n const { allPids, matched } = parseProcessList(output, names, prefixes);\n\n for (const pid of startTimeCache.keys()) {\n if (!allPids.has(pid)) startTimeCache.delete(pid);\n }\n\n const { isCandidate } = options;\n const candidates = isCandidate ? matched.filter((process) => isCandidate(process)) : matched;\n const candidatePids = candidates.map((process) => process.pid);\n const uncachedPids = candidates\n .filter((process) => cachedStartTime(process) === undefined)\n .map((process) => process.pid);\n\n const [cwdMap, startTimeMap] = await Promise.all([\n batchGetProcessCwdsAsync(exec, candidatePids),\n batchGetProcessStartTimesAsync(exec, uncachedPids),\n ]);\n\n for (const process of candidates) {\n const startTime = startTimeMap.get(process.pid);\n if (startTime) {\n startTimeCache.set(process.pid, { identity: processIdentity(process), startTime });\n }\n }\n\n const candidateSet = new Set(candidatePids);\n return matched.map((process) =>\n candidateSet.has(process.pid)\n ? {\n ...process,\n cwd: cwdMap.get(process.pid) || \"\",\n startTime: cachedStartTime(process),\n }\n : process,\n );\n } catch {\n return [];\n }\n };\n}\n\n/**\n * Capture and enrich relevant processes without blocking the event loop.\n * One base process listing is shared by every requested executable name;\n * only processes accepted by `options.isCandidate` are enriched.\n */\nexport const captureProcessSnapshot: ProcessSnapshotCapture = createProcessSnapshotCapture();\n\n/**\n * Batch-get current working directories for multiple PIDs.\n *\n * Single `lsof -a -d cwd -Fn -p PID1,PID2,...` call.\n * Returns partial results — if lsof fails for one PID, others still return.\n */\nexport function batchGetProcessCwds(pids: number[]): Map<number, string> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\"lsof\", [\"-a\", \"-d\", \"cwd\", \"-Fn\", \"-p\", pids.join(\",\")], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n\n // lsof output format: p{PID}\\nn{path}\\np{PID}\\nn{path}...\n let currentPid: number | null = null;\n for (const line of output.trim().split(\"\\n\")) {\n if (line.startsWith(\"p\")) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith(\"n\") && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n } catch {\n // Try per-PID fallback with pwdx (Linux)\n for (const pid of pids) {\n try {\n const output = execFileSync(\"pwdx\", [String(pid)], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n const match = output.match(/^\\d+:\\s*(.+)$/);\n if (match) {\n result.set(pid, match[1].trim());\n }\n } catch {\n // Skip this PID\n }\n }\n }\n\n return result;\n}\n\n/**\n * Batch-get process start times for multiple PIDs.\n *\n * Single `ps -o pid=,lstart= -p PID1,PID2,...` call.\n * Uses lstart format which gives full timestamp (e.g., \"Thu Feb 5 16:00:57 2026\").\n * Returns partial results.\n */\nexport function batchGetProcessStartTimes(pids: number[]): Map<number, Date> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\"ps\", [\"-o\", \"pid=,lstart=\", \"-p\", pids.join(\",\")], {\n encoding: \"utf-8\",\n });\n\n for (const rawLine of output.split(\"\\n\")) {\n const line = rawLine.trim();\n if (!line) continue;\n\n // Format: \" PID DAY MON DD HH:MM:SS YYYY\"\n // e.g., \" 78070 Wed Mar 18 23:18:01 2026\"\n const match = line.match(/^\\s*(\\d+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const dateStr = match[2].trim();\n\n if (!Number.isFinite(pid)) continue;\n\n const date = new Date(dateStr);\n if (!Number.isNaN(date.getTime())) {\n result.set(pid, date);\n }\n }\n } catch {\n // Return whatever we have\n }\n\n return result;\n}\n\n/**\n * Enrich ProcessInfo array with cwd and startTime.\n *\n * Calls batchGetProcessCwds and batchGetProcessStartTimes in batched shell calls,\n * then populates each ProcessInfo in-place. Returns partial results —\n * if a PID fails, that process keeps empty cwd / undefined startTime.\n */\nexport function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {\n if (processes.length === 0) return processes;\n\n const pids = processes.map((p) => p.pid);\n const cwdMap = batchGetProcessCwds(pids);\n const startTimeMap = batchGetProcessStartTimes(pids);\n\n for (const proc of processes) {\n proc.cwd = cwdMap.get(proc.pid) || \"\";\n proc.startTime = startTimeMap.get(proc.pid);\n }\n\n return processes;\n}\n\nfunction isSameTerminalProcess(proc: ProcessInfo, matched: ProcessInfo): boolean {\n const sameTty = proc.tty !== \"\" && proc.tty !== \"?\" && proc.tty === matched.tty;\n const sameCwd = proc.cwd !== \"\" && proc.cwd === matched.cwd;\n\n return sameTty && (sameCwd || proc.cwd === \"\" || matched.cwd === \"\");\n}\n\nfunction matchesProcessIdentity(proc: ProcessInfo, matched: ProcessInfo): boolean {\n return proc.pid === matched.pid || isSameTerminalProcess(proc, matched);\n}\n\nexport function findWrapperProcess(\n processes: ProcessInfo[],\n child: ProcessInfo,\n): ProcessInfo | undefined {\n return processes.find(\n (proc) =>\n proc.pid !== child.pid && child.ppid === proc.pid && matchesProcessIdentity(proc, child),\n );\n}\n\n/**\n * Find parent wrapper processes that should not be reported as separate agents.\n *\n * A process is considered a wrapper when it is the parent of another candidate\n * agent process in the same terminal/worktree, or when it points at the same\n * terminal/worktree as an already session-matched process.\n */\nexport function findWrapperProcessPids(\n processes: ProcessInfo[],\n matchedProcesses: ProcessInfo[] = [],\n): Set<number> {\n const wrappers = new Set<number>();\n\n for (const child of processes) {\n const wrapper = findWrapperProcess(processes, child);\n if (wrapper) {\n wrappers.add(wrapper.pid);\n }\n }\n\n for (const proc of processes) {\n if (\n matchedProcesses.some(\n (matched) => proc.pid !== matched.pid && isSameTerminalProcess(proc, matched),\n )\n ) {\n wrappers.add(proc.pid);\n }\n }\n\n return wrappers;\n}\n\n/**\n * Get the TTY device for a specific process\n */\nexport function getProcessTty(pid: number): string {\n try {\n const output = execFileSync(\"ps\", [\"-p\", String(pid), \"-o\", \"tty=\"], { encoding: \"utf-8\" });\n\n const tty = output.trim();\n return tty.startsWith(\"/dev/\") ? tty.slice(5) : tty;\n } catch {\n return \"?\";\n }\n}\n"],"names":["fs","path","execFile","execFileSync","PROCESS_EXEC_MAX_BUFFER","VALID_EXECUTABLE_NAME","ABSOLUTE_PATH","PATH_SEPARATOR","pathBasename","executable","basename","replace","toLowerCase","isFile","filePath","statSync","throwIfNoEntry","executablePath","command","parts","trim","split","first","length","test","prefix","i","executableBasename","normalizeExecutableName","name","lower","endsWith","slice","normalizedProcessNames","namePatterns","Set","filter","Boolean","map","matchesExecutableName","names","tokens","has","mayContinuePath","some","token","matchesExecutablePrefix","prefixes","size","base","startsWith","matchesProcessPatterns","filterByProcessNames","processes","namePrefixes","process","listAgentProcesses","namePattern","output","encoding","line","match","pid","parseInt","ppid","Number","isNaN","tty","ttyShort","push","cwd","snapshotOptions","isCandidate","execFileText","file","args","Promise","resolve","reject","maxBuffer","error","stdout","parseProcessList","allPids","matched","add","getProcessCwdsAsync","pids","batchGetProcessCwdsAsync","exec","result","Map","join","currentPid","set","entries","all","String","entry","batchGetProcessStartTimesAsync","rawLine","date","Date","isFinite","getTime","processIdentity","createProcessSnapshotCapture","deps","startTimeCache","cachedStartTime","cached","get","identity","startTime","undefined","options","valid","keys","delete","candidates","candidatePids","uncachedPids","cwdMap","startTimeMap","candidateSet","captureProcessSnapshot","batchGetProcessCwds","stdio","batchGetProcessStartTimes","dateStr","enrichProcesses","p","proc","isSameTerminalProcess","sameTty","sameCwd","matchesProcessIdentity","findWrapperProcess","child","find","findWrapperProcessPids","matchedProcesses","wrappers","wrapper","getProcessTty"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/AgentManager.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
captureProcessSnapshot,
|
|
26
26
|
filterByProcessNames,
|
|
27
|
+
snapshotOptions,
|
|
27
28
|
type ProcessSnapshotCapture,
|
|
28
29
|
} from "./utils/process.js";
|
|
29
30
|
import {
|
|
@@ -33,6 +34,17 @@ import {
|
|
|
33
34
|
type HerdrAgentPane,
|
|
34
35
|
} from "./runtime/herdr/HerdrAgentDiscovery.js";
|
|
35
36
|
|
|
37
|
+
/** Discovery patterns of one adapter: exact names plus basename prefixes. */
|
|
38
|
+
function adapterPatterns(adapter: AgentAdapter): {
|
|
39
|
+
names: string[];
|
|
40
|
+
prefixes: string[];
|
|
41
|
+
} {
|
|
42
|
+
return {
|
|
43
|
+
names: adapter.processNames ? [...adapter.processNames] : [],
|
|
44
|
+
prefixes: adapter.processNamePrefixes ? [...adapter.processNamePrefixes] : [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
export interface ListAgentsOptions {
|
|
37
49
|
/**
|
|
38
50
|
* Sort key for the returned list. Defaults to `status`, which orders
|
|
@@ -204,12 +216,20 @@ export class AgentManager {
|
|
|
204
216
|
adapters.flatMap((adapter) => (adapter.processNames ? [...adapter.processNames] : [])),
|
|
205
217
|
),
|
|
206
218
|
);
|
|
219
|
+
const namePrefixes = Array.from(
|
|
220
|
+
new Set(
|
|
221
|
+
adapters.flatMap((adapter) =>
|
|
222
|
+
adapter.processNamePrefixes ? [...adapter.processNamePrefixes] : [],
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
207
226
|
let processes: readonly ProcessInfo[] = [];
|
|
208
|
-
if (processNames.length > 0) {
|
|
227
|
+
if (processNames.length > 0 || namePrefixes.length > 0) {
|
|
209
228
|
try {
|
|
210
|
-
processes = await this.captureSnapshot(
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
processes = await this.captureSnapshot(
|
|
230
|
+
processNames,
|
|
231
|
+
snapshotOptions(namePrefixes, (process) => this.isCandidateProcess(adapters, process)),
|
|
232
|
+
);
|
|
213
233
|
} catch {
|
|
214
234
|
processes = [];
|
|
215
235
|
}
|
|
@@ -218,9 +238,12 @@ export class AgentManager {
|
|
|
218
238
|
// Query all adapters in parallel using executable-scoped slices of the shared snapshot.
|
|
219
239
|
const adapterPromises = adapters.map(async (adapter) => {
|
|
220
240
|
try {
|
|
221
|
-
const
|
|
241
|
+
const hasPatterns =
|
|
242
|
+
adapter.processNames !== undefined || adapter.processNamePrefixes !== undefined;
|
|
243
|
+
const { names, prefixes } = adapterPatterns(adapter);
|
|
244
|
+
const agents = hasPatterns
|
|
222
245
|
? await adapter.detectAgents({
|
|
223
|
-
processes: filterByProcessNames(processes,
|
|
246
|
+
processes: filterByProcessNames(processes, names, prefixes),
|
|
224
247
|
})
|
|
225
248
|
: await adapter.detectAgents();
|
|
226
249
|
return { type: adapter.type, agents, error: null };
|
|
@@ -284,8 +307,11 @@ export class AgentManager {
|
|
|
284
307
|
*/
|
|
285
308
|
private isCandidateProcess(adapters: readonly AgentAdapter[], process: ProcessInfo): boolean {
|
|
286
309
|
return adapters.some((adapter) => {
|
|
287
|
-
if (
|
|
288
|
-
|
|
310
|
+
if (adapter.processNames === undefined && adapter.processNamePrefixes === undefined) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
const { names, prefixes } = adapterPatterns(adapter);
|
|
314
|
+
if (filterByProcessNames([process], names, prefixes).length === 0) return false;
|
|
289
315
|
try {
|
|
290
316
|
return adapter.canHandle(process);
|
|
291
317
|
} catch {
|
|
@@ -23,16 +23,14 @@ import type { FixtureBundle } from "../fixtures/bundle.js";
|
|
|
23
23
|
import { captureLive, FIXTURES_ROOT } from "../fixtures/capture.js";
|
|
24
24
|
|
|
25
25
|
const bundlePaths = fs.existsSync(FIXTURES_ROOT)
|
|
26
|
-
? fs
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
: [],
|
|
35
|
-
)
|
|
26
|
+
? fs.readdirSync(FIXTURES_ROOT, { withFileTypes: true }).flatMap((d) =>
|
|
27
|
+
d.isDirectory()
|
|
28
|
+
? fs
|
|
29
|
+
.readdirSync(path.join(FIXTURES_ROOT, d.name))
|
|
30
|
+
.filter((f) => f.endsWith(".json"))
|
|
31
|
+
.map((f) => path.join(FIXTURES_ROOT, d.name, f))
|
|
32
|
+
: [],
|
|
33
|
+
)
|
|
36
34
|
: [];
|
|
37
35
|
|
|
38
36
|
/** Adapters whose detect path consults AgentRegistry. */
|
|
@@ -51,7 +49,11 @@ function adapterFor(type: string, registry?: AgentRegistry) {
|
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
/** Seed an isolated registry at `<home>/.ai-devkit/agents.json`. */
|
|
54
|
-
function seedRegistry(
|
|
52
|
+
function seedRegistry(
|
|
53
|
+
bundle: FixtureBundle,
|
|
54
|
+
home: string,
|
|
55
|
+
nowIso: string,
|
|
56
|
+
): AgentRegistry | undefined {
|
|
55
57
|
if (!REGISTRY_AWARE.has(bundle.adapter)) return undefined;
|
|
56
58
|
const registry = new AgentRegistry(path.join(home, ".ai-devkit", "agents.json"));
|
|
57
59
|
const entries = (bundle.registry ?? []).map(
|
|
@@ -125,9 +127,7 @@ function materialize(bundle: FixtureBundle) {
|
|
|
125
127
|
// Pids whose startTime resolves to the materialization instant — Codex
|
|
126
128
|
// derives session day-dirs from start times, so only these paths contain
|
|
127
129
|
// the replay-day key and normalize to $TODAY.
|
|
128
|
-
const nowPids = new Set(
|
|
129
|
-
bundle.processes.filter((p) => p.startTime === "$NOW").map((p) => p.pid),
|
|
130
|
-
);
|
|
130
|
+
const nowPids = new Set(bundle.processes.filter((p) => p.startTime === "$NOW").map((p) => p.pid));
|
|
131
131
|
return {
|
|
132
132
|
home,
|
|
133
133
|
processes,
|
|
@@ -177,9 +177,7 @@ describe("fixture replay (TS parity oracle)", () => {
|
|
|
177
177
|
// process startTime was $NOW — static-start agents keep concrete
|
|
178
178
|
// dates so live bundles stay replayable on any day.
|
|
179
179
|
const actual = JSON.parse(JSON.stringify(normalizeAgents(agents, home))).map((a) =>
|
|
180
|
-
nowPids.has(a.pid)
|
|
181
|
-
? JSON.parse(JSON.stringify(a).split(todayKey).join("$TODAY"))
|
|
182
|
-
: a,
|
|
180
|
+
nowPids.has(a.pid) ? JSON.parse(JSON.stringify(a).split(todayKey).join("$TODAY")) : a,
|
|
183
181
|
);
|
|
184
182
|
expect(actual).toEqual(expected);
|
|
185
183
|
} finally {
|
|
@@ -239,9 +239,7 @@ describe("DevinAdapter", () => {
|
|
|
239
239
|
{ id: "newer", directory: "/repo", timeCreated: now - 200, lastActivityAt: now - 60 },
|
|
240
240
|
{ id: "hidden-sess", directory: "/repo", timeCreated: now - 10, hidden: 1 },
|
|
241
241
|
],
|
|
242
|
-
nodes: [
|
|
243
|
-
{ sessionId: "newer", nodeId: 1, role: "user", content: "hi", createdAt: now - 60 },
|
|
244
|
-
],
|
|
242
|
+
nodes: [{ sessionId: "newer", nodeId: 1, role: "user", content: "hi", createdAt: now - 60 }],
|
|
245
243
|
});
|
|
246
244
|
|
|
247
245
|
const agents = await adapter.detectAgents({
|
|
@@ -59,9 +59,7 @@ describe("DevinSessionParser", () => {
|
|
|
59
59
|
});
|
|
60
60
|
const db = openDb();
|
|
61
61
|
try {
|
|
62
|
-
expect(parser.getConversation(db, "s")).toEqual([
|
|
63
|
-
{ role: "assistant", content: "final" },
|
|
64
|
-
]);
|
|
62
|
+
expect(parser.getConversation(db, "s")).toEqual([{ role: "assistant", content: "final" }]);
|
|
65
63
|
} finally {
|
|
66
64
|
db.close();
|
|
67
65
|
}
|
|
@@ -99,9 +97,7 @@ describe("DevinSessionParser", () => {
|
|
|
99
97
|
});
|
|
100
98
|
const db = openDb();
|
|
101
99
|
try {
|
|
102
|
-
expect(parser.getConversation(db, "s")).toEqual([
|
|
103
|
-
{ role: "assistant", content: "working" },
|
|
104
|
-
]);
|
|
100
|
+
expect(parser.getConversation(db, "s")).toEqual([{ role: "assistant", content: "working" }]);
|
|
105
101
|
expect(parser.getConversation(db, "s", { verbose: true })).toEqual([
|
|
106
102
|
{ role: "assistant", content: "[thinking] hmm" },
|
|
107
103
|
{ role: "assistant", content: "working" },
|
|
@@ -145,9 +141,7 @@ describe("DevinSessionParser", () => {
|
|
|
145
141
|
});
|
|
146
142
|
const db = openDb();
|
|
147
143
|
try {
|
|
148
|
-
expect(parser.getConversation(db, "s")).toEqual([
|
|
149
|
-
{ role: "assistant", content: "ok" },
|
|
150
|
-
]);
|
|
144
|
+
expect(parser.getConversation(db, "s")).toEqual([{ role: "assistant", content: "ok" }]);
|
|
151
145
|
expect(parser.getConversation(db, "missing")).toEqual([]);
|
|
152
146
|
} finally {
|
|
153
147
|
db.close();
|