peaks-loop-internal-runtime 0.0.31 → 0.0.33
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/auto-compact-adapter.d.ts +40 -1
- package/dist/auto-compact-adapter.d.ts.map +1 -1
- package/dist/auto-compact-adapter.js +46 -11
- package/dist/dispatch.d.ts +7 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +18 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/process-supervisor.d.ts +9 -0
- package/dist/process-supervisor.d.ts.map +1 -1
- package/dist/process-supervisor.js +33 -1
- package/dist/vendor/detect-binary.js +1 -1
- package/package.json +2 -2
- package/src/auto-compact-adapter.ts +58 -13
- package/src/dispatch.ts +30 -4
- package/src/index.ts +1 -1
- package/src/process-supervisor.ts +44 -1
- package/src/vendor/detect-binary.ts +1 -1
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { AutoCompactEvent } from './status-protocol.js';
|
|
2
|
+
/**
|
|
3
|
+
* `vendorWindow` is OPTIONAL and, when absent, the attribute is omitted.
|
|
4
|
+
*
|
|
5
|
+
* Why it is not defaulted: this used to be filled in by the caller with
|
|
6
|
+
* `adapter.maxPromptBytes / 40` (204.8 for the claude adapter) — a number that
|
|
7
|
+
* is not a context window, handed to a child that was then told to measure "85%
|
|
8
|
+
* of" it. A fabricated denominator is worse than none, because the child cannot
|
|
9
|
+
* tell that it is fabricated. When the caller genuinely knows the window it is
|
|
10
|
+
* still carried verbatim (E5, rid 2026-09-13-defects-e).
|
|
11
|
+
*/
|
|
2
12
|
export interface MarkerOpts {
|
|
3
13
|
rid: string;
|
|
4
14
|
sid: string;
|
|
5
|
-
vendorWindow
|
|
15
|
+
vendorWindow?: number;
|
|
6
16
|
}
|
|
7
17
|
export interface ScratchPayload {
|
|
8
18
|
seq: number;
|
|
@@ -11,6 +21,35 @@ export interface ScratchPayload {
|
|
|
11
21
|
decisionsKept?: string[];
|
|
12
22
|
discardedOptions?: string[];
|
|
13
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The `<peaks-auto-compact>` marker injected at the head of a detached child's
|
|
26
|
+
* prompt.
|
|
27
|
+
*
|
|
28
|
+
* REWRITTEN IN E5 (rid 2026-09-13-defects-e). The previous text assigned the
|
|
29
|
+
* child three capabilities it does not have, and the third of them was not even
|
|
30
|
+
* a registered command:
|
|
31
|
+
*
|
|
32
|
+
* 1. "主动 compact 自己的会话" — a model cannot compact its own session. `/compact`
|
|
33
|
+
* is not model-invokable, hooks may only observe or veto, there is no
|
|
34
|
+
* `--compact` flag, and no external process can reach a running session's
|
|
35
|
+
* memory. The HARNESS compacts, on its own window and its own schedule.
|
|
36
|
+
* 2. "把摘要 + 当前任务状态拼回 prompt 头部" — the child's prompt IS the `-p` argv
|
|
37
|
+
* string built at spawn time; `ClaudeAdapter.headlessArgs` prepends this
|
|
38
|
+
* very marker to it. Nothing rewrites it afterwards: `dispatchDetached` is
|
|
39
|
+
* fire-and-forget and the spawning CLI exits, so there is no re-assembly
|
|
40
|
+
* step in this process or any other.
|
|
41
|
+
* 3. "调用 peaks runtime write-compact-event CLI 记录事件" — no such verb exists.
|
|
42
|
+
* `peaks runtime` registers `detect` / `list` / `compact` and nothing else.
|
|
43
|
+
*
|
|
44
|
+
* The failure mode was worse than useless: at exactly the moment the child's
|
|
45
|
+
* context is nearly full, it was told to spend what remained on an instruction
|
|
46
|
+
* that could not succeed, and to believe it had.
|
|
47
|
+
*
|
|
48
|
+
* What survives is the part that IS reachable and useful: the child can write
|
|
49
|
+
* durable state to disk. That is stated as the action, together with the plain
|
|
50
|
+
* negative — nothing flows back into its context — so the child neither wastes
|
|
51
|
+
* effort on the impossible nor mistakes a written file for a restored context.
|
|
52
|
+
*/
|
|
14
53
|
export declare class AutoCompactAdapter {
|
|
15
54
|
marker(opts: MarkerOpts): string;
|
|
16
55
|
parseScratchFile(p: ScratchPayload): Partial<AutoCompactEvent>;
|
|
@@ -1 +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;
|
|
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;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAAE;AAChF,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,kBAAkB;IAC7B,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM;IAuBhC,gBAAgB,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAG/D"}
|
|
@@ -1,17 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<peaks-auto-compact>` marker injected at the head of a detached child's
|
|
3
|
+
* prompt.
|
|
4
|
+
*
|
|
5
|
+
* REWRITTEN IN E5 (rid 2026-09-13-defects-e). The previous text assigned the
|
|
6
|
+
* child three capabilities it does not have, and the third of them was not even
|
|
7
|
+
* a registered command:
|
|
8
|
+
*
|
|
9
|
+
* 1. "主动 compact 自己的会话" — a model cannot compact its own session. `/compact`
|
|
10
|
+
* is not model-invokable, hooks may only observe or veto, there is no
|
|
11
|
+
* `--compact` flag, and no external process can reach a running session's
|
|
12
|
+
* memory. The HARNESS compacts, on its own window and its own schedule.
|
|
13
|
+
* 2. "把摘要 + 当前任务状态拼回 prompt 头部" — the child's prompt IS the `-p` argv
|
|
14
|
+
* string built at spawn time; `ClaudeAdapter.headlessArgs` prepends this
|
|
15
|
+
* very marker to it. Nothing rewrites it afterwards: `dispatchDetached` is
|
|
16
|
+
* fire-and-forget and the spawning CLI exits, so there is no re-assembly
|
|
17
|
+
* step in this process or any other.
|
|
18
|
+
* 3. "调用 peaks runtime write-compact-event CLI 记录事件" — no such verb exists.
|
|
19
|
+
* `peaks runtime` registers `detect` / `list` / `compact` and nothing else.
|
|
20
|
+
*
|
|
21
|
+
* The failure mode was worse than useless: at exactly the moment the child's
|
|
22
|
+
* context is nearly full, it was told to spend what remained on an instruction
|
|
23
|
+
* that could not succeed, and to believe it had.
|
|
24
|
+
*
|
|
25
|
+
* What survives is the part that IS reachable and useful: the child can write
|
|
26
|
+
* durable state to disk. That is stated as the action, together with the plain
|
|
27
|
+
* negative — nothing flows back into its context — so the child neither wastes
|
|
28
|
+
* effort on the impossible nor mistakes a written file for a restored context.
|
|
29
|
+
*/
|
|
1
30
|
export class AutoCompactAdapter {
|
|
2
31
|
marker(opts) {
|
|
32
|
+
const windowAttr = opts.vendorWindow === undefined ? '' : ` vendor-window="${opts.vendorWindow}"`;
|
|
3
33
|
return [
|
|
4
|
-
`<peaks-auto-compact threshold="0.85|0.95"
|
|
5
|
-
|
|
6
|
-
`-
|
|
7
|
-
`
|
|
8
|
-
`
|
|
9
|
-
` →
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`
|
|
13
|
-
|
|
14
|
-
|
|
34
|
+
`<peaks-auto-compact threshold="0.85|0.95"${windowAttr}>`,
|
|
35
|
+
`上下文续命协议(如实版 —— 只写你确实能做的事):`,
|
|
36
|
+
`- 你的会话由 vendor harness(Claude Code 自己)压缩:它按自己的 auto-compact window 触发,`,
|
|
37
|
+
` 到点直接压,不需要你请求,也无法被你触发。peaks-loop 不能压缩一个正在运行的会话 ——`,
|
|
38
|
+
` 没有模型可调用的 compact,外部进程也触及不到你的会话内存。`,
|
|
39
|
+
` → 所以这里没有让你自己压缩会话的入口,也不要声称做过。`,
|
|
40
|
+
`- 你能做、也必须做的是「在被压缩之前把不能丢的东西落到磁盘」:`,
|
|
41
|
+
` 1) 摘要 + 当前任务状态 + 已定决策 + 已弃选项,写到`,
|
|
42
|
+
` .peaks/_runtime/${opts.sid}/detached/${opts.rid}/compact/<n>.json`,
|
|
43
|
+
` (<n> 从 1 递增;每次新增一个文件,不要改写已有的)`,
|
|
44
|
+
` 2) 进度 / 心跳照常写 status.json(progress / state / note)`,
|
|
45
|
+
`- 你的 prompt 在 spawn 时已经固定,没有任何机制能把摘要拼回它。写进文件里的东西`,
|
|
46
|
+
` 是给 peaks 主进程和人看的进度证据,不会回流到你的上下文。`,
|
|
47
|
+
`- 这些文件由你自己维护:不要等 peaks 主进程来催,它不会替你写。`,
|
|
48
|
+
`- token 费用:peaks-loop 不设费用上限、也不因费用中断你(没有费用闸门)。`,
|
|
49
|
+
` 这不对应任何 flag —— 它是 peaks-loop 没有做的事,不是一条你能引用的授权。`,
|
|
15
50
|
`</peaks-auto-compact>`,
|
|
16
51
|
].join('\n');
|
|
17
52
|
}
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface DispatchResult {
|
|
|
15
15
|
pid: number;
|
|
16
16
|
dispatchRecordPath: string;
|
|
17
17
|
child?: ChildProcess;
|
|
18
|
+
/**
|
|
19
|
+
* Typed launch failure, or `null` when the vendor CLI started. A vendor CLI
|
|
20
|
+
* that is not installed is an expected environment, not a crash: the ENOENT
|
|
21
|
+
* used to surface as an unhandled 'error' event (see ProcessSupervisor.spawn)
|
|
22
|
+
* and escape every awaiting caller's promise chain.
|
|
23
|
+
*/
|
|
24
|
+
spawnError: NodeJS.ErrnoException | null;
|
|
18
25
|
}
|
|
19
26
|
export declare function dispatchDetached(i: DispatchInput): Promise<DispatchResult>;
|
|
20
27
|
//# sourceMappingURL=dispatch.d.ts.map
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAYvD,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;
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAYvD,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;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;CAC1C;AAED,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAqDhF"}
|
package/dist/dispatch.js
CHANGED
|
@@ -15,7 +15,14 @@ export async function dispatchDetached(i) {
|
|
|
15
15
|
throw new Error(`vendor adapter not registered: ${i.vendor}`);
|
|
16
16
|
const pb = new PromptBuilder();
|
|
17
17
|
const ac = new AutoCompactAdapter();
|
|
18
|
-
|
|
18
|
+
// E5 (rid 2026-09-13-defects-e): the marker used to carry
|
|
19
|
+
// `adapter.maxPromptBytes / 40` as its `vendor-window` — 204.8 for the claude
|
|
20
|
+
// adapter. That is not a context window (it is a prompt BYTE budget divided by
|
|
21
|
+
// forty), and the child was then told to measure "85% of" it, with no way to
|
|
22
|
+
// tell the number was fabricated. peaks-loop does not know the child's window
|
|
23
|
+
// here — the child's own harness owns it — so the attribute is omitted rather
|
|
24
|
+
// than invented.
|
|
25
|
+
const marker = ac.marker({ rid: i.rid, sid: i.sid });
|
|
19
26
|
const prompt = pb.assemble({
|
|
20
27
|
rid: i.rid, role: i.role, vendor: i.vendor,
|
|
21
28
|
files: i.files, refs: i.refs, userTask: i.userTask,
|
|
@@ -32,13 +39,21 @@ export async function dispatchDetached(i) {
|
|
|
32
39
|
// no-op (ProcessSupervisor forces detached:false). Pass `false` to
|
|
33
40
|
// reflect the post-F2 contract explicitly.
|
|
34
41
|
const handle = await sup.spawn(adapter.binary, args, { detach: false, rid: i.rid });
|
|
42
|
+
// `settled` never rejects; it carries the launch outcome as a value. Awaiting
|
|
43
|
+
// it here is what makes a missing vendor CLI an assertable result rather than
|
|
44
|
+
// an exception thrown from outside this function's promise chain.
|
|
45
|
+
const spawnError = (await handle.settled) ?? null;
|
|
35
46
|
lo.register(handle.pid, i.rid, i.sid);
|
|
36
47
|
// Write dispatch record (placeholder — final shape per Task 8 schema)
|
|
37
48
|
const recPath = join(i.subAgentsDir, `dispatch-${i.rid}-${Date.now()}.json`);
|
|
38
49
|
mkdirSync(i.subAgentsDir, { recursive: true });
|
|
39
50
|
writeFileSync(recPath, JSON.stringify({
|
|
40
51
|
rid: i.rid, mode: 'detached', vendor: i.vendor,
|
|
41
|
-
|
|
52
|
+
// The record must not claim a child is running when the launch failed —
|
|
53
|
+
// that is the on-disk form of "looks green, checked nothing".
|
|
54
|
+
status: spawnError ? 'failed' : 'running',
|
|
55
|
+
...(spawnError ? { spawnError: { code: spawnError.code, message: spawnError.message } } : {}),
|
|
56
|
+
heartbeats: [], at: Date.now(),
|
|
42
57
|
}, null, 2));
|
|
43
|
-
return { pid: handle.pid, dispatchRecordPath: recPath, child: handle.child };
|
|
58
|
+
return { pid: handle.pid, dispatchRecordPath: recPath, child: handle.child, spawnError };
|
|
44
59
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "4.0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "4.0.48";
|
|
2
2
|
export declare const RUNTIME_NPM_VERSION = "0.0.21";
|
|
3
3
|
export { ClaudeAdapter } from './vendor/claude-adapter.js';
|
|
4
4
|
export { CodexAdapter } from './vendor/codex-adapter.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// API contract: RUNTIME_VERSION tracks peaks-loop root version (4.0.x).
|
|
2
2
|
// This is what consumers (sub-agents dispatched by peaks-code) check at
|
|
3
3
|
// runtime to verify protocol compatibility.
|
|
4
|
-
export const RUNTIME_VERSION = '4.0.
|
|
4
|
+
export const RUNTIME_VERSION = '4.0.48';
|
|
5
5
|
// npm version: independent 0.0.x SemVer. Runtime is its own package
|
|
6
6
|
// (peaks-loop-internal-runtime@NPM_VERSION) on the registry; bumps
|
|
7
7
|
// per peaks release notes when its public surface actually changes
|
|
@@ -17,6 +17,15 @@ export interface SpawnHandle {
|
|
|
17
17
|
pid: number;
|
|
18
18
|
child: ChildProcess;
|
|
19
19
|
kill(signal?: NodeJS.Signals): void;
|
|
20
|
+
/**
|
|
21
|
+
* Resolves once the OS confirms the launch (`'spawn'`) or the launch fails
|
|
22
|
+
* (`'error'`). Never rejects: `null` means the process is running; a launch
|
|
23
|
+
* failure resolves to the otherwise-unhandled ErrnoException (ENOENT when the
|
|
24
|
+
* vendor CLI is not installed). The listener that settles this is attached
|
|
25
|
+
* synchronously in `spawn()` — see the comment there for why no caller can
|
|
26
|
+
* attach one in time.
|
|
27
|
+
*/
|
|
28
|
+
settled: Promise<NodeJS.ErrnoException | null>;
|
|
20
29
|
}
|
|
21
30
|
export declare class ProcessSupervisor {
|
|
22
31
|
private readonly cfg;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-supervisor.d.ts","sourceRoot":"","sources":["../src/process-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AAIzF,MAAM,WAAW,SAAS;IACxB;;;;;;;;OAQG;IACH,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;
|
|
1
|
+
{"version":3,"file":"process-supervisor.d.ts","sourceRoot":"","sources":["../src/process-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AAIzF,MAAM,WAAW,SAAS;IACxB;;;;;;;;OAQG;IACH,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;IACpC;;;;;;;OAOG;IACH,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CAChD;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;CAiEnF"}
|
|
@@ -23,16 +23,48 @@ export class ProcessSupervisor {
|
|
|
23
23
|
// Suppress the popup console window on Windows without changing
|
|
24
24
|
// process group membership. The child is still part of the
|
|
25
25
|
// parent's process group (no Windows process-group detach flag).
|
|
26
|
+
// Deliberately NOT set on POSIX — pinned by
|
|
27
|
+
// `tests/unit/runtime/process-supervisor-in-shell.test.ts` 1a/1b,
|
|
28
|
+
// which assert this platform split by name. The spawn-hygiene guard
|
|
29
|
+
// reads this assignment form; see `collectWindowsHideAssignments`.
|
|
26
30
|
spawnOpts.windowsHide = true;
|
|
27
31
|
}
|
|
28
32
|
const child = nodeSpawn(binary, args, spawnOpts);
|
|
33
|
+
// Attach the 'error' listener in the SAME synchronous turn the child is
|
|
34
|
+
// created, because no caller can do it in time. A missing binary does not
|
|
35
|
+
// throw from spawn(): Node emits it asynchronously as an 'error' event, and
|
|
36
|
+
// an EventEmitter with no 'error' listener re-throws it as a process-level
|
|
37
|
+
// uncaught exception. That emission runs on the nextTick queue, which
|
|
38
|
+
// drains BEFORE the awaiting caller resumes — so the documented caller-side
|
|
39
|
+
// pattern (`await spawn(...); child.on('error', …)`) loses the race by
|
|
40
|
+
// construction, whatever caller writes it. Capturing it here turns the
|
|
41
|
+
// failure into a typed value on `settled` and removes the crash.
|
|
42
|
+
const settled = new Promise((resolve) => {
|
|
43
|
+
child.on('error', (err) => resolve(err));
|
|
44
|
+
child.on('spawn', () => resolve(null));
|
|
45
|
+
});
|
|
29
46
|
const dir = join(this.cfg.runtimeDir, opts.rid);
|
|
30
47
|
mkdirSync(dir, { recursive: true });
|
|
31
|
-
|
|
48
|
+
// `<rid>/pid` exists IFF a real OS process was launched. `child.pid` is
|
|
49
|
+
// `undefined` until the OS confirms the spawn, so a failed launch used to
|
|
50
|
+
// write `String(undefined ?? '')` — an EMPTY pid file. That is not a
|
|
51
|
+
// cosmetic wart: `Number('')` is `0`, so a cleanup path doing
|
|
52
|
+
// `kill(Number(readFileSync(pid)))` would send its signal to pid 0 (the
|
|
53
|
+
// whole process group), and an existence check for "is something running
|
|
54
|
+
// here" reads `true` for a launch that never happened.
|
|
55
|
+
//
|
|
56
|
+
// Nothing is lost by omitting it: the launch failure is recorded as a
|
|
57
|
+
// VALUE on the dispatch record (`status: 'failed'` + `spawnError`), which
|
|
58
|
+
// is the surface a reader should consult. Absence in the pid file is the
|
|
59
|
+
// one encoding that cannot be misread as a pid.
|
|
60
|
+
if (child.pid !== undefined) {
|
|
61
|
+
writeFileSync(join(dir, 'pid'), String(child.pid));
|
|
62
|
+
}
|
|
32
63
|
return {
|
|
33
64
|
pid: child.pid ?? -1,
|
|
34
65
|
child,
|
|
35
66
|
kill: (signal = 'SIGTERM') => child.kill(signal),
|
|
67
|
+
settled,
|
|
36
68
|
};
|
|
37
69
|
}
|
|
38
70
|
}
|
|
@@ -4,7 +4,7 @@ const DETECT_TIMEOUT_MS = 3000;
|
|
|
4
4
|
export function detectBinaryInstalled(binary) {
|
|
5
5
|
const locator = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
6
6
|
return new Promise((resolve) => {
|
|
7
|
-
execFile(locator, [binary], { timeout: DETECT_TIMEOUT_MS }, (error, stdout) => {
|
|
7
|
+
execFile(locator, [binary], { timeout: DETECT_TIMEOUT_MS, windowsHide: true }, (error, stdout) => {
|
|
8
8
|
resolve(error === null && stdout.trim().length > 0);
|
|
9
9
|
});
|
|
10
10
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop-internal-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"description": "Detached sub-agent runtime: ProcessSupervisor + LifecycleOwner + VendorAdapter (claude/codex/copilot) + G8 infinite-context auto-compact. Slice 2026-08-10.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"peaks-loop-shared": "0.0.
|
|
19
|
+
"peaks-loop-shared": "0.0.82"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"typescript": "^5.4.0",
|
|
@@ -1,25 +1,70 @@
|
|
|
1
1
|
import type { AutoCompactEvent } from './status-protocol.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `vendorWindow` is OPTIONAL and, when absent, the attribute is omitted.
|
|
5
|
+
*
|
|
6
|
+
* Why it is not defaulted: this used to be filled in by the caller with
|
|
7
|
+
* `adapter.maxPromptBytes / 40` (204.8 for the claude adapter) — a number that
|
|
8
|
+
* is not a context window, handed to a child that was then told to measure "85%
|
|
9
|
+
* of" it. A fabricated denominator is worse than none, because the child cannot
|
|
10
|
+
* tell that it is fabricated. When the caller genuinely knows the window it is
|
|
11
|
+
* still carried verbatim (E5, rid 2026-09-13-defects-e).
|
|
12
|
+
*/
|
|
13
|
+
export interface MarkerOpts { rid: string; sid: string; vendorWindow?: number; }
|
|
4
14
|
export interface ScratchPayload {
|
|
5
15
|
seq: number; at: number; summary: string;
|
|
6
16
|
decisionsKept?: string[]; discardedOptions?: string[];
|
|
7
17
|
}
|
|
8
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The `<peaks-auto-compact>` marker injected at the head of a detached child's
|
|
21
|
+
* prompt.
|
|
22
|
+
*
|
|
23
|
+
* REWRITTEN IN E5 (rid 2026-09-13-defects-e). The previous text assigned the
|
|
24
|
+
* child three capabilities it does not have, and the third of them was not even
|
|
25
|
+
* a registered command:
|
|
26
|
+
*
|
|
27
|
+
* 1. "主动 compact 自己的会话" — a model cannot compact its own session. `/compact`
|
|
28
|
+
* is not model-invokable, hooks may only observe or veto, there is no
|
|
29
|
+
* `--compact` flag, and no external process can reach a running session's
|
|
30
|
+
* memory. The HARNESS compacts, on its own window and its own schedule.
|
|
31
|
+
* 2. "把摘要 + 当前任务状态拼回 prompt 头部" — the child's prompt IS the `-p` argv
|
|
32
|
+
* string built at spawn time; `ClaudeAdapter.headlessArgs` prepends this
|
|
33
|
+
* very marker to it. Nothing rewrites it afterwards: `dispatchDetached` is
|
|
34
|
+
* fire-and-forget and the spawning CLI exits, so there is no re-assembly
|
|
35
|
+
* step in this process or any other.
|
|
36
|
+
* 3. "调用 peaks runtime write-compact-event CLI 记录事件" — no such verb exists.
|
|
37
|
+
* `peaks runtime` registers `detect` / `list` / `compact` and nothing else.
|
|
38
|
+
*
|
|
39
|
+
* The failure mode was worse than useless: at exactly the moment the child's
|
|
40
|
+
* context is nearly full, it was told to spend what remained on an instruction
|
|
41
|
+
* that could not succeed, and to believe it had.
|
|
42
|
+
*
|
|
43
|
+
* What survives is the part that IS reachable and useful: the child can write
|
|
44
|
+
* durable state to disk. That is stated as the action, together with the plain
|
|
45
|
+
* negative — nothing flows back into its context — so the child neither wastes
|
|
46
|
+
* effort on the impossible nor mistakes a written file for a restored context.
|
|
47
|
+
*/
|
|
9
48
|
export class AutoCompactAdapter {
|
|
10
49
|
marker(opts: MarkerOpts): string {
|
|
50
|
+
const windowAttr = opts.vendorWindow === undefined ? '' : ` vendor-window="${opts.vendorWindow}"`;
|
|
11
51
|
return [
|
|
12
|
-
`<peaks-auto-compact threshold="0.85|0.95"
|
|
13
|
-
|
|
14
|
-
`-
|
|
15
|
-
`
|
|
16
|
-
`
|
|
17
|
-
` →
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
`
|
|
21
|
-
|
|
22
|
-
|
|
52
|
+
`<peaks-auto-compact threshold="0.85|0.95"${windowAttr}>`,
|
|
53
|
+
`上下文续命协议(如实版 —— 只写你确实能做的事):`,
|
|
54
|
+
`- 你的会话由 vendor harness(Claude Code 自己)压缩:它按自己的 auto-compact window 触发,`,
|
|
55
|
+
` 到点直接压,不需要你请求,也无法被你触发。peaks-loop 不能压缩一个正在运行的会话 ——`,
|
|
56
|
+
` 没有模型可调用的 compact,外部进程也触及不到你的会话内存。`,
|
|
57
|
+
` → 所以这里没有让你自己压缩会话的入口,也不要声称做过。`,
|
|
58
|
+
`- 你能做、也必须做的是「在被压缩之前把不能丢的东西落到磁盘」:`,
|
|
59
|
+
` 1) 摘要 + 当前任务状态 + 已定决策 + 已弃选项,写到`,
|
|
60
|
+
` .peaks/_runtime/${opts.sid}/detached/${opts.rid}/compact/<n>.json`,
|
|
61
|
+
` (<n> 从 1 递增;每次新增一个文件,不要改写已有的)`,
|
|
62
|
+
` 2) 进度 / 心跳照常写 status.json(progress / state / note)`,
|
|
63
|
+
`- 你的 prompt 在 spawn 时已经固定,没有任何机制能把摘要拼回它。写进文件里的东西`,
|
|
64
|
+
` 是给 peaks 主进程和人看的进度证据,不会回流到你的上下文。`,
|
|
65
|
+
`- 这些文件由你自己维护:不要等 peaks 主进程来催,它不会替你写。`,
|
|
66
|
+
`- token 费用:peaks-loop 不设费用上限、也不因费用中断你(没有费用闸门)。`,
|
|
67
|
+
` 这不对应任何 flag —— 它是 peaks-loop 没有做的事,不是一条你能引用的授权。`,
|
|
23
68
|
`</peaks-auto-compact>`,
|
|
24
69
|
].join('\n');
|
|
25
70
|
}
|
|
@@ -27,4 +72,4 @@ export class AutoCompactAdapter {
|
|
|
27
72
|
parseScratchFile(p: ScratchPayload): Partial<AutoCompactEvent> {
|
|
28
73
|
return { at: p.at, threshold: '0.85', tokensBefore: 0, tokensAfter: 0 };
|
|
29
74
|
}
|
|
30
|
-
}
|
|
75
|
+
}
|
package/src/dispatch.ts
CHANGED
|
@@ -19,7 +19,18 @@ export interface DispatchInput {
|
|
|
19
19
|
runtimeDir: string; subAgentsDir: string;
|
|
20
20
|
verbatimBlocks?: string[];
|
|
21
21
|
}
|
|
22
|
-
export interface DispatchResult {
|
|
22
|
+
export interface DispatchResult {
|
|
23
|
+
pid: number;
|
|
24
|
+
dispatchRecordPath: string;
|
|
25
|
+
child?: ChildProcess;
|
|
26
|
+
/**
|
|
27
|
+
* Typed launch failure, or `null` when the vendor CLI started. A vendor CLI
|
|
28
|
+
* that is not installed is an expected environment, not a crash: the ENOENT
|
|
29
|
+
* used to surface as an unhandled 'error' event (see ProcessSupervisor.spawn)
|
|
30
|
+
* and escape every awaiting caller's promise chain.
|
|
31
|
+
*/
|
|
32
|
+
spawnError: NodeJS.ErrnoException | null;
|
|
33
|
+
}
|
|
23
34
|
|
|
24
35
|
export async function dispatchDetached(i: DispatchInput): Promise<DispatchResult> {
|
|
25
36
|
const registry = new VendorAdapterRegistry([new ClaudeAdapter(), new CodexAdapter(), new CopilotAdapter()]);
|
|
@@ -28,7 +39,14 @@ export async function dispatchDetached(i: DispatchInput): Promise<DispatchResult
|
|
|
28
39
|
|
|
29
40
|
const pb = new PromptBuilder();
|
|
30
41
|
const ac = new AutoCompactAdapter();
|
|
31
|
-
|
|
42
|
+
// E5 (rid 2026-09-13-defects-e): the marker used to carry
|
|
43
|
+
// `adapter.maxPromptBytes / 40` as its `vendor-window` — 204.8 for the claude
|
|
44
|
+
// adapter. That is not a context window (it is a prompt BYTE budget divided by
|
|
45
|
+
// forty), and the child was then told to measure "85% of" it, with no way to
|
|
46
|
+
// tell the number was fabricated. peaks-loop does not know the child's window
|
|
47
|
+
// here — the child's own harness owns it — so the attribute is omitted rather
|
|
48
|
+
// than invented.
|
|
49
|
+
const marker = ac.marker({ rid: i.rid, sid: i.sid });
|
|
32
50
|
const prompt = pb.assemble({
|
|
33
51
|
rid: i.rid, role: i.role, vendor: i.vendor,
|
|
34
52
|
files: i.files, refs: i.refs, userTask: i.userTask,
|
|
@@ -48,6 +66,10 @@ export async function dispatchDetached(i: DispatchInput): Promise<DispatchResult
|
|
|
48
66
|
// no-op (ProcessSupervisor forces detached:false). Pass `false` to
|
|
49
67
|
// reflect the post-F2 contract explicitly.
|
|
50
68
|
const handle = await sup.spawn(adapter.binary, args, { detach: false, rid: i.rid });
|
|
69
|
+
// `settled` never rejects; it carries the launch outcome as a value. Awaiting
|
|
70
|
+
// it here is what makes a missing vendor CLI an assertable result rather than
|
|
71
|
+
// an exception thrown from outside this function's promise chain.
|
|
72
|
+
const spawnError = (await handle.settled) ?? null;
|
|
51
73
|
lo.register(handle.pid, i.rid, i.sid);
|
|
52
74
|
|
|
53
75
|
// Write dispatch record (placeholder — final shape per Task 8 schema)
|
|
@@ -55,8 +77,12 @@ export async function dispatchDetached(i: DispatchInput): Promise<DispatchResult
|
|
|
55
77
|
mkdirSync(i.subAgentsDir, { recursive: true });
|
|
56
78
|
writeFileSync(recPath, JSON.stringify({
|
|
57
79
|
rid: i.rid, mode: 'detached', vendor: i.vendor,
|
|
58
|
-
|
|
80
|
+
// The record must not claim a child is running when the launch failed —
|
|
81
|
+
// that is the on-disk form of "looks green, checked nothing".
|
|
82
|
+
status: spawnError ? 'failed' : 'running',
|
|
83
|
+
...(spawnError ? { spawnError: { code: spawnError.code, message: spawnError.message } } : {}),
|
|
84
|
+
heartbeats: [], at: Date.now(),
|
|
59
85
|
}, null, 2));
|
|
60
86
|
|
|
61
|
-
return { pid: handle.pid, dispatchRecordPath: recPath, child: handle.child };
|
|
87
|
+
return { pid: handle.pid, dispatchRecordPath: recPath, child: handle.child, spawnError };
|
|
62
88
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// API contract: RUNTIME_VERSION tracks peaks-loop root version (4.0.x).
|
|
2
2
|
// This is what consumers (sub-agents dispatched by peaks-code) check at
|
|
3
3
|
// runtime to verify protocol compatibility.
|
|
4
|
-
export const RUNTIME_VERSION = '4.0.
|
|
4
|
+
export const RUNTIME_VERSION = '4.0.48';
|
|
5
5
|
|
|
6
6
|
// npm version: independent 0.0.x SemVer. Runtime is its own package
|
|
7
7
|
// (peaks-loop-internal-runtime@NPM_VERSION) on the registry; bumps
|
|
@@ -20,6 +20,15 @@ export interface SpawnHandle {
|
|
|
20
20
|
pid: number;
|
|
21
21
|
child: ChildProcess;
|
|
22
22
|
kill(signal?: NodeJS.Signals): void;
|
|
23
|
+
/**
|
|
24
|
+
* Resolves once the OS confirms the launch (`'spawn'`) or the launch fails
|
|
25
|
+
* (`'error'`). Never rejects: `null` means the process is running; a launch
|
|
26
|
+
* failure resolves to the otherwise-unhandled ErrnoException (ENOENT when the
|
|
27
|
+
* vendor CLI is not installed). The listener that settles this is attached
|
|
28
|
+
* synchronously in `spawn()` — see the comment there for why no caller can
|
|
29
|
+
* attach one in time.
|
|
30
|
+
*/
|
|
31
|
+
settled: Promise<NodeJS.ErrnoException | null>;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
export class ProcessSupervisor {
|
|
@@ -42,18 +51,52 @@ export class ProcessSupervisor {
|
|
|
42
51
|
// Suppress the popup console window on Windows without changing
|
|
43
52
|
// process group membership. The child is still part of the
|
|
44
53
|
// parent's process group (no Windows process-group detach flag).
|
|
54
|
+
// Deliberately NOT set on POSIX — pinned by
|
|
55
|
+
// `tests/unit/runtime/process-supervisor-in-shell.test.ts` 1a/1b,
|
|
56
|
+
// which assert this platform split by name. The spawn-hygiene guard
|
|
57
|
+
// reads this assignment form; see `collectWindowsHideAssignments`.
|
|
45
58
|
spawnOpts.windowsHide = true;
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
const child = nodeSpawn(binary, args, spawnOpts);
|
|
62
|
+
|
|
63
|
+
// Attach the 'error' listener in the SAME synchronous turn the child is
|
|
64
|
+
// created, because no caller can do it in time. A missing binary does not
|
|
65
|
+
// throw from spawn(): Node emits it asynchronously as an 'error' event, and
|
|
66
|
+
// an EventEmitter with no 'error' listener re-throws it as a process-level
|
|
67
|
+
// uncaught exception. That emission runs on the nextTick queue, which
|
|
68
|
+
// drains BEFORE the awaiting caller resumes — so the documented caller-side
|
|
69
|
+
// pattern (`await spawn(...); child.on('error', …)`) loses the race by
|
|
70
|
+
// construction, whatever caller writes it. Capturing it here turns the
|
|
71
|
+
// failure into a typed value on `settled` and removes the crash.
|
|
72
|
+
const settled = new Promise<NodeJS.ErrnoException | null>((resolve) => {
|
|
73
|
+
child.on('error', (err: Error) => resolve(err as NodeJS.ErrnoException));
|
|
74
|
+
child.on('spawn', () => resolve(null));
|
|
75
|
+
});
|
|
76
|
+
|
|
49
77
|
const dir = join(this.cfg.runtimeDir, opts.rid);
|
|
50
78
|
mkdirSync(dir, { recursive: true });
|
|
51
|
-
|
|
79
|
+
// `<rid>/pid` exists IFF a real OS process was launched. `child.pid` is
|
|
80
|
+
// `undefined` until the OS confirms the spawn, so a failed launch used to
|
|
81
|
+
// write `String(undefined ?? '')` — an EMPTY pid file. That is not a
|
|
82
|
+
// cosmetic wart: `Number('')` is `0`, so a cleanup path doing
|
|
83
|
+
// `kill(Number(readFileSync(pid)))` would send its signal to pid 0 (the
|
|
84
|
+
// whole process group), and an existence check for "is something running
|
|
85
|
+
// here" reads `true` for a launch that never happened.
|
|
86
|
+
//
|
|
87
|
+
// Nothing is lost by omitting it: the launch failure is recorded as a
|
|
88
|
+
// VALUE on the dispatch record (`status: 'failed'` + `spawnError`), which
|
|
89
|
+
// is the surface a reader should consult. Absence in the pid file is the
|
|
90
|
+
// one encoding that cannot be misread as a pid.
|
|
91
|
+
if (child.pid !== undefined) {
|
|
92
|
+
writeFileSync(join(dir, 'pid'), String(child.pid));
|
|
93
|
+
}
|
|
52
94
|
|
|
53
95
|
return {
|
|
54
96
|
pid: child.pid ?? -1,
|
|
55
97
|
child,
|
|
56
98
|
kill: (signal: NodeJS.Signals = 'SIGTERM') => child.kill(signal),
|
|
99
|
+
settled,
|
|
57
100
|
};
|
|
58
101
|
}
|
|
59
102
|
}
|
|
@@ -7,7 +7,7 @@ export function detectBinaryInstalled(binary: string): Promise<boolean> {
|
|
|
7
7
|
const locator = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
8
8
|
|
|
9
9
|
return new Promise((resolve) => {
|
|
10
|
-
execFile(locator, [binary], { timeout: DETECT_TIMEOUT_MS }, (error, stdout) => {
|
|
10
|
+
execFile(locator, [binary], { timeout: DETECT_TIMEOUT_MS, windowsHide: true }, (error, stdout) => {
|
|
11
11
|
resolve(error === null && stdout.trim().length > 0);
|
|
12
12
|
});
|
|
13
13
|
});
|