peaks-loop-internal-runtime 0.0.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.
Files changed (58) hide show
  1. package/dist/auto-compact-adapter.d.ts +18 -0
  2. package/dist/auto-compact-adapter.d.ts.map +1 -0
  3. package/dist/auto-compact-adapter.js +21 -0
  4. package/dist/dispatch.d.ts +18 -0
  5. package/dist/dispatch.d.ts.map +1 -0
  6. package/dist/dispatch.js +40 -0
  7. package/dist/guards/resource-budget.d.ts +23 -0
  8. package/dist/guards/resource-budget.d.ts.map +1 -0
  9. package/dist/guards/resource-budget.js +18 -0
  10. package/dist/index.d.ts +16 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +12 -0
  13. package/dist/lifecycle.d.ts +9 -0
  14. package/dist/lifecycle.d.ts.map +1 -0
  15. package/dist/lifecycle.js +39 -0
  16. package/dist/process-supervisor.d.ts +19 -0
  17. package/dist/process-supervisor.d.ts.map +1 -0
  18. package/dist/process-supervisor.js +30 -0
  19. package/dist/prompt-builder.d.ts +13 -0
  20. package/dist/prompt-builder.d.ts.map +1 -0
  21. package/dist/prompt-builder.js +27 -0
  22. package/dist/status-protocol.d.ts +19 -0
  23. package/dist/status-protocol.d.ts.map +1 -0
  24. package/dist/status-protocol.js +18 -0
  25. package/dist/types.d.ts +12 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/dist/types.js +1 -0
  28. package/dist/vendor/adapter.d.ts +12 -0
  29. package/dist/vendor/adapter.d.ts.map +1 -0
  30. package/dist/vendor/adapter.js +1 -0
  31. package/dist/vendor/claude-adapter.d.ts +13 -0
  32. package/dist/vendor/claude-adapter.d.ts.map +1 -0
  33. package/dist/vendor/claude-adapter.js +41 -0
  34. package/dist/vendor/codex-adapter.d.ts +11 -0
  35. package/dist/vendor/codex-adapter.d.ts.map +1 -0
  36. package/dist/vendor/codex-adapter.js +31 -0
  37. package/dist/vendor/copilot-adapter.d.ts +11 -0
  38. package/dist/vendor/copilot-adapter.d.ts.map +1 -0
  39. package/dist/vendor/copilot-adapter.js +29 -0
  40. package/dist/vendor/registry.d.ts +10 -0
  41. package/dist/vendor/registry.d.ts.map +1 -0
  42. package/dist/vendor/registry.js +14 -0
  43. package/package.json +25 -0
  44. package/src/auto-compact-adapter.ts +30 -0
  45. package/src/dispatch.ts +57 -0
  46. package/src/guards/resource-budget.ts +20 -0
  47. package/src/index.ts +24 -0
  48. package/src/lifecycle.ts +40 -0
  49. package/src/process-supervisor.ts +42 -0
  50. package/src/prompt-builder.ts +36 -0
  51. package/src/status-protocol.ts +23 -0
  52. package/src/types.ts +11 -0
  53. package/src/vendor/adapter.ts +10 -0
  54. package/src/vendor/claude-adapter.ts +41 -0
  55. package/src/vendor/codex-adapter.ts +25 -0
  56. package/src/vendor/copilot-adapter.ts +23 -0
  57. package/src/vendor/registry.ts +16 -0
  58. package/tsconfig.json +9 -0
