@crouton-kit/crouter 0.3.18 → 0.3.19
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/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +3 -2
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +2 -0
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +607 -61
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/loader.d.ts +7 -0
- package/dist/core/personas/loader.js +36 -5
- package/dist/core/personas/resolve.js +13 -3
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +32 -14
- package/dist/core/runtime/placement.js +206 -52
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +5 -0
- package/dist/core/spawn.js +20 -2
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +87 -23
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
// fake-engine.ts — a deterministic in-process stand-in for the pi SDK engine,
|
|
2
|
+
// the SDK analog of fixtures/fake-pi-host.ts.
|
|
3
|
+
//
|
|
4
|
+
// This is NOT an LLM and NOT a mock of the canvas extensions. It occupies the
|
|
5
|
+
// pi SDK's place INSIDE the REAL broker (src/core/runtime/broker.ts): the broker
|
|
6
|
+
// loads it through the `CRTR_BROKER_ENGINE` seam (broker-sdk.ts `loadBrokerEngine`),
|
|
7
|
+
// then drives it exactly as it drives the real `@earendil-works/pi-coding-agent`
|
|
8
|
+
// engine — SessionManager.open/create, createAgentSession, session.bindExtensions,
|
|
9
|
+
// session.subscribe, session.prompt. We load the REAL `-e` canvas extensions (the
|
|
10
|
+
// modules the runtime put in the node's broker-launch.json) and fire REAL
|
|
11
|
+
// lifecycle events under harness control, so the real hooks (canvas-stophook,
|
|
12
|
+
// canvas-inbox-watcher) and the real broker drive real canvas state — across the
|
|
13
|
+
// detached broker process boundary, with NOTHING in the broker mocked.
|
|
14
|
+
//
|
|
15
|
+
// It is run UNDER tsx in the broker process (the harness puts `--import tsx/esm`
|
|
16
|
+
// in the broker's NODE_OPTIONS), so the `.ts` extension paths and this `.ts`
|
|
17
|
+
// module both resolve. By the time the broker calls our SessionManager/
|
|
18
|
+
// createAgentSession, piInvocationToSdkConfig has merged inv.env into
|
|
19
|
+
// process.env, so CRTR_NODE_ID / CRTR_HOME are set — that is how we find the
|
|
20
|
+
// node dir.
|
|
21
|
+
//
|
|
22
|
+
// Proof / control surface — IDENTICAL filenames to fake-pi-host.ts, so the
|
|
23
|
+
// EXISTING harness observers (awaitBoot, awaitWake, eventCount, bootCount,
|
|
24
|
+
// injected) work for broker nodes UNCHANGED:
|
|
25
|
+
// <nodeDir>/fake-pi.boot.json — argv + env + loaded exts (latest boot)
|
|
26
|
+
// <nodeDir>/fake-pi.boots.jsonl — append-only, one line per boot (broker pid)
|
|
27
|
+
// <nodeDir>/fake-pi.events.jsonl — append-only, one line per fired event
|
|
28
|
+
// <nodeDir>/fake-pi.injected.jsonl — every sendUserMessage (inbox-watcher wake)
|
|
29
|
+
// <nodeDir>/fake-pi.error — any boot/import failure
|
|
30
|
+
// <nodeDir>/fake-pi.cmd — harness writes ONE JSON command (turn|stop|dialog)
|
|
31
|
+
// <nodeDir>/fake-pi.dialog.jsonl — one line per resolved unattended dialog (§5.4)
|
|
32
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync, } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { randomUUID } from 'node:crypto';
|
|
35
|
+
import { DefaultResourceLoader, getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
36
|
+
/** Test seam (M-1 regression): a FRESH-start kickoff prompt carrying this token
|
|
37
|
+
* makes the fake engine THROW inside bindExtensions BEFORE it fires
|
|
38
|
+
* session_start — the exact failure mode that records no pid and no session, so
|
|
39
|
+
* the broker exits(1) and the daemon must surface a boot failure rather than
|
|
40
|
+
* strand the node. Carried on the prompt (not a global env) so it is per-spawn. */
|
|
41
|
+
export const FAIL_BEFORE_SESSION_START = '__FAIL_BEFORE_SESSION_START__';
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// node dir / proof helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
function nodeDirFromEnv() {
|
|
46
|
+
const home = (process.env['CRTR_HOME'] ?? '').trim();
|
|
47
|
+
const nodeId = (process.env['CRTR_NODE_ID'] ?? '').trim();
|
|
48
|
+
if (home === '' || nodeId === '') {
|
|
49
|
+
throw new Error('[fake-engine] CRTR_HOME / CRTR_NODE_ID missing in env');
|
|
50
|
+
}
|
|
51
|
+
const dir = join(home, 'nodes', nodeId);
|
|
52
|
+
mkdirSync(dir, { recursive: true });
|
|
53
|
+
return dir;
|
|
54
|
+
}
|
|
55
|
+
function recordError(dir, msg) {
|
|
56
|
+
try {
|
|
57
|
+
appendFileSync(join(dir, 'fake-pi.error'), msg + '\n');
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* best effort */
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
process.stderr.write('[fake-engine] ' + msg + '\n');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* ignore */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// SessionManager — the broker calls .open(path) (resume) or .create(cwd) (fresh)
|
|
71
|
+
// and hands the result to createAgentSession. sessionId/sessionFile round-trip
|
|
72
|
+
// through it, so a resume (`--session <pi_session_file>`) re-opens the SAME
|
|
73
|
+
// .jsonl the fresh start created — the load-bearing identity for revive.
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
export class SessionManager {
|
|
76
|
+
sessionId;
|
|
77
|
+
sessionFile;
|
|
78
|
+
resumed;
|
|
79
|
+
constructor(sessionId, sessionFile, resumed) {
|
|
80
|
+
this.sessionId = sessionId;
|
|
81
|
+
this.sessionFile = sessionFile;
|
|
82
|
+
this.resumed = resumed;
|
|
83
|
+
}
|
|
84
|
+
/** Fresh start: a brand-new session whose .jsonl lives under the node dir
|
|
85
|
+
* (created empty if missing, like fake-pi-host). */
|
|
86
|
+
static create(_cwd) {
|
|
87
|
+
const dir = nodeDirFromEnv();
|
|
88
|
+
const sessionFile = join(dir, 'fake-session.jsonl');
|
|
89
|
+
if (!existsSync(sessionFile))
|
|
90
|
+
writeFileSync(sessionFile, '');
|
|
91
|
+
return new SessionManager(`fake-sess-${randomUUID()}`, sessionFile, false);
|
|
92
|
+
}
|
|
93
|
+
/** Resume: round-trip the .jsonl PATH the revive passed via `--session`. */
|
|
94
|
+
static open(path) {
|
|
95
|
+
return new SessionManager(`fake-sess-resume-${randomUUID()}`, path, true);
|
|
96
|
+
}
|
|
97
|
+
getSessionId() {
|
|
98
|
+
return this.sessionId;
|
|
99
|
+
}
|
|
100
|
+
getSessionFile() {
|
|
101
|
+
return this.sessionFile;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// AgentSessionEvent stream synthesis (T8 / G1, G3, G4, G8). The fake emits a
|
|
106
|
+
// REALISTIC streaming assistant turn through session.subscribe — the SAME channel
|
|
107
|
+
// the real pi engine feeds the broker's fan-out — so the frames a `crtr attach`
|
|
108
|
+
// client receives are byte-for-byte real AgentSessionEvents. The EMITTED event
|
|
109
|
+
// shapes are typed against the real `AgentSessionEvent` union from pi (whose full
|
|
110
|
+
// 17 variants are message_start/update/end, tool_execution_start/update/end,
|
|
111
|
+
// agent_start/end, turn_start/end, queue_update, compaction_start/end, auto_retry_
|
|
112
|
+
// start/end, session_info_changed, thinking_level_changed) — emit() takes that
|
|
113
|
+
// union, so a drift in the shape of any variant emitTurn() constructs (the
|
|
114
|
+
// streaming subset: the message_*, tool_execution_*, agent/turn start+end events)
|
|
115
|
+
// fails the build, not a test.
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
/** A minimal but fully-typed AssistantMessage. `pad` bytes of filler text let a
|
|
118
|
+
* test size a single frame to multiple MiB (G8 backpressure flood). */
|
|
119
|
+
function assistantMessage(text, pad = 0) {
|
|
120
|
+
return {
|
|
121
|
+
role: 'assistant',
|
|
122
|
+
content: [{ type: 'text', text: pad > 0 ? `${text} ${'x'.repeat(pad)}` : text }],
|
|
123
|
+
api: 'anthropic-messages',
|
|
124
|
+
provider: 'anthropic',
|
|
125
|
+
model: 'fake-sonnet',
|
|
126
|
+
usage: {
|
|
127
|
+
input: 10,
|
|
128
|
+
output: 5,
|
|
129
|
+
cacheRead: 0,
|
|
130
|
+
cacheWrite: 0,
|
|
131
|
+
totalTokens: 15,
|
|
132
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
133
|
+
},
|
|
134
|
+
stopReason: 'stop',
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** A streaming text-delta event (the `assistantMessageEvent` carried by a
|
|
139
|
+
* message_update). Kept small (the big payload rides in message_update.message). */
|
|
140
|
+
function textDelta(partial, delta) {
|
|
141
|
+
return { type: 'text_delta', contentIndex: 0, delta, partial };
|
|
142
|
+
}
|
|
143
|
+
class FakeSession {
|
|
144
|
+
sm;
|
|
145
|
+
loader;
|
|
146
|
+
dir;
|
|
147
|
+
extensions = [];
|
|
148
|
+
injected = [];
|
|
149
|
+
uiContext;
|
|
150
|
+
shutdownHandler;
|
|
151
|
+
streaming = false;
|
|
152
|
+
disposed = false;
|
|
153
|
+
eventSeq = 0;
|
|
154
|
+
timer;
|
|
155
|
+
// The broker's fan-out subscribers (broker.ts `session.subscribe(...)`) and the
|
|
156
|
+
// accumulating message history the catch-up snapshot serializes (G3).
|
|
157
|
+
listeners = new Set();
|
|
158
|
+
messageLog = [];
|
|
159
|
+
constructor(sm, loader) {
|
|
160
|
+
this.sm = sm;
|
|
161
|
+
this.loader = loader;
|
|
162
|
+
this.dir = nodeDirFromEnv();
|
|
163
|
+
}
|
|
164
|
+
// --- broker-read getters. buildSnapshot (on a client hello) reads messages/
|
|
165
|
+
// stats/model: T8 clients attach and assert on welcome.snapshot, so messages
|
|
166
|
+
// returns the accrued history; stats/model stay cheap stubs. ---------------
|
|
167
|
+
get messages() {
|
|
168
|
+
return this.messageLog;
|
|
169
|
+
}
|
|
170
|
+
get sessionId() {
|
|
171
|
+
return this.sm.getSessionId();
|
|
172
|
+
}
|
|
173
|
+
get sessionFile() {
|
|
174
|
+
return this.sm.getSessionFile();
|
|
175
|
+
}
|
|
176
|
+
get model() {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
get isStreaming() {
|
|
180
|
+
return this.streaming;
|
|
181
|
+
}
|
|
182
|
+
getSessionStats() {
|
|
183
|
+
return { messages: 0, tokens: 0 };
|
|
184
|
+
}
|
|
185
|
+
// --- naming is cosmetic (broker calls it in a try/catch) ---------------------
|
|
186
|
+
setSessionName(_name) {
|
|
187
|
+
/* no-op recording stub */
|
|
188
|
+
}
|
|
189
|
+
// --- controller-drive stubs (no controller connects in these tests) ----------
|
|
190
|
+
prompt(text, _options) {
|
|
191
|
+
// The broker calls this for BOTH the fresh-start kickoff AND a controller's
|
|
192
|
+
// `prompt` frame (G1/G4). Either way emit a realistic streaming turn on the
|
|
193
|
+
// subscribe channel; the broker fans it out to every attached viewer VERBATIM.
|
|
194
|
+
// This is the SUBSCRIBE channel only — it does NOT run the canvas stophook
|
|
195
|
+
// (that fires via the separate extension-handler channel in dispatch()/fire()),
|
|
196
|
+
// so it never tears the broker down. The broker fire-and-forgets the promise.
|
|
197
|
+
return this.emitTurn(text);
|
|
198
|
+
}
|
|
199
|
+
steer(_text) {
|
|
200
|
+
return Promise.resolve();
|
|
201
|
+
}
|
|
202
|
+
followUp(_text) {
|
|
203
|
+
return Promise.resolve();
|
|
204
|
+
}
|
|
205
|
+
abort() {
|
|
206
|
+
return Promise.resolve();
|
|
207
|
+
}
|
|
208
|
+
// --- the single engine event stream the broker fans out (broker.ts subscribes
|
|
209
|
+
// here and broadcasts each event VERBATIM to every attached viewer) --------
|
|
210
|
+
subscribe(listener) {
|
|
211
|
+
const l = listener;
|
|
212
|
+
this.listeners.add(l);
|
|
213
|
+
return () => {
|
|
214
|
+
this.listeners.delete(l);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** Fan one typed AgentSessionEvent out to every broker subscriber. A throwing
|
|
218
|
+
* listener is recorded, never propagated (mirrors the broker's m7 try/catch). */
|
|
219
|
+
emit(event) {
|
|
220
|
+
for (const l of this.listeners) {
|
|
221
|
+
try {
|
|
222
|
+
l(event);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
recordError(this.dir, `subscribe listener threw: ${String(e)}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Emit a realistic streaming assistant turn on the subscribe channel:
|
|
230
|
+
* agent_start → turn_start → message_start → message_update×N →
|
|
231
|
+
* tool_execution_start/update/end → message_end → turn_end → agent_end. Drives
|
|
232
|
+
* G1 (controller prompt relay), G3 (produce-while-detached, accrues messageLog),
|
|
233
|
+
* and G8 (a fast event stream — `updates`/`padBytes` size the flood that sheds a
|
|
234
|
+
* stalled viewer at the broker HWM). A per-update setImmediate yield lets a
|
|
235
|
+
* fast viewer drain between frames so only the stalled one trips the HWM. */
|
|
236
|
+
async emitTurn(text, opts = {}) {
|
|
237
|
+
if (this.disposed)
|
|
238
|
+
return;
|
|
239
|
+
const updates = opts.updates ?? 3;
|
|
240
|
+
const pad = opts.padBytes ?? 0;
|
|
241
|
+
const withTool = opts.tool ?? true;
|
|
242
|
+
const small = assistantMessage(text);
|
|
243
|
+
// A normal turn yields with setImmediate (fast). A sized flood (G8, pad>0) is
|
|
244
|
+
// PACED with a small real delay so a fast (reading) viewer always drains each
|
|
245
|
+
// frame and stays under the HWM, while a stalled (non-reading) viewer's backlog
|
|
246
|
+
// still climbs monotonically past the 32 MiB byte cap and is shed. Without the
|
|
247
|
+
// pace the burst outruns the reader's event loop and sheds the fast viewer too.
|
|
248
|
+
const yieldTick = () => pad > 0 ? new Promise((r) => setTimeout(r, 3)) : new Promise((r) => setImmediate(r));
|
|
249
|
+
this.streaming = true;
|
|
250
|
+
this.emit({ type: 'agent_start' });
|
|
251
|
+
this.emit({ type: 'turn_start' });
|
|
252
|
+
this.emit({ type: 'message_start', message: small });
|
|
253
|
+
// F2 (message_update coalescing): the broker now coalesces message_update
|
|
254
|
+
// relays (latest-wins, ~75 ms window), so a padded flood riding message_update
|
|
255
|
+
// can no longer climb a stalled viewer's backlog — the coalescer would shed
|
|
256
|
+
// the flood before the HWM does. A sized flood (pad>0, G8) therefore rides
|
|
257
|
+
// tool_execution_update frames, which the broker relays VERBATIM (only
|
|
258
|
+
// message_update is coalesced — tool output streams remain the realistic
|
|
259
|
+
// unbounded-backlog vector M1 guards against). The flood gets its own
|
|
260
|
+
// tool_execution_start so the frame sequence stays well-formed.
|
|
261
|
+
const floodToolCallId = pad > 0 ? `flood-${++this.eventSeq}` : undefined;
|
|
262
|
+
if (floodToolCallId !== undefined) {
|
|
263
|
+
this.emit({
|
|
264
|
+
type: 'tool_execution_start',
|
|
265
|
+
toolCallId: floodToolCallId,
|
|
266
|
+
toolName: 'read',
|
|
267
|
+
args: { path: 'flood.txt' },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
for (let i = 0; i < updates; i++) {
|
|
271
|
+
if (this.disposed)
|
|
272
|
+
return;
|
|
273
|
+
this.emit({
|
|
274
|
+
type: 'message_update',
|
|
275
|
+
message: assistantMessage(`${text} [${i + 1}/${updates}]`),
|
|
276
|
+
assistantMessageEvent: textDelta(small, `chunk ${i + 1}`),
|
|
277
|
+
});
|
|
278
|
+
if (floodToolCallId !== undefined) {
|
|
279
|
+
// The big payload (G8) rides the partialResult; ~padBytes per frame.
|
|
280
|
+
this.emit({
|
|
281
|
+
type: 'tool_execution_update',
|
|
282
|
+
toolCallId: floodToolCallId,
|
|
283
|
+
toolName: 'read',
|
|
284
|
+
args: { path: 'flood.txt' },
|
|
285
|
+
partialResult: { chunk: 'x'.repeat(pad) },
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
await yieldTick();
|
|
289
|
+
}
|
|
290
|
+
if (floodToolCallId !== undefined) {
|
|
291
|
+
this.emit({
|
|
292
|
+
type: 'tool_execution_end',
|
|
293
|
+
toolCallId: floodToolCallId,
|
|
294
|
+
toolName: 'read',
|
|
295
|
+
result: { ok: true },
|
|
296
|
+
isError: false,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (withTool) {
|
|
300
|
+
const toolCallId = `call-${++this.eventSeq}`;
|
|
301
|
+
this.emit({ type: 'tool_execution_start', toolCallId, toolName: 'read', args: { path: 'README.md' } });
|
|
302
|
+
this.emit({
|
|
303
|
+
type: 'tool_execution_update',
|
|
304
|
+
toolCallId,
|
|
305
|
+
toolName: 'read',
|
|
306
|
+
args: { path: 'README.md' },
|
|
307
|
+
partialResult: { bytes: 42 },
|
|
308
|
+
});
|
|
309
|
+
this.emit({ type: 'tool_execution_end', toolCallId, toolName: 'read', result: { ok: true }, isError: false });
|
|
310
|
+
}
|
|
311
|
+
if (this.disposed)
|
|
312
|
+
return;
|
|
313
|
+
const finalMsg = assistantMessage(text);
|
|
314
|
+
this.emit({ type: 'message_end', message: finalMsg });
|
|
315
|
+
this.emit({ type: 'turn_end', message: finalMsg, toolResults: [] });
|
|
316
|
+
this.messageLog.push(finalMsg);
|
|
317
|
+
this.streaming = false;
|
|
318
|
+
// agent_end on the SUBSCRIBE channel is a relayed frame ONLY (willRetry=false);
|
|
319
|
+
// it never runs the stophook, so the broker stays alive (G2/G3/G4/G8).
|
|
320
|
+
this.emit({ type: 'agent_end', messages: this.messageLog.slice(), willRetry: false });
|
|
321
|
+
}
|
|
322
|
+
dispose() {
|
|
323
|
+
this.disposed = true;
|
|
324
|
+
if (this.timer !== undefined)
|
|
325
|
+
clearInterval(this.timer);
|
|
326
|
+
}
|
|
327
|
+
// --- bindExtensions: drive the loader's ALREADY-loaded extensions. The REAL
|
|
328
|
+
// DefaultResourceLoader.reload() (broker.ts, BEFORE createAgentSession) has
|
|
329
|
+
// already imported the `-e` canvas extensions via jiti and called each
|
|
330
|
+
// default(pi) against the SDK's `notInitialized` runtime — registering their
|
|
331
|
+
// handlers but leaving every action method (pi.sendUserMessage, …) a stub
|
|
332
|
+
// that THROWS during load. We must NOT re-import them: a native import() is a
|
|
333
|
+
// jiti-independent SECOND module instance, so the inbox-watcher's module-
|
|
334
|
+
// level timer would run TWICE — and the reload() copy (bound to the throwing
|
|
335
|
+
// runtime) wins the race for the shared on-disk inbox cursor, consuming the
|
|
336
|
+
// entry and never delivering. Instead we replicate the minimal slice of
|
|
337
|
+
// agent-session's bindCore: swap the shared runtime's action methods IN PLACE
|
|
338
|
+
// so the real handlers' pi.sendUserMessage delivers into our proof files,
|
|
339
|
+
// then emit session_start into their handler maps so the REAL canvas-stophook
|
|
340
|
+
// records pi_session_id/file + recordPid(process.pid=broker pid). The broker's
|
|
341
|
+
// shutdownHandler is wired into ctx.shutdown so the stophook's done/idle-
|
|
342
|
+
// release/refresh branches exit the broker. -------------------------------
|
|
343
|
+
async bindExtensions(opts) {
|
|
344
|
+
this.uiContext = opts.uiContext;
|
|
345
|
+
this.shutdownHandler = opts.shutdownHandler;
|
|
346
|
+
const { argv, extPaths, label, prompt } = readLaunch(this.dir);
|
|
347
|
+
const resuming = this.sm.resumed;
|
|
348
|
+
// M-1 regression seam: simulate a broker that throws BEFORE session_start
|
|
349
|
+
// (no pid, no session ever recorded). Only a fresh start carries the kickoff
|
|
350
|
+
// prompt; the throw propagates out of bindExtensions → runBroker rejects →
|
|
351
|
+
// broker-cli's fatal catch logs to job/broker.log + exit(1).
|
|
352
|
+
if (!resuming && (prompt ?? '').includes(FAIL_BEFORE_SESSION_START)) {
|
|
353
|
+
throw new Error('[fake-engine] simulated pre-session_start boot failure');
|
|
354
|
+
}
|
|
355
|
+
// bindCore (minimal): adopt the loader's extensions + their shared runtime, and
|
|
356
|
+
// replace the throwing notInitialized action methods with working ones. The
|
|
357
|
+
// handlers themselves stay the REAL ones the SDK registered during reload().
|
|
358
|
+
const loaded = [];
|
|
359
|
+
const failedExt = [];
|
|
360
|
+
try {
|
|
361
|
+
const result = this.loader?.getExtensions();
|
|
362
|
+
this.extensions = result?.extensions ?? [];
|
|
363
|
+
for (const e of this.extensions)
|
|
364
|
+
loaded.push(e.path);
|
|
365
|
+
for (const err of result?.errors ?? []) {
|
|
366
|
+
failedExt.push(String(err?.error ?? err));
|
|
367
|
+
}
|
|
368
|
+
const rt = result?.runtime;
|
|
369
|
+
if (rt !== undefined) {
|
|
370
|
+
rt.assertActive = () => { };
|
|
371
|
+
rt.sendUserMessage = (content, options) => this.recordInjected({ content, deliverAs: options?.deliverAs });
|
|
372
|
+
rt.sendMessage = (message, options) => this.recordInjected({ content: JSON.stringify(message), deliverAs: options?.deliverAs });
|
|
373
|
+
rt.setSessionName = () => { };
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
recordError(this.dir, 'bindExtensions: resourceLoader.getExtensions() returned no runtime');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
recordError(this.dir, `bindExtensions getExtensions/bindCore failed: ${String(e)}`);
|
|
381
|
+
}
|
|
382
|
+
const ctx = this.buildCtx();
|
|
383
|
+
// session_start: the real boot-confirm hook captures session id/file + the
|
|
384
|
+
// broker's pid (recordPid(process.pid)) and clears any pending refresh.
|
|
385
|
+
await this.fire('session_start', { reason: resuming ? 'resume' : 'startup' }, ctx);
|
|
386
|
+
// The boot proof the harness asserts on (same shape as fake-pi-host's boot).
|
|
387
|
+
const env = process.env;
|
|
388
|
+
const boot = {
|
|
389
|
+
pid: process.pid, // the broker pid (recorded as pi_pid by the stophook)
|
|
390
|
+
nodeId: (env['CRTR_NODE_ID'] ?? '').trim(),
|
|
391
|
+
home: (env['CRTR_HOME'] ?? '').trim(),
|
|
392
|
+
rawArgv: argv,
|
|
393
|
+
extPaths,
|
|
394
|
+
loaded,
|
|
395
|
+
failedExt,
|
|
396
|
+
sessionId: this.sm.getSessionId(),
|
|
397
|
+
sessionFile: this.sm.getSessionFile(),
|
|
398
|
+
resuming,
|
|
399
|
+
label: label ?? null,
|
|
400
|
+
prompt: prompt ?? null,
|
|
401
|
+
env: {
|
|
402
|
+
CRTR_NODE_ID: env['CRTR_NODE_ID'] ?? null,
|
|
403
|
+
CRTR_KIND: env['CRTR_KIND'] ?? null,
|
|
404
|
+
CRTR_MODE: env['CRTR_MODE'] ?? null,
|
|
405
|
+
CRTR_LIFECYCLE: env['CRTR_LIFECYCLE'] ?? null,
|
|
406
|
+
CRTR_NODE_CWD: env['CRTR_NODE_CWD'] ?? null,
|
|
407
|
+
CRTR_HOME: env['CRTR_HOME'] ?? null,
|
|
408
|
+
CRTR_PARENT_NODE_ID: env['CRTR_PARENT_NODE_ID'] ?? null,
|
|
409
|
+
CRTR_ROOT_SESSION: env['CRTR_ROOT_SESSION'] ?? null,
|
|
410
|
+
CRTR_SUBTREE: env['CRTR_SUBTREE'] ?? null,
|
|
411
|
+
CRTR_FRONT_DOOR: env['CRTR_FRONT_DOOR'] ?? null,
|
|
412
|
+
},
|
|
413
|
+
injectedDuringBoot: this.injected.slice(),
|
|
414
|
+
};
|
|
415
|
+
writeFileSync(join(this.dir, 'fake-pi.boot.json'), JSON.stringify(boot, null, 2));
|
|
416
|
+
try {
|
|
417
|
+
appendFileSync(join(this.dir, 'fake-pi.boots.jsonl'), JSON.stringify(boot) + '\n');
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
/* best effort */
|
|
421
|
+
}
|
|
422
|
+
// Control loop: poll for one harness command at a time (turn | stop | dialog).
|
|
423
|
+
this.timer = setInterval(() => {
|
|
424
|
+
void this.step().catch((e) => recordError(this.dir, `step: ${String(e)}`));
|
|
425
|
+
}, 100);
|
|
426
|
+
if (typeof this.timer.unref === 'function')
|
|
427
|
+
this.timer.unref();
|
|
428
|
+
}
|
|
429
|
+
// --- record an injected user message (the inbox-watcher wake proof). Called by
|
|
430
|
+
// the runtime's swapped-in sendUserMessage/sendMessage. ---------------------
|
|
431
|
+
recordInjected(rec) {
|
|
432
|
+
this.injected.push(rec);
|
|
433
|
+
try {
|
|
434
|
+
appendFileSync(join(this.dir, 'fake-pi.injected.jsonl'), JSON.stringify({ ...rec, ts: Date.now() }) + '\n');
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
/* best effort */
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// --- the ctx passed to fired handlers (union across both focus extensions) ---
|
|
441
|
+
buildCtx() {
|
|
442
|
+
return {
|
|
443
|
+
sessionManager: {
|
|
444
|
+
getSessionId: () => this.sm.getSessionId(),
|
|
445
|
+
getSessionFile: () => this.sm.getSessionFile(),
|
|
446
|
+
},
|
|
447
|
+
getContextUsage: () => ({ tokens: 1000 }),
|
|
448
|
+
// Wired to the broker's shutdownHandler: the stophook calls ctx.shutdown()
|
|
449
|
+
// in exactly its done / idle-release / refresh branches → disposeAndExit.
|
|
450
|
+
shutdown: () => {
|
|
451
|
+
this.shutdownHandler?.();
|
|
452
|
+
},
|
|
453
|
+
isIdle: () => !this.streaming,
|
|
454
|
+
abort: () => { },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
// Append a durable record of every fired event BEFORE its handlers run — the
|
|
458
|
+
// harness's robust "the broker received my command and is dispatching it"
|
|
459
|
+
// signal, surviving a handler that exits the process (agent_end → ctx.shutdown
|
|
460
|
+
// → broker disposeAndExit → process.exit).
|
|
461
|
+
recordEvent(event, ev) {
|
|
462
|
+
try {
|
|
463
|
+
const reason = ev?.reason;
|
|
464
|
+
appendFileSync(join(this.dir, 'fake-pi.events.jsonl'), JSON.stringify({ seq: ++this.eventSeq, event, reason: reason ?? null, ts: Date.now() }) + '\n');
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
/* best effort */
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async fire(event, ev, ctx) {
|
|
471
|
+
this.recordEvent(event, ev);
|
|
472
|
+
// Dispatch into the loader's REAL extension handler maps (the same handlers
|
|
473
|
+
// the SDK's reload() registered as each extension called pi.on(event, …)).
|
|
474
|
+
for (const ext of this.extensions) {
|
|
475
|
+
for (const h of ext.handlers.get(event) ?? []) {
|
|
476
|
+
try {
|
|
477
|
+
await h(ev, ctx);
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
recordError(this.dir, `handler ${event} threw: ${String(e)}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// --- the command channel: one JSON command at a time, atomically renamed in --
|
|
486
|
+
async step() {
|
|
487
|
+
if (this.disposed)
|
|
488
|
+
return;
|
|
489
|
+
const cmdFile = join(this.dir, 'fake-pi.cmd');
|
|
490
|
+
if (!existsSync(cmdFile))
|
|
491
|
+
return;
|
|
492
|
+
let cmd = null;
|
|
493
|
+
try {
|
|
494
|
+
cmd = JSON.parse(readFileSync(cmdFile, 'utf8'));
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
cmd = null;
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
unlinkSync(cmdFile);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
/* ignore */
|
|
504
|
+
}
|
|
505
|
+
if (cmd)
|
|
506
|
+
await this.dispatch(cmd);
|
|
507
|
+
}
|
|
508
|
+
async dispatch(cmd) {
|
|
509
|
+
const ctx = this.buildCtx();
|
|
510
|
+
switch (cmd.cmd) {
|
|
511
|
+
case 'stream':
|
|
512
|
+
// Drive a subscribe-channel streaming turn WITHOUT a controller (G3:
|
|
513
|
+
// produce-while-detached) or with sized frames (G8: a fast event stream
|
|
514
|
+
// that sheds a stalled viewer at the HWM). Pure fan-out — no stophook, no
|
|
515
|
+
// exit — so the broker stays alive.
|
|
516
|
+
await this.emitTurn(cmd.text ?? 'streamed turn', {
|
|
517
|
+
updates: cmd.updates,
|
|
518
|
+
padBytes: cmd.padBytes,
|
|
519
|
+
tool: cmd.tool,
|
|
520
|
+
});
|
|
521
|
+
break;
|
|
522
|
+
case 'turn':
|
|
523
|
+
this.streaming = true;
|
|
524
|
+
await this.fire('agent_start', {}, ctx);
|
|
525
|
+
this.streaming = false;
|
|
526
|
+
await this.fire('turn_end', { message: { role: 'assistant', usage: { input: 10, output: 5 }, model: 'fake' } }, ctx);
|
|
527
|
+
await this.fire('agent_end', { messages: [{ role: 'assistant', stopReason: 'stop', content: [{ type: 'text', text: cmd.text ?? '' }] }] }, ctx);
|
|
528
|
+
break;
|
|
529
|
+
case 'stop':
|
|
530
|
+
this.streaming = false;
|
|
531
|
+
await this.fire('agent_end', {
|
|
532
|
+
messages: [
|
|
533
|
+
{ role: 'assistant', stopReason: cmd.reason ?? 'stop', content: [{ type: 'text', text: cmd.text ?? '' }] },
|
|
534
|
+
],
|
|
535
|
+
}, ctx);
|
|
536
|
+
break;
|
|
537
|
+
case 'dialog':
|
|
538
|
+
// Raise a blocking confirm() through the broker's REAL uiContext. The path
|
|
539
|
+
// taken depends on who is attached when it raises:
|
|
540
|
+
// • ZERO viewers (C2 / G6a): makeBrokerUiContext resolves the default
|
|
541
|
+
// (false) IMMEDIATELY — noOp fallback, no timer, no wait, no deadlock.
|
|
542
|
+
// • a controller attached (G5): forwarded as an extension_ui_request the
|
|
543
|
+
// controller answers via extension_ui_response — the engine proceeds.
|
|
544
|
+
// • a controller attached but silent (G6b): resolves on the broker-side
|
|
545
|
+
// timeout. Pass a SHORT explicit per-dialog `timeout` so that path is
|
|
546
|
+
// fast and deterministic (never the broker's 120s default).
|
|
547
|
+
await this.runDialog(cmd.timeout ?? 0);
|
|
548
|
+
break;
|
|
549
|
+
default:
|
|
550
|
+
recordError(this.dir, `unknown cmd: ${String(cmd.cmd)}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async runDialog(timeout) {
|
|
554
|
+
const start = Date.now();
|
|
555
|
+
try {
|
|
556
|
+
const resolved = await Promise.resolve(this.uiContext?.confirm?.('headless-dialog', 'forward-progress check', { timeout }));
|
|
557
|
+
appendFileSync(join(this.dir, 'fake-pi.dialog.jsonl'), JSON.stringify({ resolved: resolved ?? null, ms: Date.now() - start, ts: Date.now() }) + '\n');
|
|
558
|
+
}
|
|
559
|
+
catch (e) {
|
|
560
|
+
recordError(this.dir, `dialog: ${String(e)}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
export async function createAgentSessionServices(options) {
|
|
565
|
+
const loader = new DefaultResourceLoader({
|
|
566
|
+
cwd: options.cwd,
|
|
567
|
+
agentDir: options.agentDir ?? getAgentDir(),
|
|
568
|
+
additionalExtensionPaths: options.resourceLoaderOptions?.additionalExtensionPaths,
|
|
569
|
+
appendSystemPrompt: options.resourceLoaderOptions?.appendSystemPrompt,
|
|
570
|
+
});
|
|
571
|
+
await loader.reload();
|
|
572
|
+
return {
|
|
573
|
+
resourceLoader: loader,
|
|
574
|
+
modelRegistry: { find: () => undefined },
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
export async function createAgentSessionFromServices(options) {
|
|
578
|
+
return { session: new FakeSession(options.sessionManager, options.services.resourceLoader) };
|
|
579
|
+
}
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// VERSION — set to the SDK's published version so assertEngineVersion (which
|
|
582
|
+
// only logs, never throws) emits no spurious mismatch warning.
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
export const VERSION = '0.78.1';
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// Parse the broker-launch.json recipe the host serialized (the PiInvocation
|
|
587
|
+
// buildPiArgv produced) for the proof shape — the same `-e`/`-n`/positional
|
|
588
|
+
// vocabulary fake-pi-host parses off its argv.
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
function readLaunch(dir) {
|
|
591
|
+
let argv = [];
|
|
592
|
+
try {
|
|
593
|
+
const inv = JSON.parse(readFileSync(join(dir, 'broker-launch.json'), 'utf8'));
|
|
594
|
+
argv = Array.isArray(inv.argv) ? inv.argv : [];
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
/* no launch file — leave argv empty */
|
|
598
|
+
}
|
|
599
|
+
const extPaths = [];
|
|
600
|
+
let label;
|
|
601
|
+
let prompt;
|
|
602
|
+
for (let i = 0; i < argv.length; i++) {
|
|
603
|
+
const a = argv[i];
|
|
604
|
+
if (a === '-e')
|
|
605
|
+
extPaths.push(argv[++i]);
|
|
606
|
+
else if (a === '-n')
|
|
607
|
+
label = argv[++i];
|
|
608
|
+
else if (a === '--session' || a === '--model' || a === '--tools' || a === '--append-system-prompt' || a === '--fork')
|
|
609
|
+
i++;
|
|
610
|
+
else if (!a.startsWith('-'))
|
|
611
|
+
prompt = a; // positional kickoff (fresh start only)
|
|
612
|
+
}
|
|
613
|
+
return { argv, extPaths, label, prompt };
|
|
614
|
+
}
|
|
@@ -212,6 +212,7 @@ const boot = {
|
|
|
212
212
|
CRTR_HOME: env['CRTR_HOME'] ?? null,
|
|
213
213
|
CRTR_PARENT_NODE_ID: env['CRTR_PARENT_NODE_ID'] ?? null,
|
|
214
214
|
CRTR_ROOT_SESSION: env['CRTR_ROOT_SESSION'] ?? null,
|
|
215
|
+
CRTR_SUBTREE: env['CRTR_SUBTREE'] ?? null,
|
|
215
216
|
CRTR_FRONT_DOOR: env['CRTR_FRONT_DOOR'] ?? null,
|
|
216
217
|
},
|
|
217
218
|
injectedDuringBoot: injected.slice(),
|
|
@@ -36,7 +36,13 @@
|
|
|
36
36
|
// pane-alive but pi-dead) — unit: daemon-liveness.test.ts "idle-release +
|
|
37
37
|
// live (frozen) pane …"; the grace-window double-spawn guard around it is
|
|
38
38
|
// exercised faithfully in grace-clock.test.ts.
|
|
39
|
-
// • node lifecycle --detach (A3: orphaned-focus-row hazard) —
|
|
39
|
+
// • node lifecycle --detach (A3: orphaned-focus-row hazard) — faithful E2E:
|
|
40
|
+
// detach-focus.test.ts (the real verb on a FOCUSED live node → terminal +
|
|
41
|
+
// pane relocated to the backstage + focus row CLOSED).
|
|
42
|
+
// • focused-finish → manager-TAKEOVER (handFocusToManager swap) — NOT harness-
|
|
43
|
+
// reachable: the harness root is a paneless never-booted row, so the done-
|
|
44
|
+
// branch only ever hits the paneless-manager false guard (closeFocusToShell),
|
|
45
|
+
// never a live/dormant-idle-release takeover. Unit: placement-teardown.test.ts.
|
|
40
46
|
// • node msg / focus / cycle wake of a dormant node (A7) — untested faithfully.
|
|
41
47
|
// These are intentional boundaries, not oversights.
|
|
42
48
|
import { test } from 'node:test';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|