@crouton-kit/crouter 0.3.40 → 0.3.41
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 +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +472 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +284 -76
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +31 -24
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +52 -15
- package/dist/clients/attach/attach-cmd.js +706 -733
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/web/server.js +5 -28
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
- package/dist/commands/canvas-rebuild-index.js +5 -5
- package/dist/commands/node-snapshot.js +9 -1
- package/dist/commands/node.js +37 -17
- package/dist/commands/push.js +8 -0
- package/dist/commands/revive.d.ts +10 -0
- package/dist/commands/revive.js +28 -10
- package/dist/commands/sys/__tests__/setup-core.test.js +19 -0
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/setup-core.js +3 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/commands/worktree.d.ts +2 -0
- package/dist/commands/worktree.js +94 -0
- package/dist/core/__tests__/boot.test.js +4 -4
- package/dist/core/__tests__/canvas.test.js +19 -7
- package/dist/core/__tests__/child-followup.test.js +15 -5
- package/dist/core/__tests__/daemon-boot.test.js +6 -1
- package/dist/core/__tests__/daemon-wedge.test.js +18 -1
- package/dist/core/__tests__/fault-classifier.test.js +30 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +20 -0
- package/dist/core/__tests__/full/spike-harness.test.js +8 -5
- package/dist/core/__tests__/grace-clock.test.js +18 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +465 -0
- package/dist/core/__tests__/prune-to-limit.test.js +14 -0
- package/dist/core/__tests__/review-model-floor.test.js +32 -0
- package/dist/core/__tests__/revive.test.js +18 -4
- package/dist/core/__tests__/session-cycles.test.js +77 -0
- package/dist/core/__tests__/worktree.test.js +85 -0
- package/dist/core/canvas/boot.js +12 -7
- package/dist/core/canvas/canvas.d.ts +27 -8
- package/dist/core/canvas/canvas.js +54 -26
- package/dist/core/canvas/db.js +14 -0
- package/dist/core/canvas/history.js +1 -0
- package/dist/core/canvas/paths.d.ts +10 -9
- package/dist/core/canvas/paths.js +10 -9
- package/dist/core/canvas/pid.d.ts +155 -1
- package/dist/core/canvas/pid.js +306 -1
- package/dist/core/canvas/status-glyph.d.ts +7 -0
- package/dist/core/canvas/status-glyph.js +10 -1
- package/dist/core/canvas/types.d.ts +34 -0
- package/dist/core/fault-classifier.js +5 -1
- package/dist/core/runtime/bearings.js +4 -0
- package/dist/core/runtime/branded-host.d.ts +7 -0
- package/dist/core/runtime/branded-host.js +44 -17
- package/dist/core/runtime/broker-sdk.js +28 -68
- package/dist/core/runtime/broker.js +35 -4
- package/dist/core/runtime/host.d.ts +3 -3
- package/dist/core/runtime/host.js +148 -33
- package/dist/core/runtime/launch.d.ts +15 -15
- package/dist/core/runtime/launch.js +58 -4
- package/dist/core/runtime/naming.js +3 -2
- package/dist/core/runtime/nodes.d.ts +3 -1
- package/dist/core/runtime/nodes.js +1 -0
- package/dist/core/runtime/pi-cli.d.ts +2 -0
- package/dist/core/runtime/pi-cli.js +51 -0
- package/dist/core/runtime/placement.d.ts +1 -1
- package/dist/core/runtime/placement.js +5 -2
- package/dist/core/runtime/recap.js +2 -1
- package/dist/core/runtime/revive.d.ts +21 -15
- package/dist/core/runtime/revive.js +87 -52
- package/dist/core/runtime/session-cycles.d.ts +30 -0
- package/dist/core/runtime/session-cycles.js +77 -0
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +152 -100
- package/dist/core/runtime/tmux.d.ts +7 -10
- package/dist/core/runtime/tmux.js +9 -11
- package/dist/core/worktree.d.ts +35 -0
- package/dist/core/worktree.js +158 -0
- package/dist/daemon/crtrd.d.ts +17 -8
- package/dist/daemon/crtrd.js +191 -40
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +1 -0
- package/dist/pi-extensions/canvas-context-intro.js +34 -23
- package/dist/pi-extensions/canvas-stophook.js +11 -5
- package/dist/web-client/assets/{index-CbO8L0mN.js → index-B00YpRQ1.js} +20 -20
- package/dist/web-client/assets/index-DrkcvANq.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/docs/compat/hearth-crtr-v1.md +191 -0
- package/docs/public-api.md +75 -0
- package/package.json +3 -4
- package/dist/core/__tests__/hearth-bootstrap.test.js +0 -136
- package/dist/core/hearth/__tests__/model-auth-guest.test.js +0 -151
- package/dist/core/hearth/config.d.ts +0 -3
- package/dist/core/hearth/config.js +0 -108
- package/dist/core/hearth/guest-env.d.ts +0 -9
- package/dist/core/hearth/guest-env.js +0 -27
- package/dist/core/hearth/index.d.ts +0 -4
- package/dist/core/hearth/index.js +0 -4
- package/dist/core/hearth/model-auth-guest.d.ts +0 -8
- package/dist/core/hearth/model-auth-guest.js +0 -430
- package/dist/core/hearth/provider.d.ts +0 -36
- package/dist/core/hearth/provider.js +0 -10
- package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +0 -362
- package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +0 -12
- package/dist/core/hearth/providers/blaxel-bootstrap.js +0 -147
- package/dist/core/hearth/providers/blaxel-home.d.ts +0 -60
- package/dist/core/hearth/providers/blaxel-home.js +0 -405
- package/dist/core/hearth/providers/blaxel.d.ts +0 -36
- package/dist/core/hearth/providers/blaxel.js +0 -364
- package/dist/core/hearth/providers/types.d.ts +0 -93
- package/dist/core/hearth/types.d.ts +0 -155
- package/dist/hearth/control-plane/__tests__/error-serialization.test.js +0 -29
- package/dist/hearth/control-plane/__tests__/node-message.test.js +0 -60
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +0 -44
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +0 -49
- package/dist/hearth/control-plane/__tests__/relay-security.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/relay-security.test.js +0 -314
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +0 -133
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +0 -170
- package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/wake-roll.test.js +0 -230
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +0 -167
- package/dist/hearth/control-plane/config.d.ts +0 -21
- package/dist/hearth/control-plane/config.js +0 -77
- package/dist/hearth/control-plane/db.d.ts +0 -30
- package/dist/hearth/control-plane/db.js +0 -561
- package/dist/hearth/control-plane/hearth-target.d.ts +0 -23
- package/dist/hearth/control-plane/hearth-target.js +0 -68
- package/dist/hearth/control-plane/ingress/rate-limit.d.ts +0 -24
- package/dist/hearth/control-plane/ingress/rate-limit.js +0 -100
- package/dist/hearth/control-plane/ingress/route-store.d.ts +0 -31
- package/dist/hearth/control-plane/ingress/route-store.js +0 -61
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +0 -41
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +0 -69
- package/dist/hearth/control-plane/ingress/webhook-route.d.ts +0 -55
- package/dist/hearth/control-plane/ingress/webhook-route.js +0 -285
- package/dist/hearth/control-plane/main.d.ts +0 -1
- package/dist/hearth/control-plane/main.js +0 -88
- package/dist/hearth/control-plane/node-message.d.ts +0 -31
- package/dist/hearth/control-plane/node-message.js +0 -98
- package/dist/hearth/control-plane/register.d.ts +0 -15
- package/dist/hearth/control-plane/register.js +0 -34
- package/dist/hearth/control-plane/registry.d.ts +0 -22
- package/dist/hearth/control-plane/registry.js +0 -168
- package/dist/hearth/control-plane/relay.d.ts +0 -44
- package/dist/hearth/control-plane/relay.js +0 -711
- package/dist/hearth/control-plane/scheduler/fire-store.d.ts +0 -36
- package/dist/hearth/control-plane/scheduler/fire-store.js +0 -73
- package/dist/hearth/control-plane/scheduler/recurrence.d.ts +0 -7
- package/dist/hearth/control-plane/scheduler/recurrence.js +0 -58
- package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +0 -38
- package/dist/hearth/control-plane/scheduler/scan-loop.js +0 -138
- package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +0 -32
- package/dist/hearth/control-plane/scheduler/schedule-store.js +0 -66
- package/dist/hearth/control-plane/secrets.d.ts +0 -31
- package/dist/hearth/control-plane/secrets.js +0 -134
- package/dist/hearth/control-plane/server.d.ts +0 -27
- package/dist/hearth/control-plane/server.js +0 -482
- package/dist/hearth/control-plane/serving.d.ts +0 -15
- package/dist/hearth/control-plane/serving.js +0 -106
- package/dist/hearth/control-plane/session.d.ts +0 -68
- package/dist/hearth/control-plane/session.js +0 -273
- package/dist/hearth/control-plane/triggers/acl.d.ts +0 -14
- package/dist/hearth/control-plane/triggers/acl.js +0 -52
- package/dist/hearth/control-plane/triggers/audit-store.d.ts +0 -38
- package/dist/hearth/control-plane/triggers/audit-store.js +0 -79
- package/dist/hearth/control-plane/triggers/deliver.d.ts +0 -43
- package/dist/hearth/control-plane/triggers/deliver.js +0 -76
- package/dist/hearth/control-plane/triggers/envelope.d.ts +0 -29
- package/dist/hearth/control-plane/triggers/envelope.js +0 -38
- package/dist/hearth/control-plane/types.d.ts +0 -86
- package/dist/hearth/control-plane/types.js +0 -1
- package/dist/hearth/control-plane/wake.d.ts +0 -86
- package/dist/hearth/control-plane/wake.js +0 -550
- package/dist/web-client/assets/index-DwO46Cs5.css +0 -2
- /package/dist/{core/__tests__/hearth-bootstrap.test.d.ts → commands/__tests__/revive-now-gate.test.d.ts} +0 -0
- /package/dist/{core/hearth/__tests__/model-auth-guest.test.d.ts → commands/sys/__tests__/setup-core.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/__tests__/sweep-and-release.test.d.ts → __tests__/fault-classifier.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/types.js → __tests__/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/{hearth/types.js → __tests__/review-model-floor.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/error-serialization.test.d.ts → core/__tests__/session-cycles.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/node-message.test.d.ts → core/__tests__/worktree.test.d.ts} +0 -0
|
@@ -72,30 +72,57 @@ function readMarker() {
|
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
75
|
+
/** macOS keys FDA grants on the Mach-O cdhash. A real developer identity
|
|
76
|
+
* keeps that hash stable across branded-host rebuilds; ad-hoc signing is the
|
|
77
|
+
* portable default for machines without a local signing certificate. */
|
|
78
|
+
export function codesignIdentitiesForBrandedHost(opts = {}) {
|
|
79
|
+
const configured = opts.configured?.trim();
|
|
80
|
+
if (configured)
|
|
81
|
+
return [configured];
|
|
82
|
+
const discovered = opts.discovered?.trim();
|
|
83
|
+
return [discovered || '-'];
|
|
84
|
+
}
|
|
85
|
+
function discoverCodesignIdentity() {
|
|
86
|
+
if (process.platform !== 'darwin')
|
|
87
|
+
return null;
|
|
88
|
+
const res = spawnSync('security', ['find-identity', '-v', '-p', 'codesigning'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
89
|
+
if (res.status !== 0)
|
|
90
|
+
return null;
|
|
91
|
+
const stdout = (res.stdout ?? '').toString();
|
|
92
|
+
const identities = stdout
|
|
93
|
+
.split('\n')
|
|
94
|
+
.map((line) => line.match(/\)\s+[A-Fa-f0-9]{40}\s+"([^"]+)"/)?.[1] ?? null)
|
|
95
|
+
.filter((identity) => identity !== null);
|
|
96
|
+
return identities.find((identity) => identity.startsWith('Apple Development:'))
|
|
97
|
+
?? identities.find((identity) => identity.startsWith('Developer ID Application:'))
|
|
98
|
+
?? identities[0]
|
|
99
|
+
?? null;
|
|
100
|
+
}
|
|
101
|
+
function runCodesign(binPath, identity) {
|
|
102
|
+
const res = spawnSync('codesign', ['-s', identity, '--deep', '--force', binPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
103
|
+
return { ok: res.status === 0, stderr: (res.stderr ?? '').toString().trim() };
|
|
104
|
+
}
|
|
79
105
|
function signBrandedHost(binPath) {
|
|
80
106
|
if (process.platform !== 'darwin')
|
|
81
107
|
return;
|
|
82
108
|
try {
|
|
83
|
-
const
|
|
84
|
-
'
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
const identities = codesignIdentitiesForBrandedHost({
|
|
110
|
+
configured: process.env['CRTR_CODESIGN_IDENTITY'],
|
|
111
|
+
discovered: discoverCodesignIdentity(),
|
|
112
|
+
});
|
|
113
|
+
let lastError = '';
|
|
114
|
+
for (const identity of identities) {
|
|
115
|
+
const res = runCodesign(binPath, identity);
|
|
116
|
+
if (res.ok)
|
|
117
|
+
return;
|
|
118
|
+
lastError = res.stderr;
|
|
119
|
+
}
|
|
120
|
+
if (lastError && !lastError.includes('valid on disk')) {
|
|
121
|
+
console.debug(`[crouter] codesign failed (expected on some systems): ${lastError}`);
|
|
95
122
|
}
|
|
96
123
|
}
|
|
97
124
|
catch {
|
|
98
|
-
// codesign not found or other spawn error — non-fatal, continue.
|
|
125
|
+
// codesign/security not found or other spawn error — non-fatal, continue.
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
128
|
export function signBrandedHostArtifacts(binPath, libnodePath, sign = signBrandedHost) {
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
// broker-sdk.ts — the single import-and-version-assert shim between the headless
|
|
2
2
|
// broker and the pi SDK (`@earendil-works/pi-coding-agent`).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// The broker imports the SDK in-process, and the bundled pi CLI is used only for
|
|
5
|
+
// incidental headless helper commands and the version tripwire. This shim is the
|
|
6
|
+
// one place the dependency is named, so:
|
|
7
7
|
// - the version tripwire (`assertEngineVersion`) lives here, and
|
|
8
8
|
// - the `CRTR_BROKER_ENGINE` test seam (T11) has a single dynamic-import
|
|
9
9
|
// indirection point (`loadBrokerEngine`) to swap in the fake engine.
|
|
10
|
-
//
|
|
11
|
-
// Nothing else in the tree imports this yet; it compiles but stays inert so the
|
|
12
|
-
// lifecycle suite stays green until the broker is wired up.
|
|
13
10
|
import { execFileSync } from 'node:child_process';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
11
|
+
import { readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
16
13
|
import { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION, } from '@earendil-works/pi-coding-agent';
|
|
17
14
|
import { crtrHome } from '../canvas/paths.js';
|
|
15
|
+
import { resolveBundledPiCliPath } from './pi-cli.js';
|
|
18
16
|
// Static re-exports — the real SDK surface the broker drives in production.
|
|
19
17
|
export { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION, };
|
|
20
18
|
/**
|
|
@@ -44,44 +42,10 @@ export async function loadBrokerEngine() {
|
|
|
44
42
|
VERSION: typeof mod.VERSION === 'string' ? mod.VERSION : 'unknown',
|
|
45
43
|
};
|
|
46
44
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Boot-time tripwire: compare the imported SDK `VERSION` against the `pi` binary
|
|
49
|
-
* crouter forks elsewhere (`pi --version`). On mismatch (or if the binary can't be
|
|
50
|
-
* probed) it logs a LOUD warning to stderr and proceeds — it NEVER throws. The v3
|
|
51
|
-
* session format auto-migrates on load, so minor skew round-trips; a hard fail here
|
|
52
|
-
* would risk a grace-revive crash loop. Phase 5 may harden this to fail-fast once
|
|
53
|
-
* the broker is the only host.
|
|
54
|
-
*
|
|
55
|
-
* @param engineVersion the version actually loaded (defaults to the statically
|
|
56
|
-
* imported `VERSION`; pass `engine.VERSION` from `loadBrokerEngine` to assert the
|
|
57
|
-
* version the broker is really driving).
|
|
58
|
-
*/
|
|
59
|
-
/** Resolve the `pi` executable on PATH without spawning anything — a plain walk
|
|
60
|
-
* of `$PATH` entries (`statSync` follows symlinks, so an npm/brew bin shim
|
|
61
|
-
* resolves to the real target). Returns the first match, or undefined. */
|
|
62
|
-
function resolvePiBinary() {
|
|
63
|
-
const path = process.env['PATH'];
|
|
64
|
-
if (path === undefined || path === '')
|
|
65
|
-
return undefined;
|
|
66
|
-
for (const dir of path.split(delimiter)) {
|
|
67
|
-
if (dir === '')
|
|
68
|
-
continue;
|
|
69
|
-
const candidate = join(dir, 'pi');
|
|
70
|
-
try {
|
|
71
|
-
if (existsSync(candidate))
|
|
72
|
-
return candidate;
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
/* unreadable PATH entry — skip */
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
45
|
const PI_VERSION_CACHE = () => join(crtrHome(), 'pi-version.json');
|
|
81
|
-
/** Cached `pi --version`, keyed on the
|
|
82
|
-
* re-probes but the steady state never spawns. Returns undefined when the
|
|
83
|
-
*
|
|
84
|
-
* once and back-fills via {@link writePiVersionCache}. */
|
|
46
|
+
/** Cached `pi --version`, keyed on the CLI's path+mtime+size so a pi upgrade
|
|
47
|
+
* re-probes but the steady state never spawns. Returns undefined when the cache
|
|
48
|
+
* misses — the caller then spawns once and back-fills via {@link writePiVersionCache}. */
|
|
85
49
|
function readPiVersionCache(binPath, mtimeMs, size) {
|
|
86
50
|
try {
|
|
87
51
|
const c = JSON.parse(readFileSync(PI_VERSION_CACHE(), 'utf8'));
|
|
@@ -103,41 +67,37 @@ function writePiVersionCache(entry) {
|
|
|
103
67
|
}
|
|
104
68
|
}
|
|
105
69
|
export function assertEngineVersion(engineVersion = VERSION) {
|
|
106
|
-
// Resolve + stat the
|
|
107
|
-
// cache, reuse the stored version and skip the ~0.4s `pi --version` boot —
|
|
108
|
-
// common path on every broker start. A pi upgrade changes the stat and re-probes.
|
|
109
|
-
|
|
70
|
+
// Resolve + stat the bundled CLI (no spawn). When its path+mtime+size matches
|
|
71
|
+
// the cache, reuse the stored version and skip the ~0.4s `pi --version` boot —
|
|
72
|
+
// the common path on every broker start. A pi upgrade changes the stat and re-probes.
|
|
73
|
+
let binPath;
|
|
110
74
|
let stat;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
stat = undefined;
|
|
118
|
-
}
|
|
75
|
+
try {
|
|
76
|
+
binPath = resolveBundledPiCliPath();
|
|
77
|
+
const s = statSync(binPath);
|
|
78
|
+
stat = { mtimeMs: s.mtimeMs, size: s.size };
|
|
119
79
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
80
|
+
catch (err) {
|
|
81
|
+
process.stderr.write(`[broker] WARNING: could not resolve bundled pi CLI to verify engine parity ` +
|
|
82
|
+
`(SDK ${engineVersion}): ${err.message}\n`);
|
|
83
|
+
return;
|
|
123
84
|
}
|
|
85
|
+
let binaryVersion = readPiVersionCache(binPath, stat.mtimeMs, stat.size);
|
|
124
86
|
if (binaryVersion === undefined) {
|
|
125
87
|
try {
|
|
126
|
-
binaryVersion = execFileSync(
|
|
88
|
+
binaryVersion = execFileSync(process.execPath, [binPath, '--version'], { encoding: 'utf8' }).trim();
|
|
127
89
|
}
|
|
128
90
|
catch (err) {
|
|
129
|
-
process.stderr.write(`[broker] WARNING: could not run 'pi --version' to verify engine parity ` +
|
|
91
|
+
process.stderr.write(`[broker] WARNING: could not run bundled 'pi --version' to verify engine parity ` +
|
|
130
92
|
`(SDK ${engineVersion}): ${err.message}\n`);
|
|
131
93
|
return;
|
|
132
94
|
}
|
|
133
|
-
|
|
134
|
-
writePiVersionCache({ path: binPath, mtimeMs: stat.mtimeMs, size: stat.size, version: binaryVersion });
|
|
135
|
-
}
|
|
95
|
+
writePiVersionCache({ path: binPath, mtimeMs: stat.mtimeMs, size: stat.size, version: binaryVersion });
|
|
136
96
|
}
|
|
137
97
|
if (binaryVersion !== engineVersion) {
|
|
138
98
|
process.stderr.write(`[broker] WARNING: pi SDK version mismatch — imported SDK is ${engineVersion} but ` +
|
|
139
|
-
`the
|
|
140
|
-
`minor skew round-trips, but
|
|
141
|
-
`
|
|
99
|
+
`the bundled pi CLI is ${binaryVersion}. The v3 session format auto-migrates on load so ` +
|
|
100
|
+
`minor skew round-trips, but keep the crouter dependency on '@earendil-works/pi-coding-agent' ` +
|
|
101
|
+
`in lockstep with the in-process engine.\n`);
|
|
142
102
|
}
|
|
143
103
|
}
|
|
@@ -32,6 +32,7 @@ import { isModelNotFoundError, readFault, recordFault } from './fault.js';
|
|
|
32
32
|
import { CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY } from './fault-recovery-nudge.js';
|
|
33
33
|
import { markFatalOnExhaust, nextFaultRetry, policyFor } from './fault-recovery.js';
|
|
34
34
|
import { probeOnline as probeOnlineReal } from './connectivity.js';
|
|
35
|
+
import { CRTR_CYCLE_CUSTOM_TYPE, cycleAwareMessages } from './session-cycles.js';
|
|
35
36
|
import { BUILTIN_SLASH_COMMANDS } from './pi-vendored.js';
|
|
36
37
|
import { encodeFrame, FrameDecoder, FrameOverflowError, BROKER_READ_CAPS, } from './broker-protocol.js';
|
|
37
38
|
const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh']);
|
|
@@ -1646,9 +1647,19 @@ export async function runBroker(nodeId) {
|
|
|
1646
1647
|
//
|
|
1647
1648
|
// `AgentSession.sessionManager` is a public readonly field of the real pi SDK; the
|
|
1648
1649
|
// fake-engine test fixture mirrors the same `sessionManager.buildSessionContext()`
|
|
1649
|
-
// surface,
|
|
1650
|
+
// surface (plus, where present, the tree accessors `cycleAwareMessages` reads).
|
|
1651
|
+
// A refresh-yielded node keeps every prior cycle in the SAME `.jsonl` as sibling
|
|
1652
|
+
// branches (session-cycles.ts) — `cycleAwareMessages` replays them ahead of the
|
|
1653
|
+
// live cycle's context so the welcome snapshot renders one continuous
|
|
1654
|
+
// conversation instead of just the post-yield window; single-cycle sessions and
|
|
1655
|
+
// the fake-engine fixture (no tree accessors) fall through to plain
|
|
1656
|
+
// `buildSessionContext().messages` unchanged.
|
|
1650
1657
|
export function snapshotMessages(session) {
|
|
1651
|
-
|
|
1658
|
+
// Cast: the real SDK's `SessionEntry.data` is `unknown` (untyped custom-entry
|
|
1659
|
+
// payload), narrower than `CycleSessionManagerLike`'s structural read of the
|
|
1660
|
+
// `crtr-cycle` marker's `{cycle, fromLeaf}` shape — the fields we read off it
|
|
1661
|
+
// are ones ONLY this module's own `appendCustomEntry` call (above) ever writes.
|
|
1662
|
+
return cycleAwareMessages(session.sessionManager);
|
|
1652
1663
|
}
|
|
1653
1664
|
// ---------------------------------------------------------------------------
|
|
1654
1665
|
// buildBrokerSession (plan T4 steps 2–4) — turn the launch recipe into a live
|
|
@@ -1745,7 +1756,19 @@ export async function buildBrokerSession(engine, cfg) {
|
|
|
1745
1756
|
// A fork is a fresh spawn (resuming=false) so the kickoff firstPrompt fires: a
|
|
1746
1757
|
// `node new --fork-from <id>` gives the new node the source's full history AND a
|
|
1747
1758
|
// new task to start on. A resume replays the inbox instead and sends no kickoff.
|
|
1748
|
-
|
|
1759
|
+
//
|
|
1760
|
+
// A refresh-yield CYCLES instead: `cfg.newCycle` (the `--crtr-new-cycle` token)
|
|
1761
|
+
// marks a revive that reuses this SAME `.jsonl` rather than resuming it — the
|
|
1762
|
+
// session manager opens the file, then resets the leaf and roots a `crtr-cycle`
|
|
1763
|
+
// marker entry (data: `{cycle, fromLeaf}`) as the new cycle's ROOT before the
|
|
1764
|
+
// session is built. Custom entries never enter LLM context, so the freshly
|
|
1765
|
+
// rooted branch is exactly as clean as a brand-new session; `cycleAwareMessages`
|
|
1766
|
+
// (session-cycles.ts) is the display-side counterpart that replays every prior
|
|
1767
|
+
// cycle for snapshot rendering. cycling ⇒ resuming=false, so the kickoff
|
|
1768
|
+
// firstPrompt fires below just like a true fresh launch.
|
|
1769
|
+
const hasSessionPath = !forking && resumePath !== undefined && resumePath !== '';
|
|
1770
|
+
const cycling = hasSessionPath && cfg.newCycle === true;
|
|
1771
|
+
const resuming = hasSessionPath && !cycling;
|
|
1749
1772
|
let sessionManager;
|
|
1750
1773
|
if (forking) {
|
|
1751
1774
|
// Spawn-time fork via pi's REAL fork seam, SessionManager.forkFrom — the exact
|
|
@@ -1759,8 +1782,16 @@ export async function buildBrokerSession(engine, cfg) {
|
|
|
1759
1782
|
// fails loudly here (forkFrom throws on an empty/invalid/header-less source).
|
|
1760
1783
|
sessionManager = engine.SessionManager.forkFrom(cfg.forkFrom, cfg.cwd);
|
|
1761
1784
|
}
|
|
1762
|
-
else if (resuming) {
|
|
1785
|
+
else if (resuming || cycling) {
|
|
1763
1786
|
sessionManager = engine.SessionManager.open(resumePath);
|
|
1787
|
+
if (cycling) {
|
|
1788
|
+
const fromLeaf = sessionManager.getLeafId();
|
|
1789
|
+
sessionManager.resetLeaf();
|
|
1790
|
+
sessionManager.appendCustomEntry(CRTR_CYCLE_CUSTOM_TYPE, {
|
|
1791
|
+
cycle: Number(process.env['CRTR_CYCLES']),
|
|
1792
|
+
fromLeaf,
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1764
1795
|
}
|
|
1765
1796
|
else {
|
|
1766
1797
|
sessionManager = engine.SessionManager.create(cfg.cwd);
|
|
@@ -11,9 +11,9 @@ export interface LaunchPlacement {
|
|
|
11
11
|
* the broker pid recorded as pi_pid; no tmux placement coords exist (the engine
|
|
12
12
|
* is never in a pane). */
|
|
13
13
|
export interface HostHandle {
|
|
14
|
-
/** Supervised pid (signal-0 target).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
/** Supervised pid (signal-0 target). Null means the host failed to spawn an
|
|
15
|
+
* engine at all. Spawn/revive record this pid immediately; the broker
|
|
16
|
+
* re-records the same pid during extension bind. */
|
|
17
17
|
pid: number | null;
|
|
18
18
|
/** Real child-exit signal from the launch handle. Resolves when the detached
|
|
19
19
|
* broker exits, so readiness waits can fail fast without pid probes. */
|
|
@@ -20,7 +20,8 @@ import { nodeDir, jobDir, viewSocketPath } from '../canvas/paths.js';
|
|
|
20
20
|
import { encodeFrame } from './broker-protocol.js';
|
|
21
21
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
22
22
|
import { hostExecPath } from './branded-host.js';
|
|
23
|
-
import { isPidAlive } from '../canvas/pid.js';
|
|
23
|
+
import { isPidAlive, killProcessTreePids, isAnyPidAlive, captureTeardownSnapshot } from '../canvas/pid.js';
|
|
24
|
+
import { logger } from '../log.js';
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// HeadlessBrokerHost (design §3.3): a detached broker PROCESS, no tmux pane. It
|
|
26
27
|
// boots the dedicated broker-cli entry directly (NEVER src/cli.ts), supervises
|
|
@@ -88,8 +89,80 @@ export function preflightBrokerLaunch(nodeId, inv, cwd) {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
/** How long teardown waits after sending the graceful `shutdown` frame before
|
|
91
|
-
*
|
|
92
|
+
* escalating a broker/tree that connected but never fully exited (e.g. a hung
|
|
93
|
+
* dispose(), or a dispose() that reaped the broker but left a descendant
|
|
94
|
+
* behind). */
|
|
92
95
|
const BROKER_SHUTDOWN_GRACE_MS = 2_000;
|
|
96
|
+
/** How long teardown waits after the tree-wide SIGTERM before the final
|
|
97
|
+
* tree-wide SIGKILL sweep — the last resort for a broker (or its
|
|
98
|
+
* descendants) that ignores SIGTERM entirely. */
|
|
99
|
+
const BROKER_SIGKILL_GRACE_MS = 2_000;
|
|
100
|
+
/** How often `pollUntilTreeDead` re-checks the process TREE. Short enough that
|
|
101
|
+
* the common/happy path (tree already dead) pays only a couple of ticks; the
|
|
102
|
+
* full grace ceilings above are reserved for the genuinely-wedged case,
|
|
103
|
+
* where the added latency is the whole point (crouter#98 review finding 3). */
|
|
104
|
+
const TEARDOWN_POLL_MS = 150;
|
|
105
|
+
/** Poll `isAlive` every `TEARDOWN_POLL_MS` until it reports dead. If it is
|
|
106
|
+
* STILL alive once `timeoutMs` elapses, fire `onTimeout` exactly once (the
|
|
107
|
+
* next escalation rung) and keep polling so the caller can confirm that
|
|
108
|
+
* rung actually worked — up to `hardCeilingMs` total, after which this gives
|
|
109
|
+
* up (an unkillable/D-state descendant is out of scope for this primitive).
|
|
110
|
+
*
|
|
111
|
+
* Deliberately a recursive, NEVER-`.unref()`'d `setTimeout` rather than a
|
|
112
|
+
* fixed unref'd wait (crouter#98 review finding 3): most teardown callers
|
|
113
|
+
* are short-lived CLI subprocesses (`node lifecycle close`, `canvas prune`)
|
|
114
|
+
* that would otherwise exit before an unref'd SIGKILL timer ever fires,
|
|
115
|
+
* silently skipping the last-resort rung. Polling (instead of a blind fixed
|
|
116
|
+
* wait) is what keeps the happy path cheap despite being ref'd. */
|
|
117
|
+
function pollUntil(isAlive, timeoutMs, onTimeout, hardCeilingMs = timeoutMs + 5_000) {
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
let escalated = false;
|
|
120
|
+
const tick = () => {
|
|
121
|
+
if (!isAlive())
|
|
122
|
+
return; // confirmed dead — stop polling
|
|
123
|
+
const elapsed = Date.now() - start;
|
|
124
|
+
if (!escalated && elapsed >= timeoutMs) {
|
|
125
|
+
escalated = true;
|
|
126
|
+
onTimeout();
|
|
127
|
+
}
|
|
128
|
+
if (elapsed >= hardCeilingMs)
|
|
129
|
+
return; // best-effort exhausted — give up
|
|
130
|
+
setTimeout(tick, TEARDOWN_POLL_MS);
|
|
131
|
+
};
|
|
132
|
+
tick();
|
|
133
|
+
}
|
|
134
|
+
/** Escalate teardown of a broker/tree that did not exit cleanly off the
|
|
135
|
+
* graceful `shutdown` frame (or was never reachable at all): SIGTERM the
|
|
136
|
+
* whole tree, then — once `BROKER_SIGKILL_GRACE_MS` has elapsed with the tree
|
|
137
|
+
* still alive — SIGKILL the whole tree as the last resort.
|
|
138
|
+
*
|
|
139
|
+
* Takes an ALREADY-CAPTURED `tree` (pid list) and `identities` (a
|
|
140
|
+
* `captureTeardownSnapshot` identity map over that same list) — both taken
|
|
141
|
+
* ONCE by `teardown()` synchronously at its very top, before EITHER
|
|
142
|
+
* escalation path (connect-error or post-shutdown-frame) can run, and
|
|
143
|
+
* threaded through here unchanged (crouter#98 review finding 1). This
|
|
144
|
+
* function must NEVER re-walk the process tree itself: `launch()` spawns the
|
|
145
|
+
* broker `detached: true`, so
|
|
146
|
+
* signaling the broker's OWN group (the first thing `killProcessTreePids`
|
|
147
|
+
* does) typically kills the broker itself almost immediately — and the
|
|
148
|
+
* kernel reparents any surviving child away from the broker's pid the
|
|
149
|
+
* INSTANT the broker exits, severing the very ppid link a fresh
|
|
150
|
+
* `descendantPids` walk depends on. Re-deriving descendants at ANY point
|
|
151
|
+
* after the broker may have exited (including a per-rung re-walk here, or a
|
|
152
|
+
* poll that re-derives instead of checking this fixed list) would silently
|
|
153
|
+
* "lose" a still-alive detached descendant — e.g. the pi bash tool's shell
|
|
154
|
+
* child, itself `detached: true` and thus its own group leader, distinct
|
|
155
|
+
* from the broker's group, so a plain `kill(-brokerPid)` never reaches it
|
|
156
|
+
* either (crouton-labs/crouter#98). `identities` guards each signal against
|
|
157
|
+
* PID REUSE across the multi-second escalation window (review finding 3) —
|
|
158
|
+
* see `killProcessTreePids`. `null` means the initial `captureTeardownSnapshot`
|
|
159
|
+
* probe itself failed (no baseline to compare against) — `killProcessTreePids`
|
|
160
|
+
* falls through to its normal unguarded signal path in that case, never
|
|
161
|
+
* reading a probe failure as proof the pid is gone or reused. */
|
|
162
|
+
function escalateBrokerTeardown(tree, identities) {
|
|
163
|
+
killProcessTreePids(tree, 'SIGTERM', identities);
|
|
164
|
+
pollUntil(() => isAnyPidAlive(tree), BROKER_SIGKILL_GRACE_MS, () => killProcessTreePids(tree, 'SIGKILL', identities));
|
|
165
|
+
}
|
|
93
166
|
export const headlessBrokerHost = {
|
|
94
167
|
launch(nodeId, inv, opts) {
|
|
95
168
|
// Fast-fail known launch errors before we detach a broker process.
|
|
@@ -136,8 +209,51 @@ export const headlessBrokerHost = {
|
|
|
136
209
|
// Graceful: connect to view.sock + send a `shutdown` frame → the broker
|
|
137
210
|
// dispose()s the engine, unlinks the socket, exits 0. Status is already
|
|
138
211
|
// flipped done/canceled by the caller (crash-safe ordering), so the daemon
|
|
139
|
-
// won't revive. On connect failure (broker dead/crashed) fall back
|
|
140
|
-
//
|
|
212
|
+
// won't revive. On connect failure (broker dead/crashed/wedged) fall back
|
|
213
|
+
// to escalateBrokerTeardown — a tree-wide SIGTERM now, a tree-wide SIGKILL
|
|
214
|
+
// as the last resort — so no orphaned descendant of a dead/wedged broker
|
|
215
|
+
// survives the node (crouton-labs/crouter#98) — + unlink the stale socket.
|
|
216
|
+
//
|
|
217
|
+
// Capture the broker pid ONCE, synchronously, right here — never re-query
|
|
218
|
+
// `getNode(nodeId)` inside the async callbacks/timers below. Callers such
|
|
219
|
+
// as placement.ts's `reapIfEmpty()` call `teardown()` then immediately
|
|
220
|
+
// `deleteNode()` in the same tick; by the time an async callback fires the
|
|
221
|
+
// row can already be gone, so a re-query would silently resolve to
|
|
222
|
+
// `undefined` and skip escalation entirely (crouter#98 review finding 2).
|
|
223
|
+
const node = getNode(nodeId);
|
|
224
|
+
const pid = node?.pi_pid;
|
|
225
|
+
// Snapshot the full descendant TREE (crouter#98 review finding 1) — the
|
|
226
|
+
// root pid plus every transitive descendant — and a stable identity
|
|
227
|
+
// fingerprint for every pid in it (review finding 3), BOTH from ONE `ps`
|
|
228
|
+
// table read (`captureTeardownSnapshot`, final review: closes the small
|
|
229
|
+
// race between two separate probes), exactly ONCE, right here, while the
|
|
230
|
+
// broker (if alive) still holds parentage over any detached descendant.
|
|
231
|
+
// EVERY escalation path below — connect-error AND post-shutdown-frame —
|
|
232
|
+
// reuses this ONE fixed snapshot; neither may re-walk `ps` by ppid once
|
|
233
|
+
// the broker might have exited, because the kernel reparents any
|
|
234
|
+
// surviving child away from `pid` the INSTANT the broker exits, severing
|
|
235
|
+
// the ppid link a fresh walk depends on — exactly the reparenting hazard
|
|
236
|
+
// this fix closes. (If the broker is ALREADY dead when `teardown()`
|
|
237
|
+
// starts, its detached descendants were already reparented before we ever
|
|
238
|
+
// got here — unrecoverable by ppid, and out of scope: the #98 repro is
|
|
239
|
+
// the wedged-but-ALIVE broker, which this covers.)
|
|
240
|
+
//
|
|
241
|
+
// `node?.pi_pid_identity` is the LAUNCH-time baseline `recordPid` captured
|
|
242
|
+
// the instant this broker was spawned/bound (final review, Fix 1): if
|
|
243
|
+
// `pid` has ALREADY been recycled for an unrelated process before
|
|
244
|
+
// teardown ever ran — the pid-reuse hazard this guard exists for — the
|
|
245
|
+
// snapshot's `reused: true` comes back with an EMPTY tree, so nothing
|
|
246
|
+
// below ever signals the stranger (or walks its process tree looking for
|
|
247
|
+
// "descendants", which would be the stranger's children, not ours).
|
|
248
|
+
const snapshot = pid != null ? captureTeardownSnapshot(pid, node?.pi_pid_identity ?? null) : null;
|
|
249
|
+
if (snapshot?.reused === true) {
|
|
250
|
+
logger('host').warn('refusing to signal a reused pid — launch-time identity mismatch', {
|
|
251
|
+
nodeId,
|
|
252
|
+
pid,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const tree = snapshot?.tree ?? [];
|
|
256
|
+
const identities = snapshot?.identities ?? null;
|
|
141
257
|
const sockPath = viewSocketPath(nodeId);
|
|
142
258
|
const cleanupSocket = () => {
|
|
143
259
|
try {
|
|
@@ -152,18 +268,11 @@ export const headlessBrokerHost = {
|
|
|
152
268
|
let connected = false;
|
|
153
269
|
sock.on('error', () => {
|
|
154
270
|
// A post-connect error (broker exiting after we sent shutdown) is benign
|
|
155
|
-
// and swallowed; only a connect failure triggers the
|
|
271
|
+
// and swallowed; only a connect failure triggers the escalation fallback.
|
|
156
272
|
if (connected)
|
|
157
273
|
return;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
process.kill(pid, 'SIGTERM');
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
/* already gone */
|
|
165
|
-
}
|
|
166
|
-
}
|
|
274
|
+
if (tree.length > 0)
|
|
275
|
+
escalateBrokerTeardown(tree, identities);
|
|
167
276
|
cleanupSocket();
|
|
168
277
|
});
|
|
169
278
|
sock.once('connect', () => {
|
|
@@ -176,25 +285,31 @@ export const headlessBrokerHost = {
|
|
|
176
285
|
}
|
|
177
286
|
sock.end();
|
|
178
287
|
cleanupSocket();
|
|
179
|
-
// Bounded exit confirmation
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
288
|
+
// Bounded exit confirmation: a broker that connects fine but then HANGS
|
|
289
|
+
// inside session.dispose() (disposeAndExit catches a throw, not a hang)
|
|
290
|
+
// would leak the process holding the sole .jsonl writer — and any
|
|
291
|
+
// bash→test→app descendants it spawned along the way (crouter#98). Poll
|
|
292
|
+
// the whole process TREE (not just the broker's own pid) for up to
|
|
293
|
+
// `BROKER_SHUTDOWN_GRACE_MS`, UNCONDITIONAL on the broker's own liveness
|
|
294
|
+
// (crouter#98 review finding 1): a broker that exits cleanly can still
|
|
295
|
+
// leave a live descendant behind (dispose() partially failed, or never
|
|
296
|
+
// reached a detached SDK child), which is exactly the shape this
|
|
297
|
+
// closes. `pollUntil` is ref'd — not `.unref()`'d — so a short-lived
|
|
298
|
+
// CLI caller stays alive long enough for the SIGKILL rung to actually
|
|
299
|
+
// fire, while the happy path (tree already dead) still pays only a
|
|
300
|
+
// couple of poll ticks.
|
|
301
|
+
//
|
|
302
|
+
// Checks `isAnyPidAlive(tree)` against the FIXED pre-signal snapshot,
|
|
303
|
+
// NEVER a fresh `isProcessTreeAlive`/`descendantPids` re-walk (crouter#98
|
|
304
|
+
// review finding 1, fixed): the broker can exit at any point during this
|
|
305
|
+
// grace window while leaving a detached descendant alive, and the
|
|
306
|
+
// instant it does, the kernel reparents that descendant away from `pid`
|
|
307
|
+
// — a re-walk-by-ppid would then find nothing and silently skip
|
|
308
|
+
// escalation, even though the descendant is still very much alive. The
|
|
309
|
+
// fixed `tree` snapshot (taken before the shutdown frame was even sent)
|
|
310
|
+
// has no such blind spot.
|
|
311
|
+
if (tree.length > 0) {
|
|
312
|
+
pollUntil(() => isAnyPidAlive(tree), BROKER_SHUTDOWN_GRACE_MS, () => escalateBrokerTeardown(tree, identities));
|
|
198
313
|
}
|
|
199
314
|
});
|
|
200
315
|
},
|
|
@@ -37,17 +37,6 @@ export interface EquivalentProviderModel {
|
|
|
37
37
|
* is per-turn state: once a provider has produced a retryable outage error, do
|
|
38
38
|
* not bounce back to it or keep reattempting the same failed provider. */
|
|
39
39
|
export declare function equivalentOtherProviderModel(currentModel: string, failedProviders?: ReadonlySet<ModelProvider>): EquivalentProviderModel | null;
|
|
40
|
-
/** Resolve a persona/caller `model:` token to the concrete `model:thinking`
|
|
41
|
-
* spec pi gets via `--model`, in order:
|
|
42
|
-
* 1. `provider/rest` — apply strength aliases to `rest`; if `provider` is
|
|
43
|
-
* `anthropic`/`openai` AND `rest` is a strength, return that ladder cell
|
|
44
|
-
* (the word `openai` maps to the openai-codex ladder). Otherwise it is a
|
|
45
|
-
* concrete `provider/id` (incl. any `:thinking` suffix) → pass through.
|
|
46
|
-
* 2. Bare strength (incl. aliases normal/weak) → the default-provider ladder
|
|
47
|
-
* cell (config default provider, then CRTR_MODEL_PROVIDER, else anthropic).
|
|
48
|
-
* 3. Bare family alias (opus/sonnet/haiku) → the Anthropic ladder's
|
|
49
|
-
* strong/medium/light, ALWAYS anthropic (ignores the env/config default).
|
|
50
|
-
* 4. Anything else → pass through unchanged. */
|
|
51
40
|
export declare function normalizeModel(model: string): string;
|
|
52
41
|
/** The minimal static system prompt every node launches with — a thin launcher
|
|
53
42
|
* frame keeping the `\n\nGuidelines:` anchor the `before_agent_start` substrate
|
|
@@ -101,20 +90,26 @@ export interface PiInvocation {
|
|
|
101
90
|
* `headlessBrokerHost`'s preflight (host.ts) fatally rejects a bare
|
|
102
91
|
* `--session` id before ever spawning the broker, so a real revive only
|
|
103
92
|
* ever resumes by path (or falls through to fresh when no path exists).
|
|
104
|
-
* - refresh-yield:
|
|
93
|
+
* - refresh-yield: same `.jsonl` (`resumeSessionPath`) PLUS `newCycle` —
|
|
94
|
+
* emits the valueless `--crtr-new-cycle` token alongside `--session`. The
|
|
95
|
+
* broker resets the session-tree leaf and roots the new cycle as a fresh
|
|
96
|
+
* sibling branch instead of starting a new file; the node re-reads its
|
|
97
|
+
* roadmap exactly as a fresh launch would. */
|
|
105
98
|
export declare function buildPiArgv(meta: NodeMeta, opts?: {
|
|
106
99
|
prompt?: string;
|
|
107
100
|
resumeSessionId?: string;
|
|
108
101
|
resumeSessionPath?: string;
|
|
109
102
|
forkFrom?: string;
|
|
103
|
+
newCycle?: boolean;
|
|
110
104
|
}): PiInvocation;
|
|
111
105
|
/** The pi-SDK launch config the headless broker drives an in-process engine
|
|
112
106
|
* with — the structural inverse of `buildPiArgv`'s flag vocabulary. Each field
|
|
113
107
|
* maps one of buildPiArgv's emitted flags back to its SDK option:
|
|
114
108
|
* `-e`→`extensionPaths`, `-n`→`editorName`, `--fork`→`forkFrom`,
|
|
115
|
-
* `--session`→`resumeSessionPath|resumeSessionId`, `--
|
|
116
|
-
* `--
|
|
117
|
-
*
|
|
109
|
+
* `--session`→`resumeSessionPath|resumeSessionId`, `--crtr-new-cycle`→
|
|
110
|
+
* `newCycle`, `--model`→`model`, `--tools`→`tools`,
|
|
111
|
+
* `--append-system-prompt`→`appendSystemPromptPath`, the trailing
|
|
112
|
+
* positional→`firstPrompt`. */
|
|
118
113
|
export interface BrokerSdkConfig {
|
|
119
114
|
/** The node's pinned working dir (from `CRTR_NODE_CWD`, else `process.cwd()`). */
|
|
120
115
|
cwd: string;
|
|
@@ -130,6 +125,11 @@ export interface BrokerSdkConfig {
|
|
|
130
125
|
* caller sets it — revive resumes by absolute path only, and the broker
|
|
131
126
|
* preflight rejects a bare `--session`. */
|
|
132
127
|
resumeSessionId?: string;
|
|
128
|
+
/** A refresh-yield's marker: reset the session-tree leaf and root a fresh
|
|
129
|
+
* cycle as a new branch of the SAME resumed `.jsonl`, instead of starting a
|
|
130
|
+
* new session file (`--crtr-new-cycle`). Only meaningful alongside
|
|
131
|
+
* `resumeSessionPath`; never set together with `forkFrom`. */
|
|
132
|
+
newCycle?: boolean;
|
|
133
133
|
/** Model spec, e.g. `anthropic/sonnet` (`--model`). */
|
|
134
134
|
model?: string;
|
|
135
135
|
/** Tool allowlist (`--tools a,b,c` → `['a','b','c']`). */
|