@@ -0,0 +1,18 @@
1
+ import type { AutoCompactEvent } from './status-protocol.js';
2
+ export interface MarkerOpts {
3
+ rid: string;
4
+ sid: string;
5
+ vendorWindow: number;
6
+ }
7
+ export interface ScratchPayload {
8
+ seq: number;
9
+ at: number;
10
+ summary: string;
11
+ decisionsKept?: string[];
12
+ discardedOptions?: string[];
13
+ }
14
+ export declare class AutoCompactAdapter {
15
+ marker(opts: MarkerOpts): string;
16
+ parseScratchFile(p: ScratchPayload): Partial<AutoCompactEvent>;
17
+ }
18
+ //# sourceMappingURL=auto-compact-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-compact-adapter.d.ts","sourceRoot":"","sources":["../src/auto-compact-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,MAAM,WAAW,UAAU;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;CAAE;AAC/E,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CACvD;AAED,qBAAa,kBAAkB;IAC7B,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM;IAiBhC,gBAAgB,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAG/D"}
@@ -0,0 +1,21 @@
1
+ export class AutoCompactAdapter {
2
+ marker(opts) {
3
+ return [
4
+ `<peaks-auto-compact threshold="0.85|0.95" vendor-window="${opts.vendorWindow}">`,
5
+ `协议:`,
6
+ `- 你(子进程 LLM)估算自己已用上下文 ≥ 85% vendor window`,
7
+ ` → 主动 compact 自己的会话:把对话历史摘要写到`,
8
+ ` .peaks/_runtime/${opts.sid}/detached/${opts.rid}/compact/<n>.json`,
9
+ ` → 把摘要 + 当前任务状态拼回 prompt 头部`,
10
+ ` → 调用 peaks runtime write-compact-event CLI 记录事件`,
11
+ `- 当 ≥ 95% → 同步 compact + 立刻通知 peaks 主进程`,
12
+ ` (写 status.json note: 'compact-emergency')`,
13
+ `- 不要等 peaks 主进程来催;子进程 LLM 自己监控自己的 context`,
14
+ `- 不限费用(用户授权)—— compact 本身消耗的 token 随它去`,
15
+ `</peaks-auto-compact>`,
16
+ ].join('\n');
17
+ }
18
+ parseScratchFile(p) {
19
+ return { at: p.at, threshold: '0.85', tokensBefore: 0, tokensAfter: 0 };
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ export interface DispatchInput {
2
+ sid: string;
3
+ rid: string;
4
+ role: 'rd' | 'qa' | 'ui' | 'txt' | 'general-purpose';
5
+ vendor: 'claude' | 'codex' | 'copilot';
6
+ userTask: string;
7
+ files: string[];
8
+ refs: string[];
9
+ runtimeDir: string;
10
+ subAgentsDir: string;
11
+ verbatimBlocks?: string[];
12
+ }
13
+ export interface DispatchResult {
14
+ pid: number;
15
+ dispatchRecordPath: string;
16
+ }
17
+ export declare function dispatchDetached(i: DispatchInput): Promise<DispatchResult>;
18
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,GAAC,IAAI,GAAC,IAAI,GAAC,KAAK,GAAC,iBAAiB,CAAC;IACvE,MAAM,EAAE,QAAQ,GAAC,OAAO,GAAC,SAAS,CAAC;IACnC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AACD,MAAM,WAAW,cAAc;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAC;CAAE;AAE5E,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAkChF"}
@@ -0,0 +1,40 @@
1
+ import { writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { ProcessSupervisor } from './process-supervisor.js';
4
+ import { LifecycleOwner } from './lifecycle.js';
5
+ import { VendorAdapterRegistry } from './vendor/registry.js';
6
+ import { ClaudeAdapter } from './vendor/claude-adapter.js';
7
+ import { CodexAdapter } from './vendor/codex-adapter.js';
8
+ import { CopilotAdapter } from './vendor/copilot-adapter.js';
9
+ import { PromptBuilder } from './prompt-builder.js';
10
+ import { AutoCompactAdapter } from './auto-compact-adapter.js';
11
+ export async function dispatchDetached(i) {
12
+ const registry = new VendorAdapterRegistry([new ClaudeAdapter(), new CodexAdapter(), new CopilotAdapter()]);
13
+ const adapter = registry.get(i.vendor);
14
+ if (!adapter)
15
+ throw new Error(`vendor adapter not registered: ${i.vendor}`);
16
+ const pb = new PromptBuilder();
17
+ const ac = new AutoCompactAdapter();
18
+ const marker = ac.marker({ rid: i.rid, sid: i.sid, vendorWindow: adapter.maxPromptBytes / 40 /* rough */ });
19
+ const prompt = pb.assemble({
20
+ rid: i.rid, role: i.role, vendor: i.vendor,
21
+ files: i.files, refs: i.refs, userTask: i.userTask,
22
+ verbatimBlocks: [marker, ...(i.verbatimBlocks ?? [])],
23
+ });
24
+ const args = adapter.headlessArgs(prompt, { autoCompactMarker: marker });
25
+ const dir = join(i.runtimeDir, i.rid);
26
+ mkdirSync(dir, { recursive: true });
27
+ writeFileSync(join(dir, 'owner-session'), i.sid);
28
+ const sup = new ProcessSupervisor({ runtimeDir: i.runtimeDir });
29
+ const lo = new LifecycleOwner(i.runtimeDir);
30
+ const handle = await sup.spawn(adapter.binary, args, { detach: true, rid: i.rid });
31
+ lo.register(handle.pid, i.rid, i.sid);
32
+ // Write dispatch record (placeholder — final shape per Task 8 schema)
33
+ const recPath = join(i.subAgentsDir, `dispatch-${i.rid}-${Date.now()}.json`);
34
+ mkdirSync(i.subAgentsDir, { recursive: true });
35
+ writeFileSync(recPath, JSON.stringify({
36
+ rid: i.rid, mode: 'detached', vendor: i.vendor,
37
+ status: 'running', heartbeats: [], at: Date.now(),
38
+ }, null, 2));
39
+ return { pid: handle.pid, dispatchRecordPath: recPath };
40
+ }
@@ -0,0 +1,23 @@
1
+ export interface Sample {
2
+ rssMb: number;
3
+ cpuPct: number;
4
+ }
5
+ export interface EnforceInput {
6
+ active: number;
7
+ }
8
+ export interface EnforceOpts {
9
+ maxConcurrent: number;
10
+ }
11
+ export declare class ResourceBudgetGuard {
12
+ private readonly cfg;
13
+ constructor(cfg: {
14
+ maxRssMb: number;
15
+ maxCpuPct: number;
16
+ });
17
+ sample(): Sample;
18
+ enforce(input: EnforceInput, opts: EnforceOpts): {
19
+ throttle: boolean;
20
+ kill?: string[];
21
+ };
22
+ }
23
+ //# sourceMappingURL=resource-budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-budget.d.ts","sourceRoot":"","sources":["../../src/guards/resource-budget.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;CAAE;AAC1D,MAAM,WAAW,YAAY;IAAG,MAAM,EAAE,MAAM,CAAC;CAAE;AACjD,MAAM,WAAW,WAAW;IAAG,aAAa,EAAE,MAAM,CAAC;CAAE;AAEvD,qBAAa,mBAAmB;IAClB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAEzE,MAAM,IAAI,MAAM;IAQhB,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE;CAIxF"}
@@ -0,0 +1,18 @@
1
+ export class ResourceBudgetGuard {
2
+ cfg;
3
+ constructor(cfg) {
4
+ this.cfg = cfg;
5
+ }
6
+ sample() {
7
+ const m = process.memoryUsage();
8
+ const c = process.cpuUsage();
9
+ const rssMb = Math.round(m.rss / 1024 / 1024);
10
+ const cpuPct = Math.round(((c.user + c.system) / 1_000_000) * 100) / 100;
11
+ return { rssMb, cpuPct };
12
+ }
13
+ enforce(input, opts) {
14
+ if (input.active > opts.maxConcurrent)
15
+ return { throttle: true };
16
+ return { throttle: false };
17
+ }
18
+ }
@@ -0,0 +1,16 @@
1
+ export declare const RUNTIME_VERSION = "4.0.19";
2
+ export { ClaudeAdapter } from './vendor/claude-adapter.js';
3
+ export { CodexAdapter } from './vendor/codex-adapter.js';
4
+ export { CopilotAdapter } from './vendor/copilot-adapter.js';
5
+ export { VendorAdapterRegistry, defaultRegistry } from './vendor/registry.js';
6
+ export type { VendorAdapter } from './vendor/adapter.js';
7
+ export { ProcessSupervisor } from './process-supervisor.js';
8
+ export { LifecycleOwner } from './lifecycle.js';
9
+ export { PromptBuilder } from './prompt-builder.js';
10
+ export { StatusProtocol } from './status-protocol.js';
11
+ export type { HeartbeatEntry, AutoCompactEvent } from './status-protocol.js';
12
+ export { AutoCompactAdapter } from './auto-compact-adapter.js';
13
+ export { ResourceBudgetGuard } from './guards/resource-budget.js';
14
+ export { dispatchDetached } from './dispatch.js';
15
+ export type { DispatchInput, DispatchResult } from './dispatch.js';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,WAAW,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC9E,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export const RUNTIME_VERSION = '4.0.19';
2
+ export { ClaudeAdapter } from './vendor/claude-adapter.js';
3
+ export { CodexAdapter } from './vendor/codex-adapter.js';
4
+ export { CopilotAdapter } from './vendor/copilot-adapter.js';
5
+ export { VendorAdapterRegistry, defaultRegistry } from './vendor/registry.js';
6
+ export { ProcessSupervisor } from './process-supervisor.js';
7
+ export { LifecycleOwner } from './lifecycle.js';
8
+ export { PromptBuilder } from './prompt-builder.js';
9
+ export { StatusProtocol } from './status-protocol.js';
10
+ export { AutoCompactAdapter } from './auto-compact-adapter.js';
11
+ export { ResourceBudgetGuard } from './guards/resource-budget.js';
12
+ export { dispatchDetached } from './dispatch.js';
@@ -0,0 +1,9 @@
1
+ export declare class LifecycleOwner {
2
+ private readonly runtimeDir;
3
+ private active;
4
+ constructor(runtimeDir: string);
5
+ register(pid: number, rid: string, ownerSession: string): void;
6
+ markExit(rid: string, code: number, signal?: string): Promise<void>;
7
+ reap(currentSessionId: string): Promise<string[]>;
8
+ }
9
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAKA,qBAAa,cAAc;IAGb,OAAO,CAAC,QAAQ,CAAC,UAAU;IAFvC,OAAO,CAAC,MAAM,CAAmC;gBAEpB,UAAU,EAAE,MAAM;IAE/C,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAIxD,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAOxD"}
@@ -0,0 +1,39 @@
1
+ import { existsSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export class LifecycleOwner {
4
+ runtimeDir;
5
+ active = new Map();
6
+ constructor(runtimeDir) {
7
+ this.runtimeDir = runtimeDir;
8
+ }
9
+ register(pid, rid, ownerSession) {
10
+ this.active.set(rid, { pid, rid, ownerSession });
11
+ }
12
+ async markExit(rid, code, signal) {
13
+ const dir = join(this.runtimeDir, rid);
14
+ if (!existsSync(dir))
15
+ return;
16
+ const exit = { code, signal, at: Date.now() };
17
+ writeFileSync(join(dir, 'exit.json'), JSON.stringify(exit));
18
+ // Archive
19
+ if (existsSync(join(dir, 'log.txt')))
20
+ renameSync(join(dir, 'log.txt'), join(dir, 'log-archive.txt'));
21
+ if (existsSync(join(dir, 'status.json')))
22
+ renameSync(join(dir, 'status.json'), join(dir, 'status-final.json'));
23
+ // Delete active markers
24
+ for (const f of ['pid', 'owner-session']) {
25
+ const p = join(dir, f);
26
+ if (existsSync(p))
27
+ rmSync(p);
28
+ }
29
+ this.active.delete(rid);
30
+ }
31
+ async reap(currentSessionId) {
32
+ const orphans = [];
33
+ for (const rec of this.active.values()) {
34
+ if (rec.ownerSession !== currentSessionId)
35
+ orphans.push(rec.rid);
36
+ }
37
+ return orphans; // user decides via `peaks sub-agent cleanup --orphan`
38
+ }
39
+ }
@@ -0,0 +1,19 @@
1
+ import { ChildProcess } from 'node:child_process';
2
+ export interface SpawnOpts {
3
+ detach: boolean;
4
+ rid: string;
5
+ stdio?: 'pipe' | 'ignore';
6
+ }
7
+ export interface SpawnHandle {
8
+ pid: number;
9
+ child: ChildProcess;
10
+ kill(signal?: NodeJS.Signals): void;
11
+ }
12
+ export declare class ProcessSupervisor {
13
+ private readonly cfg;
14
+ constructor(cfg: {
15
+ runtimeDir: string;
16
+ });
17
+ spawn(binary: string, args: string[], opts: SpawnOpts): Promise<SpawnHandle>;
18
+ }
19
+ //# sourceMappingURL=process-supervisor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-supervisor.d.ts","sourceRoot":"","sources":["../src/process-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAItE,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC3B;AACD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,YAAY,CAAC;IACpB,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;CACrC;AAED,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE;IAElD,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;CAuBnF"}
@@ -0,0 +1,30 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { writeFileSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export class ProcessSupervisor {
5
+ cfg;
6
+ constructor(cfg) {
7
+ this.cfg = cfg;
8
+ }
9
+ async spawn(binary, args, opts) {
10
+ const isWin = process.platform === 'win32';
11
+ const spawnOpts = {
12
+ detached: opts.detach,
13
+ stdio: opts.stdio ?? 'pipe',
14
+ };
15
+ if (isWin) {
16
+ spawnOpts.windowsHide = true;
17
+ // CREATE_NEW_PROCESS_GROUP = 0x00000200, DETACHED_PROCESS = 0x00000008
18
+ spawnOpts.detached = true;
19
+ }
20
+ const child = nodeSpawn(binary, args, spawnOpts);
21
+ const dir = join(this.cfg.runtimeDir, opts.rid);
22
+ mkdirSync(dir, { recursive: true });
23
+ writeFileSync(join(dir, 'pid'), String(child.pid ?? ''));
24
+ return {
25
+ pid: child.pid ?? -1,
26
+ child,
27
+ kill: (signal = 'SIGTERM') => child.kill(signal),
28
+ };
29
+ }
30
+ }
@@ -0,0 +1,13 @@
1
+ export interface AssembleInput {
2
+ rid: string;
3
+ role: 'rd' | 'qa' | 'ui' | 'txt' | 'general-purpose';
4
+ vendor: string;
5
+ files: string[];
6
+ refs: string[];
7
+ userTask: string;
8
+ verbatimBlocks?: string[];
9
+ }
10
+ export declare class PromptBuilder {
11
+ assemble(i: AssembleInput): string;
12
+ }
13
+ //# sourceMappingURL=prompt-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-builder.d.ts","sourceRoot":"","sources":["../src/prompt-builder.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,iBAAiB,CAAC;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,qBAAa,aAAa;IACxB,QAAQ,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM;CAsBnC"}
@@ -0,0 +1,27 @@
1
+ const FORBIDDEN = '@@@ORCHESTRATOR_SESSION_HISTORY_BOUNDARY@@@';
2
+ export class PromptBuilder {
3
+ assemble(i) {
4
+ if (i.userTask.includes(FORBIDDEN))
5
+ throw new Error('forbidden marker in user task');
6
+ const parts = [
7
+ `rid: ${i.rid}`,
8
+ `role: ${i.role}`,
9
+ `vendor: ${i.vendor}`,
10
+ ``,
11
+ `## Task`,
12
+ i.userTask,
13
+ ``,
14
+ `## Files (read-only)`,
15
+ ...i.files.map(f => `- ${f}`),
16
+ ``,
17
+ `## References`,
18
+ ...i.refs.map(r => `- ${r}`),
19
+ ``,
20
+ ...(i.verbatimBlocks ?? []),
21
+ ];
22
+ const out = parts.join('\n');
23
+ if (out.includes(FORBIDDEN))
24
+ throw new Error('forbidden marker leak');
25
+ return out;
26
+ }
27
+ }
@@ -0,0 +1,19 @@
1
+ import type { ChildStatus } from './types.js';
2
+ export interface HeartbeatEntry {
3
+ progress: number;
4
+ note: string;
5
+ ts: number;
6
+ }
7
+ export interface AutoCompactEvent {
8
+ at: number;
9
+ threshold: '0.85' | '0.95';
10
+ tokensBefore: number;
11
+ tokensAfter: number;
12
+ scratchFile?: string;
13
+ }
14
+ export declare class StatusProtocol {
15
+ merge(rec: any, s: ChildStatus): any;
16
+ isStale(lastBeatAt: number, thresholdSec?: number): boolean;
17
+ appendCompactEvent(rec: any, ev: AutoCompactEvent): any;
18
+ }
19
+ //# sourceMappingURL=status-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-protocol.d.ts","sourceRoot":"","sources":["../src/status-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,cAAc;IAAG,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;CAAE;AAC/E,MAAM,WAAW,gBAAgB;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAAE;AAE9I,qBAAa,cAAc;IACzB,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,WAAW,GAAG,GAAG;IAUpC,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,SAAM,GAAG,OAAO;IAGxD,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,gBAAgB,GAAG,GAAG;CAGxD"}
@@ -0,0 +1,18 @@
1
+ export class StatusProtocol {
2
+ merge(rec, s) {
3
+ const next = { ...rec };
4
+ next.heartbeats = [...(rec.heartbeats ?? []), { progress: s.progress, note: s.note, ts: s.ts }];
5
+ if (next.heartbeats.length > 100) {
6
+ next.heartbeats = next.heartbeats.slice(-100);
7
+ next.heartbeatsTruncated = true;
8
+ }
9
+ next.status = s.state === 'running' ? 'running' : s.state;
10
+ return next;
11
+ }
12
+ isStale(lastBeatAt, thresholdSec = 300) {
13
+ return Date.now() - lastBeatAt > thresholdSec * 1000;
14
+ }
15
+ appendCompactEvent(rec, ev) {
16
+ return { ...rec, autoCompactEvents: [...(rec.autoCompactEvents ?? []), ev] };
17
+ }
18
+ }
@@ -0,0 +1,12 @@
1
+ export type DetachedMode = 'in-process' | 'detached';
2
+ export type VendorId = 'claude' | 'codex' | 'copilot';
3
+ export interface ChildStatus {
4
+ rid: string;
5
+ vendor: VendorId;
6
+ progress: number;
7
+ state: 'running' | 'stale' | 'crashed' | 'oom-killed' | 'done' | 'spawn-failed';
8
+ note: string;
9
+ ts: number;
10
+ etaSec?: number;
11
+ }
12
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,UAAU,CAAC;AACrD,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AACtD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,CAAC;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { ChildStatus } from '../types.js';
2
+ export interface VendorAdapter {
3
+ readonly id: 'claude' | 'codex' | 'copilot';
4
+ readonly binary: string;
5
+ readonly maxPromptBytes: number;
6
+ headlessArgs(prompt: string, opts?: {
7
+ autoCompactMarker?: string;
8
+ }): string[];
9
+ parseStatusLine(stdout: string): ChildStatus | null;
10
+ detectInstalled(): Promise<boolean>;
11
+ }
12
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/vendor/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,EAAE,CAAC;IAC9E,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IACpD,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { VendorAdapter } from './adapter.js';
2
+ import type { ChildStatus } from '../types.js';
3
+ export declare class ClaudeAdapter implements VendorAdapter {
4
+ readonly id: "claude";
5
+ readonly binary = "claude";
6
+ readonly maxPromptBytes: number;
7
+ headlessArgs(prompt: string, opts?: {
8
+ autoCompactMarker?: string;
9
+ }): string[];
10
+ parseStatusLine(stdout: string): ChildStatus | null;
11
+ detectInstalled(): Promise<boolean>;
12
+ }
13
+ //# sourceMappingURL=claude-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-adapter.d.ts","sourceRoot":"","sources":["../../src/vendor/claude-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,qBAAa,aAAc,YAAW,aAAa;IACjD,QAAQ,CAAC,EAAE,EAAG,QAAQ,CAAU;IAChC,QAAQ,CAAC,MAAM,YAAY;IAC3B,QAAQ,CAAC,cAAc,SAAY;IAEnC,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,EAAE;IAO7E,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAe7C,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;CAM1C"}
@@ -0,0 +1,41 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const pExecFile = promisify(execFile);
4
+ export class ClaudeAdapter {
5
+ id = 'claude';
6
+ binary = 'claude';
7
+ maxPromptBytes = 8 * 1024;
8
+ headlessArgs(prompt, opts) {
9
+ const injected = opts?.autoCompactMarker
10
+ ? `${opts.autoCompactMarker}\n\n${prompt}`
11
+ : prompt;
12
+ return ['-p', injected, '--output-format', 'json', '--include-partial-messages'];
13
+ }
14
+ parseStatusLine(stdout) {
15
+ try {
16
+ const obj = JSON.parse(stdout);
17
+ if (typeof obj.progress !== 'number')
18
+ return null;
19
+ return {
20
+ rid: String(obj.rid ?? ''),
21
+ vendor: 'claude',
22
+ progress: obj.progress,
23
+ state: obj.state,
24
+ note: String(obj.note ?? ''),
25
+ ts: Number(obj.ts ?? Date.now()),
26
+ };
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ async detectInstalled() {
33
+ try {
34
+ const { stdout } = await pExecFile(this.binary, ['--version'], { timeout: 3000 });
35
+ return stdout.length > 0;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,11 @@
1
+ import type { VendorAdapter } from './adapter.js';
2
+ import type { ChildStatus } from '../types.js';
3
+ export declare class CodexAdapter implements VendorAdapter {
4
+ readonly id: "codex";
5
+ readonly binary = "codex";
6
+ readonly maxPromptBytes: number;
7
+ headlessArgs(prompt: string): string[];
8
+ parseStatusLine(stdout: string): ChildStatus | null;
9
+ detectInstalled(): Promise<boolean>;
10
+ }
11
+ //# sourceMappingURL=codex-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-adapter.d.ts","sourceRoot":"","sources":["../../src/vendor/codex-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,qBAAa,YAAa,YAAW,aAAa;IAChD,QAAQ,CAAC,EAAE,EAAG,OAAO,CAAU;IAC/B,QAAQ,CAAC,MAAM,WAAW;IAC1B,QAAQ,CAAC,cAAc,SAAY;IACnC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE;IAGtC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAO7C,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;CAI1C"}
@@ -0,0 +1,31 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const pExecFile = promisify(execFile);
4
+ export class CodexAdapter {
5
+ id = 'codex';
6
+ binary = 'codex';
7
+ maxPromptBytes = 5 * 1024;
8
+ headlessArgs(prompt) {
9
+ return ['exec', '--json', prompt];
10
+ }
11
+ parseStatusLine(stdout) {
12
+ try {
13
+ const o = JSON.parse(stdout);
14
+ if (typeof o.progress !== 'number')
15
+ return null;
16
+ return { rid: String(o.rid ?? ''), vendor: 'codex', progress: o.progress, state: o.state, note: String(o.note ?? ''), ts: Number(o.ts ?? Date.now()) };
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ async detectInstalled() {
23
+ try {
24
+ const { stdout } = await pExecFile(this.binary, ['--version'], { timeout: 3000 });
25
+ return stdout.length > 0;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,11 @@
1
+ import type { VendorAdapter } from './adapter.js';
2
+ import type { ChildStatus } from '../types.js';
3
+ export declare class CopilotAdapter implements VendorAdapter {
4
+ readonly id: "copilot";
5
+ readonly binary = "copilot";
6
+ readonly maxPromptBytes: number;
7
+ headlessArgs(prompt: string): string[];
8
+ parseStatusLine(stdout: string): ChildStatus | null;
9
+ detectInstalled(): Promise<boolean>;
10
+ }
11
+ //# sourceMappingURL=copilot-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copilot-adapter.d.ts","sourceRoot":"","sources":["../../src/vendor/copilot-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,qBAAa,cAAe,YAAW,aAAa;IAClD,QAAQ,CAAC,EAAE,EAAG,SAAS,CAAU;IACjC,QAAQ,CAAC,MAAM,aAAa;IAC5B,QAAQ,CAAC,cAAc,SAAY;IACnC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE;IACtC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAO7C,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;CAI1C"}
@@ -0,0 +1,29 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const pExecFile = promisify(execFile);
4
+ export class CopilotAdapter {
5
+ id = 'copilot';
6
+ binary = 'copilot';
7
+ maxPromptBytes = 6 * 1024;
8
+ headlessArgs(prompt) { return ['-p', prompt, '--output-format', 'json']; }
9
+ parseStatusLine(stdout) {
10
+ try {
11
+ const o = JSON.parse(stdout);
12
+ if (typeof o.progress !== 'number')
13
+ return null;
14
+ return { rid: String(o.rid ?? ''), vendor: 'copilot', progress: o.progress, state: o.state, note: String(o.note ?? ''), ts: Number(o.ts ?? Date.now()) };
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ async detectInstalled() {
21
+ try {
22
+ const { stdout } = await pExecFile(this.binary, ['--version'], { timeout: 3000 });
23
+ return stdout.length > 0;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,10 @@
1
+ import type { VendorAdapter } from './adapter.js';
2
+ export declare class VendorAdapterRegistry {
3
+ private map;
4
+ constructor(initial?: VendorAdapter[]);
5
+ register(a: VendorAdapter): void;
6
+ get(id: string): VendorAdapter | undefined;
7
+ list(): VendorAdapter[];
8
+ }
9
+ export declare const defaultRegistry: () => VendorAdapterRegistry;
10
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/vendor/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAKlD,qBAAa,qBAAqB;IAChC,OAAO,CAAC,GAAG,CAAoC;gBACnC,OAAO,GAAE,aAAa,EAAO;IAGzC,QAAQ,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI;IAChC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAC1C,IAAI,IAAI,aAAa,EAAE;CACxB;AAED,eAAO,MAAM,eAAe,6BAAmG,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { ClaudeAdapter } from './claude-adapter.js';
2
+ import { CodexAdapter } from './codex-adapter.js';
3
+ import { CopilotAdapter } from './copilot-adapter.js';
4
+ export class VendorAdapterRegistry {
5
+ map = new Map();
6
+ constructor(initial = []) {
7
+ for (const a of initial)
8
+ this.map.set(a.id, a);
9
+ }
10
+ register(a) { this.map.set(a.id, a); }
11
+ get(id) { return this.map.get(id); }
12
+ list() { return [...this.map.values()]; }
13
+ }
14
+ export const defaultRegistry = () => new VendorAdapterRegistry([new ClaudeAdapter(), new CodexAdapter(), new CopilotAdapter()]);
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "peaks-loop-internal-runtime",
3
+ "version": "0.0.1",
4
+ "description": "Detached sub-agent runtime: ProcessSupervisor + LifecycleOwner + VendorAdapter (claude/codex/copilot) + G8 infinite-context auto-compact. Slice 2026-08-10.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "test": "vitest run tests/unit/runtime"
17
+ },
18
+ "peerDependencies": {
19
+ "peaks-loop-shared": "workspace:*"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.4.0",
23
+ "vitest": "4.1.10"
24
+ }
25
+ }
@@ -0,0 +1,30 @@
1
+ import type { AutoCompactEvent } from './status-protocol.js';
2
+
3
+ export interface MarkerOpts { rid: string; sid: string; vendorWindow: number; }
4
+ export interface ScratchPayload {
5
+ seq: number; at: number; summary: string;
6
+ decisionsKept?: string[]; discardedOptions?: string[];
7
+ }
8
+
9
+ export class AutoCompactAdapter {
10
+ marker(opts: MarkerOpts): string {
11
+ return [
12
+ `<peaks-auto-compact threshold="0.85|0.95" vendor-window="${opts.vendorWindow}">`,
13
+ `协议:`,
14
+ `- 你(子进程 LLM)估算自己已用上下文 ≥ 85% vendor window`,
15
+ ` → 主动 compact 自己的会话:把对话历史摘要写到`,
16
+ ` .peaks/_runtime/${opts.sid}/detached/${opts.rid}/compact/<n>.json`,
17
+ ` → 把摘要 + 当前任务状态拼回 prompt 头部`,
18
+ ` → 调用 peaks runtime write-compact-event CLI 记录事件`,
19
+ `- 当 ≥ 95% → 同步 compact + 立刻通知 peaks 主进程`,
20
+ ` (写 status.json note: 'compact-emergency')`,
21
+ `- 不要等 peaks 主进程来催;子进程 LLM 自己监控自己的 context`,
22
+ `- 不限费用(用户授权)—— compact 本身消耗的 token 随它去`,
23
+ `</peaks-auto-compact>`,
24
+ ].join('\n');
25
+ }
26
+
27
+ parseScratchFile(p: ScratchPayload): Partial<AutoCompactEvent> {
28
+ return { at: p.at, threshold: '0.85', tokensBefore: 0, tokensAfter: 0 };
29
+ }
30
+ }
@@ -0,0 +1,57 @@
1
+ import { writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { ProcessSupervisor } from './process-supervisor.js';
4
+ import { LifecycleOwner } from './lifecycle.js';
5
+ import { VendorAdapterRegistry } from './vendor/registry.js';
6
+ import { ClaudeAdapter } from './vendor/claude-adapter.js';
7
+ import { CodexAdapter } from './vendor/codex-adapter.js';
8
+ import { CopilotAdapter } from './vendor/copilot-adapter.js';
9
+ import { PromptBuilder } from './prompt-builder.js';
10
+ import { StatusProtocol } from './status-protocol.js';
11
+ import { AutoCompactAdapter } from './auto-compact-adapter.js';
12
+ import { ResourceBudgetGuard } from './guards/resource-budget.js';
13
+
14
+ export interface DispatchInput {
15
+ sid: string; rid: string; role: 'rd'|'qa'|'ui'|'txt'|'general-purpose';
16
+ vendor: 'claude'|'codex'|'copilot';
17
+ userTask: string; files: string[]; refs: string[];
18
+ runtimeDir: string; subAgentsDir: string;
19
+ verbatimBlocks?: string[];
20
+ }
21
+ export interface DispatchResult { pid: number; dispatchRecordPath: string; }
22
+
23
+ export async function dispatchDetached(i: DispatchInput): Promise<DispatchResult> {
24
+ const registry = new VendorAdapterRegistry([new ClaudeAdapter(), new CodexAdapter(), new CopilotAdapter()]);
25
+ const adapter = registry.get(i.vendor);
26
+ if (!adapter) throw new Error(`vendor adapter not registered: ${i.vendor}`);
27
+
28
+ const pb = new PromptBuilder();
29
+ const ac = new AutoCompactAdapter();
30
+ const marker = ac.marker({ rid: i.rid, sid: i.sid, vendorWindow: adapter.maxPromptBytes / 40 /* rough */ });
31
+ const prompt = pb.assemble({
32
+ rid: i.rid, role: i.role, vendor: i.vendor,
33
+ files: i.files, refs: i.refs, userTask: i.userTask,
34
+ verbatimBlocks: [marker, ...(i.verbatimBlocks ?? [])],
35
+ });
36
+
37
+ const args = adapter.headlessArgs(prompt, { autoCompactMarker: marker });
38
+
39
+ const dir = join(i.runtimeDir, i.rid);
40
+ mkdirSync(dir, { recursive: true });
41
+ writeFileSync(join(dir, 'owner-session'), i.sid);
42
+
43
+ const sup = new ProcessSupervisor({ runtimeDir: i.runtimeDir });
44
+ const lo = new LifecycleOwner(i.runtimeDir);
45
+ const handle = await sup.spawn(adapter.binary, args, { detach: true, rid: i.rid });
46
+ lo.register(handle.pid, i.rid, i.sid);
47
+
48
+ // Write dispatch record (placeholder — final shape per Task 8 schema)
49
+ const recPath = join(i.subAgentsDir, `dispatch-${i.rid}-${Date.now()}.json`);
50
+ mkdirSync(i.subAgentsDir, { recursive: true });
51
+ writeFileSync(recPath, JSON.stringify({
52
+ rid: i.rid, mode: 'detached', vendor: i.vendor,
53
+ status: 'running', heartbeats: [], at: Date.now(),
54
+ }, null, 2));
55
+
56
+ return { pid: handle.pid, dispatchRecordPath: recPath };
57
+ }
@@ -0,0 +1,20 @@
1
+ export interface Sample { rssMb: number; cpuPct: number; }
2
+ export interface EnforceInput { active: number; }
3
+ export interface EnforceOpts { maxConcurrent: number; }
4
+
5
+ export class ResourceBudgetGuard {
6
+ constructor(private readonly cfg: { maxRssMb: number; maxCpuPct: number }) {}
7
+
8
+ sample(): Sample {
9
+ const m = process.memoryUsage();
10
+ const c = process.cpuUsage();
11
+ const rssMb = Math.round(m.rss / 1024 / 1024);
12
+ const cpuPct = Math.round(((c.user + c.system) / 1_000_000) * 100) / 100;
13
+ return { rssMb, cpuPct };
14
+ }
15
+
16
+ enforce(input: EnforceInput, opts: EnforceOpts): { throttle: boolean; kill?: string[] } {
17
+ if (input.active > opts.maxConcurrent) return { throttle: true };
18
+ return { throttle: false };
19
+ }
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ // API contract: RUNTIME_VERSION tracks peaks-loop root version (4.0.x).
2
+ // This is what consumers (sub-agents dispatched by peaks-code) check at
3
+ // runtime to verify protocol compatibility.
4
+ export const RUNTIME_VERSION = '4.0.20';
5
+
6
+ // npm version: independent 0.0.x SemVer. Runtime is its own package
7
+ // (peaks-loop-internal-runtime@NPM_VERSION) on the registry; bumps
8
+ // per peaks release notes when its public surface actually changes
9
+ // (not per root version bump).
10
+ export const RUNTIME_NPM_VERSION = '0.0.1';
11
+ export { ClaudeAdapter } from './vendor/claude-adapter.js';
12
+ export { CodexAdapter } from './vendor/codex-adapter.js';
13
+ export { CopilotAdapter } from './vendor/copilot-adapter.js';
14
+ export { VendorAdapterRegistry, defaultRegistry } from './vendor/registry.js';
15
+ export type { VendorAdapter } from './vendor/adapter.js';
16
+ export { ProcessSupervisor } from './process-supervisor.js';
17
+ export { LifecycleOwner } from './lifecycle.js';
18
+ export { PromptBuilder } from './prompt-builder.js';
19
+ export { StatusProtocol } from './status-protocol.js';
20
+ export type { HeartbeatEntry, AutoCompactEvent } from './status-protocol.js';
21
+ export { AutoCompactAdapter } from './auto-compact-adapter.js';
22
+ export { ResourceBudgetGuard } from './guards/resource-budget.js';
23
+ export { dispatchDetached } from './dispatch.js';
24
+ export type { DispatchInput, DispatchResult } from './dispatch.js';
@@ -0,0 +1,40 @@
1
+ import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ interface ActiveRecord { pid: number; rid: string; ownerSession: string; }
5
+
6
+ export class LifecycleOwner {
7
+ private active = new Map<string, ActiveRecord>();
8
+
9
+ constructor(private readonly runtimeDir: string) {}
10
+
11
+ register(pid: number, rid: string, ownerSession: string): void {
12
+ this.active.set(rid, { pid, rid, ownerSession });
13
+ }
14
+
15
+ async markExit(rid: string, code: number, signal?: string): Promise<void> {
16
+ const dir = join(this.runtimeDir, rid);
17
+ if (!existsSync(dir)) return;
18
+ const exit = { code, signal, at: Date.now() };
19
+ writeFileSync(join(dir, 'exit.json'), JSON.stringify(exit));
20
+
21
+ // Archive
22
+ if (existsSync(join(dir, 'log.txt'))) renameSync(join(dir, 'log.txt'), join(dir, 'log-archive.txt'));
23
+ if (existsSync(join(dir, 'status.json'))) renameSync(join(dir, 'status.json'), join(dir, 'status-final.json'));
24
+
25
+ // Delete active markers
26
+ for (const f of ['pid', 'owner-session']) {
27
+ const p = join(dir, f);
28
+ if (existsSync(p)) rmSync(p);
29
+ }
30
+ this.active.delete(rid);
31
+ }
32
+
33
+ async reap(currentSessionId: string): Promise<string[]> {
34
+ const orphans: string[] = [];
35
+ for (const rec of this.active.values()) {
36
+ if (rec.ownerSession !== currentSessionId) orphans.push(rec.rid);
37
+ }
38
+ return orphans; // user decides via `peaks sub-agent cleanup --orphan`
39
+ }
40
+ }
@@ -0,0 +1,42 @@
1
+ import { spawn as nodeSpawn, ChildProcess } from 'node:child_process';
2
+ import { writeFileSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ export interface SpawnOpts {
6
+ detach: boolean;
7
+ rid: string;
8
+ stdio?: 'pipe' | 'ignore';
9
+ }
10
+ export interface SpawnHandle {
11
+ pid: number;
12
+ child: ChildProcess;
13
+ kill(signal?: NodeJS.Signals): void;
14
+ }
15
+
16
+ export class ProcessSupervisor {
17
+ constructor(private readonly cfg: { runtimeDir: string }) {}
18
+
19
+ async spawn(binary: string, args: string[], opts: SpawnOpts): Promise<SpawnHandle> {
20
+ const isWin = process.platform === 'win32';
21
+ const spawnOpts: any = {
22
+ detached: opts.detach,
23
+ stdio: opts.stdio ?? 'pipe',
24
+ };
25
+ if (isWin) {
26
+ spawnOpts.windowsHide = true;
27
+ // CREATE_NEW_PROCESS_GROUP = 0x00000200, DETACHED_PROCESS = 0x00000008
28
+ spawnOpts.detached = true;
29
+ }
30
+
31
+ const child = nodeSpawn(binary, args, spawnOpts);
32
+ const dir = join(this.cfg.runtimeDir, opts.rid);
33
+ mkdirSync(dir, { recursive: true });
34
+ writeFileSync(join(dir, 'pid'), String(child.pid ?? ''));
35
+
36
+ return {
37
+ pid: child.pid ?? -1,
38
+ child,
39
+ kill: (signal: NodeJS.Signals = 'SIGTERM') => child.kill(signal),
40
+ };
41
+ }
42
+ }
@@ -0,0 +1,36 @@
1
+ const FORBIDDEN = '@@@ORCHESTRATOR_SESSION_HISTORY_BOUNDARY@@@';
2
+
3
+ export interface AssembleInput {
4
+ rid: string;
5
+ role: 'rd' | 'qa' | 'ui' | 'txt' | 'general-purpose';
6
+ vendor: string;
7
+ files: string[];
8
+ refs: string[];
9
+ userTask: string;
10
+ verbatimBlocks?: string[];
11
+ }
12
+
13
+ export class PromptBuilder {
14
+ assemble(i: AssembleInput): string {
15
+ if (i.userTask.includes(FORBIDDEN)) throw new Error('forbidden marker in user task');
16
+ const parts = [
17
+ `rid: ${i.rid}`,
18
+ `role: ${i.role}`,
19
+ `vendor: ${i.vendor}`,
20
+ ``,
21
+ `## Task`,
22
+ i.userTask,
23
+ ``,
24
+ `## Files (read-only)`,
25
+ ...i.files.map(f => `- ${f}`),
26
+ ``,
27
+ `## References`,
28
+ ...i.refs.map(r => `- ${r}`),
29
+ ``,
30
+ ...(i.verbatimBlocks ?? []),
31
+ ];
32
+ const out = parts.join('\n');
33
+ if (out.includes(FORBIDDEN)) throw new Error('forbidden marker leak');
34
+ return out;
35
+ }
36
+ }
@@ -0,0 +1,23 @@
1
+ import type { ChildStatus } from './types.js';
2
+
3
+ export interface HeartbeatEntry { progress: number; note: string; ts: number; }
4
+ export interface AutoCompactEvent { at: number; threshold: '0.85' | '0.95'; tokensBefore: number; tokensAfter: number; scratchFile?: string; }
5
+
6
+ export class StatusProtocol {
7
+ merge(rec: any, s: ChildStatus): any {
8
+ const next = { ...rec };
9
+ next.heartbeats = [...(rec.heartbeats ?? []), { progress: s.progress, note: s.note, ts: s.ts }];
10
+ if (next.heartbeats.length > 100) {
11
+ next.heartbeats = next.heartbeats.slice(-100);
12
+ next.heartbeatsTruncated = true;
13
+ }
14
+ next.status = s.state === 'running' ? 'running' : s.state;
15
+ return next;
16
+ }
17
+ isStale(lastBeatAt: number, thresholdSec = 300): boolean {
18
+ return Date.now() - lastBeatAt > thresholdSec * 1000;
19
+ }
20
+ appendCompactEvent(rec: any, ev: AutoCompactEvent): any {
21
+ return { ...rec, autoCompactEvents: [...(rec.autoCompactEvents ?? []), ev] };
22
+ }
23
+ }
package/src/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type DetachedMode = 'in-process' | 'detached';
2
+ export type VendorId = 'claude' | 'codex' | 'copilot';
3
+ export interface ChildStatus {
4
+ rid: string;
5
+ vendor: VendorId;
6
+ progress: number;
7
+ state: 'running' | 'stale' | 'crashed' | 'oom-killed' | 'done' | 'spawn-failed';
8
+ note: string;
9
+ ts: number;
10
+ etaSec?: number;
11
+ }
@@ -0,0 +1,10 @@
1
+ import type { ChildStatus } from '../types.js';
2
+
3
+ export interface VendorAdapter {
4
+ readonly id: 'claude' | 'codex' | 'copilot';
5
+ readonly binary: string;
6
+ readonly maxPromptBytes: number;
7
+ headlessArgs(prompt: string, opts?: { autoCompactMarker?: string }): string[];
8
+ parseStatusLine(stdout: string): ChildStatus | null;
9
+ detectInstalled(): Promise<boolean>;
10
+ }
@@ -0,0 +1,41 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import type { VendorAdapter } from './adapter.js';
4
+ import type { ChildStatus } from '../types.js';
5
+
6
+ const pExecFile = promisify(execFile);
7
+
8
+ export class ClaudeAdapter implements VendorAdapter {
9
+ readonly id = 'claude' as const;
10
+ readonly binary = 'claude';
11
+ readonly maxPromptBytes = 8 * 1024;
12
+
13
+ headlessArgs(prompt: string, opts?: { autoCompactMarker?: string }): string[] {
14
+ const injected = opts?.autoCompactMarker
15
+ ? `${opts.autoCompactMarker}\n\n${prompt}`
16
+ : prompt;
17
+ return ['-p', injected, '--output-format', 'json', '--include-partial-messages'];
18
+ }
19
+
20
+ parseStatusLine(stdout: string): ChildStatus | null {
21
+ try {
22
+ const obj = JSON.parse(stdout);
23
+ if (typeof obj.progress !== 'number') return null;
24
+ return {
25
+ rid: String(obj.rid ?? ''),
26
+ vendor: 'claude',
27
+ progress: obj.progress,
28
+ state: obj.state,
29
+ note: String(obj.note ?? ''),
30
+ ts: Number(obj.ts ?? Date.now()),
31
+ };
32
+ } catch { return null; }
33
+ }
34
+
35
+ async detectInstalled(): Promise<boolean> {
36
+ try {
37
+ const { stdout } = await pExecFile(this.binary, ['--version'], { timeout: 3000 });
38
+ return stdout.length > 0;
39
+ } catch { return false; }
40
+ }
41
+ }
@@ -0,0 +1,25 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import type { VendorAdapter } from './adapter.js';
4
+ import type { ChildStatus } from '../types.js';
5
+ const pExecFile = promisify(execFile);
6
+
7
+ export class CodexAdapter implements VendorAdapter {
8
+ readonly id = 'codex' as const;
9
+ readonly binary = 'codex';
10
+ readonly maxPromptBytes = 5 * 1024;
11
+ headlessArgs(prompt: string): string[] {
12
+ return ['exec', '--json', prompt];
13
+ }
14
+ parseStatusLine(stdout: string): ChildStatus | null {
15
+ try {
16
+ const o = JSON.parse(stdout);
17
+ if (typeof o.progress !== 'number') return null;
18
+ return { rid: String(o.rid ?? ''), vendor: 'codex', progress: o.progress, state: o.state, note: String(o.note ?? ''), ts: Number(o.ts ?? Date.now()) };
19
+ } catch { return null; }
20
+ }
21
+ async detectInstalled(): Promise<boolean> {
22
+ try { const { stdout } = await pExecFile(this.binary, ['--version'], { timeout: 3000 }); return stdout.length > 0; }
23
+ catch { return false; }
24
+ }
25
+ }
@@ -0,0 +1,23 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import type { VendorAdapter } from './adapter.js';
4
+ import type { ChildStatus } from '../types.js';
5
+ const pExecFile = promisify(execFile);
6
+
7
+ export class CopilotAdapter implements VendorAdapter {
8
+ readonly id = 'copilot' as const;
9
+ readonly binary = 'copilot';
10
+ readonly maxPromptBytes = 6 * 1024;
11
+ headlessArgs(prompt: string): string[] { return ['-p', prompt, '--output-format', 'json']; }
12
+ parseStatusLine(stdout: string): ChildStatus | null {
13
+ try {
14
+ const o = JSON.parse(stdout);
15
+ if (typeof o.progress !== 'number') return null;
16
+ return { rid: String(o.rid ?? ''), vendor: 'copilot', progress: o.progress, state: o.state, note: String(o.note ?? ''), ts: Number(o.ts ?? Date.now()) };
17
+ } catch { return null; }
18
+ }
19
+ async detectInstalled(): Promise<boolean> {
20
+ try { const { stdout } = await pExecFile(this.binary, ['--version'], { timeout: 3000 }); return stdout.length > 0; }
21
+ catch { return false; }
22
+ }
23
+ }
@@ -0,0 +1,16 @@
1
+ import type { VendorAdapter } from './adapter.js';
2
+ import { ClaudeAdapter } from './claude-adapter.js';
3
+ import { CodexAdapter } from './codex-adapter.js';
4
+ import { CopilotAdapter } from './copilot-adapter.js';
5
+
6
+ export class VendorAdapterRegistry {
7
+ private map = new Map<string, VendorAdapter>();
8
+ constructor(initial: VendorAdapter[] = []) {
9
+ for (const a of initial) this.map.set(a.id, a);
10
+ }
11
+ register(a: VendorAdapter): void { this.map.set(a.id, a); }
12
+ get(id: string): VendorAdapter | undefined { return this.map.get(id); }
13
+ list(): VendorAdapter[] { return [...this.map.values()]; }
14
+ }
15
+
16
+ export const defaultRegistry = () => new VendorAdapterRegistry([new ClaudeAdapter(), new CodexAdapter(), new CopilotAdapter()]);
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "declaration": true
7
+ },
8
+ "include": ["src/**/*"]
9
+ }