@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
package/dist/daemon/crtrd.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NodeRow } from '../core/canvas/index.js';
|
|
1
|
+
import { type NodeRow, type NodeMeta } from '../core/canvas/index.js';
|
|
2
2
|
import { FAULT_QUIET_MS } from '../core/runtime/fault.js';
|
|
3
3
|
export { FAULT_QUIET_MS };
|
|
4
4
|
import { isPidAlive } from '../core/canvas/pid.js';
|
|
@@ -35,14 +35,15 @@ export declare function wedgeVerdict(input: {
|
|
|
35
35
|
quietForMs: number | null;
|
|
36
36
|
cpuPercent: number | null;
|
|
37
37
|
}): WedgeVerdict;
|
|
38
|
-
/** Pure: sum CPU% across a process's entire tree (itself + every descendant).
|
|
39
|
-
* Parses `ps -ax -o pid=,ppid=,pcpu=` output, builds a ppid→children map, and
|
|
40
|
-
* DFS's from `rootPid` collecting root + all descendants; malformed/blank
|
|
41
|
-
* lines are skipped. Returns `null` if `rootPid` never appears in the output
|
|
42
|
-
* (can't measure at all — treated as UNKNOWN by wedgeVerdict, never as
|
|
43
|
-
* “low”) or the output has no parsable rows. Exported for direct unit testing
|
|
44
|
-
* — no process access of its own, mirroring wedgeVerdict's purity. */
|
|
45
38
|
export declare function sumTreeCpu(psOutput: string, rootPid: number): number | null;
|
|
39
|
+
/** Pure (issue #119): does `rootPid` have ANY descendant in this ps snapshot
|
|
40
|
+
* — a runaway subprocess to kill — or does the broker stand ALONE in its own
|
|
41
|
+
* tree (nothing to kill; the engine itself stalled)? Shares `parsePsTree`
|
|
42
|
+
* with `sumTreeCpu` so the two facts the wedge remediation needs come from
|
|
43
|
+
* one `ps` sample. Null when `rootPid` isn't in the sample at all (can't
|
|
44
|
+
* tell — same "unknown, never treated as a fact" stance `sumTreeCpu` takes on
|
|
45
|
+
* a missing root). Exported for direct unit testing. */
|
|
46
|
+
export declare function treeHasDescendants(psOutput: string, rootPid: number): boolean | null;
|
|
46
47
|
export type LivenessVerdict = 'leave' | 'pending' | 'revive';
|
|
47
48
|
/** Decide what to do with a node whose engine pid is DEAD, from how long it's
|
|
48
49
|
* been dead. Pure — the time/revive side effects live in handleNodeLiveness;
|
|
@@ -51,6 +52,14 @@ export type LivenessVerdict = 'leave' | 'pending' | 'revive';
|
|
|
51
52
|
* upstream by handleNodeLiveness.)
|
|
52
53
|
* deadFor: ms since first observed dead, or null on the first observation. */
|
|
53
54
|
export declare function livenessVerdict(piPidAlive: boolean | null, deadFor: number | null): LivenessVerdict;
|
|
55
|
+
/** Whether a crash-retry of a dead/stranded engine should resume STRICTLY
|
|
56
|
+
* (true) or retry the SAME launch attempt's cycling mode (false). Mirrors
|
|
57
|
+
* reviveNode's own cycle_pending flag (set true right before every cycling
|
|
58
|
+
* launch, cleared the instant session_start confirms boot) — so a cycling
|
|
59
|
+
* revive that died pre-session_start is retried AS a cycle (re-appending the
|
|
60
|
+
* marker + kickoff), never stranded as a strict resume into an empty marker
|
|
61
|
+
* branch. Pure so it's unit-testable without a fabricated node. */
|
|
62
|
+
export declare function retryResumeMode(meta: Pick<NodeMeta, 'cycle_pending'> | null): boolean;
|
|
54
63
|
export declare const DEAD_REAP_GRACE_MS: number;
|
|
55
64
|
/** Read the pid stored in the pidfile, or null if absent / malformed. */
|
|
56
65
|
export declare function readPidfile(): number | null;
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
// • pid dead + intent==='refresh' → fresh respawn (node asked to yield).
|
|
19
19
|
// • pid dead + intent==='idle-release' → dormant by choice; the second pass
|
|
20
20
|
// revives (resume) when its inbox gains an unseen entry.
|
|
21
|
-
// • pid dead + any other intent → grace-revive RESUME on the saved session
|
|
21
|
+
// • pid dead + any other intent → grace-revive RESUME on the saved session,
|
|
22
|
+
// UNLESS the dead engine's last launch attempt was itself an in-flight
|
|
23
|
+
// cycle (cycle_pending) — then retry it AS a cycle instead of stranding it
|
|
24
|
+
// as a strict resume into an empty marker branch.
|
|
22
25
|
//
|
|
23
26
|
// §H refresh-authority: a stale in-process stophook (pi extensions never reload,
|
|
24
27
|
// so a days-old resident carries an old hook) can leave intent='refresh' on a
|
|
@@ -38,14 +41,16 @@
|
|
|
38
41
|
// daemon keeps the live broker parked and retries it via the inbox-based
|
|
39
42
|
// fault-recovery pass so the ordinary dead-pid crash path never needs an
|
|
40
43
|
// out-of-band launch.
|
|
41
|
-
// dead-pid crash path grace-revives RESUME on the saved session
|
|
44
|
+
// dead-pid crash path grace-revives RESUME on the saved session (or, for a
|
|
45
|
+
// crash mid-cycle, retries AS a cycle — see cycle_pending above) — the manual
|
|
42
46
|
// recovery (kill broker + canvas revive) that fixes these by hand.
|
|
43
47
|
//
|
|
44
48
|
// Single-instance guarantee
|
|
45
49
|
// A PID file prevents double-runs. On start, if the file exists and the
|
|
46
|
-
// recorded pid is alive, we refuse to start (exit 0).
|
|
47
|
-
// CRTR_PIDFILE to keep this process-local on recreate; otherwise it
|
|
48
|
-
// to crtrHome()/crtrd.pid. On stop (SIGINT/SIGTERM/exit) we remove
|
|
50
|
+
// recorded pid is alive, we refuse to start (exit 0). Remote/guest deployments
|
|
51
|
+
// set CRTR_PIDFILE to keep this process-local on recreate; otherwise it
|
|
52
|
+
// defaults to crtrHome()/crtrd.pid. On stop (SIGINT/SIGTERM/exit) we remove
|
|
53
|
+
// the file.
|
|
49
54
|
import { writeFileSync, readFileSync, rmSync, existsSync, mkdirSync, statSync, } from 'node:fs';
|
|
50
55
|
import { dirname, join } from 'node:path';
|
|
51
56
|
import { homedir } from 'node:os';
|
|
@@ -55,7 +60,7 @@ import { listNodes, getRow, getNode, dueClockTriggers, consumeTrigger, advanceTr
|
|
|
55
60
|
import { fullName } from '../core/canvas/labels.js';
|
|
56
61
|
import { transition } from '../core/runtime/lifecycle.js';
|
|
57
62
|
import { isBusy, busySince } from '../core/runtime/busy.js';
|
|
58
|
-
import { FAULT_QUIET_MS, recordFault } from '../core/runtime/fault.js';
|
|
63
|
+
import { FAULT_QUIET_MS, readFault, recordFault } from '../core/runtime/fault.js';
|
|
59
64
|
import { fanDoctrineWake } from '../core/runtime/close.js';
|
|
60
65
|
import { logger } from '../core/log.js';
|
|
61
66
|
export { FAULT_QUIET_MS };
|
|
@@ -220,12 +225,19 @@ function handleYieldStall(row, pid, now) {
|
|
|
220
225
|
// with no wake and no visible fault marker (`node inspect list --hanging`
|
|
221
226
|
// returns nothing — there's no fault recorded, just silence).
|
|
222
227
|
//
|
|
223
|
-
// Detection
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
228
|
+
// Detection always NOTIFIES (fans a doctrine wake + records a `daemon→node`/
|
|
229
|
+
// 'wedged' fault so the node shows ⌘ “wedged · needs you” on the canvas graph
|
|
230
|
+
// views, same surface `node inspect list --hanging` already reads). Recovery
|
|
231
|
+
// then branches on whether the broker's process TREE has a descendant:
|
|
232
|
+
// • descendants present → a runaway SUBPROCESS is the likely cause; the
|
|
233
|
+
// daemon does NOT touch the engine — kill the subprocess (not the node);
|
|
234
|
+
// see `crtr memory read wedged-child-on-runaway-bash`.
|
|
235
|
+
// • no descendants (the broker pid stands alone — e.g. the engine itself
|
|
236
|
+
// stalled after a model_change, with nothing to kill) → the daemon
|
|
237
|
+
// performs the ONLY remaining remediation itself: SIGTERM the broker, the
|
|
238
|
+
// same on-demand kick `canvas revive --now` does, so its own dead-pid
|
|
239
|
+
// crash-grace path (REVIVE_GRACE_MS) resumes it on the saved session with
|
|
240
|
+
// no lost context.
|
|
229
241
|
//
|
|
230
242
|
// Two corroborating signals, both generous, so a routine long-running bash/
|
|
231
243
|
// build/test tool call is never mistaken for a wedge:
|
|
@@ -256,11 +268,14 @@ function handleYieldStall(row, pid, now) {
|
|
|
256
268
|
// ACCEPTED residual: a legitimate subprocess blocked on silent I/O (e.g. a
|
|
257
269
|
// 20-minute download emitting no output, a long `sleep`) reads ~0% tree
|
|
258
270
|
// CPU and is indistinguishable from a real wedge at the OS level — no CPU
|
|
259
|
-
// signal can tell a hung child from an I/O-blocked one. This is accepted
|
|
260
|
-
//
|
|
261
|
-
// of
|
|
262
|
-
// minutes of total silence with near-zero tree CPU is itself worth
|
|
263
|
-
// surfacing either way.
|
|
271
|
+
// signal can tell a hung child from an I/O-blocked one. This is accepted:
|
|
272
|
+
// that case still HAS a descendant, so the daemon only notifies (never
|
|
273
|
+
// kills) — the cost of the rare false positive is one doctrine wake, and
|
|
274
|
+
// 20+ minutes of total silence with near-zero tree CPU is itself worth
|
|
275
|
+
// surfacing either way. Only the no-descendants branch ever acts, and
|
|
276
|
+
// SIGTERM-then-resume can't lose context (the saved session is resumed
|
|
277
|
+
// unchanged), so a false positive there costs one unnecessary restart, not
|
|
278
|
+
// lost work.
|
|
264
279
|
// ---------------------------------------------------------------------------
|
|
265
280
|
// Generous: routine long tool calls settle well under this; only a turn with
|
|
266
281
|
// zero observed engine progress for 20+ minutes is even a wedge CANDIDATE.
|
|
@@ -295,6 +310,15 @@ export function wedgeVerdict(input) {
|
|
|
295
310
|
* repeat notice), which is the same tradeoff `notifiedTriggerFailures` and
|
|
296
311
|
* `unhealthySince` already accept. */
|
|
297
312
|
const wedgeNotifiedAt = new Map();
|
|
313
|
+
/** Fatal-provider-fault notify latch (issue #94), keyed on node id → the
|
|
314
|
+
* `fault.since` we already fanned a wake for. A fatal fault (dead OAuth grant,
|
|
315
|
+
* bad key, protocol) has NO retry consumer, so an errored turn just leaves the
|
|
316
|
+
* node `active` with a live-but-dormant broker and, until now, woke nobody —
|
|
317
|
+
* the manager waiting on it hung and later scheduled wakes silently re-failed.
|
|
318
|
+
* Deduped on `fault.since` so one episode fans exactly one wake; a later
|
|
319
|
+
* episode (fresh `since`) or a cleared fault re-arms. In-memory like every
|
|
320
|
+
* other latch here. */
|
|
321
|
+
const fatalFaultNotifiedAt = new Map();
|
|
298
322
|
/** Pure: sum CPU% across a process's entire tree (itself + every descendant).
|
|
299
323
|
* Parses `ps -ax -o pid=,ppid=,pcpu=` output, builds a ppid→children map, and
|
|
300
324
|
* DFS's from `rootPid` collecting root + all descendants; malformed/blank
|
|
@@ -302,7 +326,7 @@ const wedgeNotifiedAt = new Map();
|
|
|
302
326
|
* (can't measure at all — treated as UNKNOWN by wedgeVerdict, never as
|
|
303
327
|
* “low”) or the output has no parsable rows. Exported for direct unit testing
|
|
304
328
|
* — no process access of its own, mirroring wedgeVerdict's purity. */
|
|
305
|
-
|
|
329
|
+
function parsePsTree(psOutput) {
|
|
306
330
|
const byPid = new Map(); // pid → pcpu
|
|
307
331
|
const childrenOf = new Map(); // ppid → child pids
|
|
308
332
|
for (const line of psOutput.split('\n')) {
|
|
@@ -324,6 +348,10 @@ export function sumTreeCpu(psOutput, rootPid) {
|
|
|
324
348
|
else
|
|
325
349
|
siblings.push(pid);
|
|
326
350
|
}
|
|
351
|
+
return { byPid, childrenOf };
|
|
352
|
+
}
|
|
353
|
+
export function sumTreeCpu(psOutput, rootPid) {
|
|
354
|
+
const { byPid, childrenOf } = parsePsTree(psOutput);
|
|
327
355
|
if (!byPid.has(rootPid))
|
|
328
356
|
return null;
|
|
329
357
|
let total = 0;
|
|
@@ -341,19 +369,39 @@ export function sumTreeCpu(psOutput, rootPid) {
|
|
|
341
369
|
}
|
|
342
370
|
return total;
|
|
343
371
|
}
|
|
372
|
+
/** Pure (issue #119): does `rootPid` have ANY descendant in this ps snapshot
|
|
373
|
+
* — a runaway subprocess to kill — or does the broker stand ALONE in its own
|
|
374
|
+
* tree (nothing to kill; the engine itself stalled)? Shares `parsePsTree`
|
|
375
|
+
* with `sumTreeCpu` so the two facts the wedge remediation needs come from
|
|
376
|
+
* one `ps` sample. Null when `rootPid` isn't in the sample at all (can't
|
|
377
|
+
* tell — same "unknown, never treated as a fact" stance `sumTreeCpu` takes on
|
|
378
|
+
* a missing root). Exported for direct unit testing. */
|
|
379
|
+
export function treeHasDescendants(psOutput, rootPid) {
|
|
380
|
+
const { byPid, childrenOf } = parsePsTree(psOutput);
|
|
381
|
+
if (!byPid.has(rootPid))
|
|
382
|
+
return null;
|
|
383
|
+
return (childrenOf.get(rootPid) ?? []).length > 0;
|
|
384
|
+
}
|
|
344
385
|
/** Sample the broker's WHOLE PROCESS TREE — the broker pid plus every
|
|
345
|
-
* descendant
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
|
|
386
|
+
* descendant — via ONE `ps` shell-out, yielding both signals the wedge path
|
|
387
|
+
* needs: the summed tree CPU% (corroborates the heartbeat silence) and
|
|
388
|
+
* whether the tree has any descendant at all (issue #119 — decides whether
|
|
389
|
+
* remediation is "kill the subprocess" or "kick the broker"). Null on ANY
|
|
390
|
+
* failure (the shell-out itself failing, `ps` missing, or the root pid
|
|
391
|
+
* absent/output unparseable) — wedgeVerdict treats a null cpuPercent exactly
|
|
392
|
+
* like “too high”, so a broken probe can only ever suppress a false fire,
|
|
393
|
+
* never cause one; a null sample also means handleWedgeDetection never
|
|
394
|
+
* attempts the SIGTERM kick on unconfirmed information. */
|
|
395
|
+
function readTreeSample(pid) {
|
|
352
396
|
try {
|
|
353
397
|
const r = spawnSync('ps', ['-ax', '-o', 'pid=,ppid=,pcpu='], { encoding: 'utf8', timeout: 2000 });
|
|
354
398
|
if (r.status !== 0 || typeof r.stdout !== 'string')
|
|
355
399
|
return null;
|
|
356
|
-
|
|
400
|
+
const cpuPercent = sumTreeCpu(r.stdout, pid);
|
|
401
|
+
const hasDescendants = treeHasDescendants(r.stdout, pid);
|
|
402
|
+
if (cpuPercent === null || hasDescendants === null)
|
|
403
|
+
return null;
|
|
404
|
+
return { cpuPercent, hasDescendants };
|
|
357
405
|
}
|
|
358
406
|
catch {
|
|
359
407
|
return null;
|
|
@@ -372,7 +420,8 @@ function handleWedgeDetection(id, pid, now) {
|
|
|
372
420
|
}
|
|
373
421
|
const since = busySince(id);
|
|
374
422
|
const quietForMs = since === null ? null : now - since;
|
|
375
|
-
const
|
|
423
|
+
const sample = quietForMs !== null && quietForMs >= WEDGE_QUIET_MS ? readTreeSample(pid) : null;
|
|
424
|
+
const cpuPercent = sample?.cpuPercent ?? null;
|
|
376
425
|
const verdict = wedgeVerdict({ busy: true, quietForMs, cpuPercent });
|
|
377
426
|
if (verdict !== 'wedged')
|
|
378
427
|
return;
|
|
@@ -383,11 +432,43 @@ function handleWedgeDetection(id, pid, now) {
|
|
|
383
432
|
const meta = getNode(id);
|
|
384
433
|
const name = meta !== null ? fullName(meta) : id;
|
|
385
434
|
const minutes = Math.round((quietForMs ?? WEDGE_QUIET_MS) / 60_000);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
435
|
+
// A null sample can't reach here — wedgeVerdict only fires 'wedged' on a
|
|
436
|
+
// confirmed near-zero cpuPercent, which requires a non-null sample — so
|
|
437
|
+
// `sample.hasDescendants` is always defined at this point; the `?? true`
|
|
438
|
+
// is a type-level fallback only, biased toward the SAFER branch (notify,
|
|
439
|
+
// don't SIGTERM) if that invariant is ever violated.
|
|
440
|
+
const hasDescendants = sample?.hasDescendants ?? true;
|
|
441
|
+
let label;
|
|
442
|
+
if (hasDescendants) {
|
|
443
|
+
// A runaway subprocess is the likely cause — the daemon does not touch the
|
|
444
|
+
// engine; the sanctioned recovery is manual (issue #110).
|
|
445
|
+
label =
|
|
446
|
+
`Child wedged — ${name} (${id}) is alive and mid-turn but has shown NO engine progress for ` +
|
|
447
|
+
`${minutes}+ minutes and its process is near-idle. It will NOT recover on its own — read ` +
|
|
448
|
+
`\`crtr memory read wedged-child-on-runaway-bash\` and kill the runaway SUBPROCESS (not this ` +
|
|
449
|
+
`node) so its turn can resume with no lost context.`;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
// No descendant to kill — the engine itself stalled (e.g. mid model_change)
|
|
453
|
+
// with nothing a human could kill. The daemon performs the same kick
|
|
454
|
+
// `canvas revive --now` does on demand: SIGTERM the broker, then its own
|
|
455
|
+
// dead-pid crash-grace path (REVIVE_GRACE_MS) resumes it on the saved
|
|
456
|
+
// session (issue #119).
|
|
457
|
+
label =
|
|
458
|
+
`Child wedged — ${name} (${id}) is alive and mid-turn but has shown NO engine progress for ` +
|
|
459
|
+
`${minutes}+ minutes, its process is near-idle, AND it has no subprocess to kill (the engine ` +
|
|
460
|
+
`itself stalled). Kicking it now: SIGTERM to the broker — the daemon's own crash-grace path ` +
|
|
461
|
+
`resumes it on the saved session within ~${Math.round(REVIVE_GRACE_MS / 1000)}s, no lost context. ` +
|
|
462
|
+
`If it comes back idle rather than resuming its turn, nudge it (see \`crtr memory read ` +
|
|
463
|
+
`wedged-child-on-runaway-bash\`).`;
|
|
464
|
+
try {
|
|
465
|
+
process.kill(pid, 'SIGTERM');
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
/* already gone — the dead-pid path picks it up on the very next tick regardless */
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
process.stderr.write(`[crtrd] wedged ${id} (busy ${minutes}+ min with no engine progress, cpu~0%, descendants=${hasDescendants})${hasDescendants ? '' : ' — kicked'}\n`);
|
|
391
472
|
try {
|
|
392
473
|
recordFault(id, {
|
|
393
474
|
link: 'daemon→node',
|
|
@@ -406,6 +487,52 @@ function handleWedgeDetection(id, pid, now) {
|
|
|
406
487
|
child: id,
|
|
407
488
|
quiet_ms: quietForMs,
|
|
408
489
|
cpu_percent: cpuPercent,
|
|
490
|
+
kicked: !hasDescendants,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
/* best-effort, mirrors every other fan-out in this file */
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/** Enact fatal-provider-fault notification for a LIVE node (issue #94; called
|
|
498
|
+
* from the pid-alive branch of handleNodeLiveness). A FATAL fault (auth /
|
|
499
|
+
* protocol / other on `pi→provider`) has no auto-retry consumer — the errored
|
|
500
|
+
* turn already ended, so the wedge detector (busy-only) never fires and the
|
|
501
|
+
* node sits `active` forever with a dormant broker. The daemon owns the
|
|
502
|
+
* analogous wedge fan, so it owns this too: fan ONE doctrine wake per fault
|
|
503
|
+
* episode telling subscribers the child will not self-recover (and, for auth,
|
|
504
|
+
* to re-authenticate so its next wake succeeds). Detection only — the node is
|
|
505
|
+
* deliberately NOT terminalized: the auth-reload fan + next wake are its real
|
|
506
|
+
* recovery path after a re-login. */
|
|
507
|
+
function handleFatalFault(id, now) {
|
|
508
|
+
void now;
|
|
509
|
+
const fault = readFault(id);
|
|
510
|
+
if (fault === null) {
|
|
511
|
+
fatalFaultNotifiedAt.delete(id); // fault cleared — next episode re-arms
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (fault.retry.disposition !== 'fatal' || fault.link !== 'pi→provider')
|
|
515
|
+
return;
|
|
516
|
+
if (fatalFaultNotifiedAt.get(id) === fault.since)
|
|
517
|
+
return; // already fanned THIS episode
|
|
518
|
+
fatalFaultNotifiedAt.set(id, fault.since);
|
|
519
|
+
const meta = getNode(id);
|
|
520
|
+
const name = meta !== null ? fullName(meta) : id;
|
|
521
|
+
const isAuth = fault.kind === 'auth';
|
|
522
|
+
const firstLine = (fault.message.split('\n')[0] ?? '').slice(0, 200);
|
|
523
|
+
const label = `Child hit a fatal provider fault — ${name} (${id}) got a non-retryable ${fault.kind} error and will NOT recover on its own; ` +
|
|
524
|
+
`it stays alive but every wake re-fails until the fault clears. ` +
|
|
525
|
+
(isAuth
|
|
526
|
+
? `Its provider credential is dead — re-authenticate (\`/login\`, or re-auth crtr); the child recovers on its next wake once the auth reloads. `
|
|
527
|
+
: ``) +
|
|
528
|
+
`Error: ${firstLine}`;
|
|
529
|
+
process.stderr.write(`[crtrd] fatal-fault ${id} (${fault.kind}, ${fault.link}) — fanning notice to subscribers\n`);
|
|
530
|
+
try {
|
|
531
|
+
fanDoctrineWake(id, subscribersOf(id), label, {
|
|
532
|
+
reason: 'child-fault-fatal',
|
|
533
|
+
child: id,
|
|
534
|
+
kind: fault.kind,
|
|
535
|
+
message: firstLine,
|
|
409
536
|
});
|
|
410
537
|
}
|
|
411
538
|
catch {
|
|
@@ -497,6 +624,16 @@ export function livenessVerdict(piPidAlive, deadFor) {
|
|
|
497
624
|
return 'pending';
|
|
498
625
|
return 'revive';
|
|
499
626
|
}
|
|
627
|
+
/** Whether a crash-retry of a dead/stranded engine should resume STRICTLY
|
|
628
|
+
* (true) or retry the SAME launch attempt's cycling mode (false). Mirrors
|
|
629
|
+
* reviveNode's own cycle_pending flag (set true right before every cycling
|
|
630
|
+
* launch, cleared the instant session_start confirms boot) — so a cycling
|
|
631
|
+
* revive that died pre-session_start is retried AS a cycle (re-appending the
|
|
632
|
+
* marker + kickoff), never stranded as a strict resume into an empty marker
|
|
633
|
+
* branch. Pure so it's unit-testable without a fabricated node. */
|
|
634
|
+
export function retryResumeMode(meta) {
|
|
635
|
+
return meta?.cycle_pending !== true;
|
|
636
|
+
}
|
|
500
637
|
/** The ONE liveness path for EVERY node. Every node runs on a detached headless
|
|
501
638
|
* broker; a tmux pane is only a viewer and is never consulted here — liveness is
|
|
502
639
|
* the recorded engine pid (signal-0) alone. Reuses the existing supervision
|
|
@@ -523,7 +660,10 @@ export function livenessVerdict(piPidAlive, deadFor) {
|
|
|
523
660
|
* pi_pid right after launch) → leave through a boot grace; a healthy engine
|
|
524
661
|
* re-records its pid well within it. If the grace ELAPSES with still no pid,
|
|
525
662
|
* the relaunch died after the pid-clear → grace-revive RESUME on the saved
|
|
526
|
-
* session
|
|
663
|
+
* session, UNLESS that dead relaunch was itself a cycling attempt
|
|
664
|
+
* (cycle_pending) — then retry it AS a cycle instead (retryResumeMode), so
|
|
665
|
+
* it doesn't strand in an empty marker branch (else the node strands
|
|
666
|
+
* 'active' with no engine forever).
|
|
527
667
|
* • pid null + pi_session_id null (NEVER booted) → normally the sub-second SDK
|
|
528
668
|
* boot gap, but a broker that throws BEFORE session_start records no pid and
|
|
529
669
|
* no session ever — so after a boot grace with STILL nothing, crash +
|
|
@@ -533,7 +673,10 @@ export function livenessVerdict(piPidAlive, deadFor) {
|
|
|
533
673
|
* • pid dead + intent==='idle-release' → dormant by choice; leave, the second
|
|
534
674
|
* pass revives (resume) on the next unseen inbox entry.
|
|
535
675
|
* • pid dead + any other intent (a crash) → grace-revive RESUME on the saved
|
|
536
|
-
* session (livenessVerdict → REVIVE_GRACE_MS → unhealthySince)
|
|
676
|
+
* session (livenessVerdict → REVIVE_GRACE_MS → unhealthySince), UNLESS the
|
|
677
|
+
* dead engine's last launch attempt was itself a cycling revive that never
|
|
678
|
+
* reached session_start (cycle_pending) — then retry it AS a cycle instead
|
|
679
|
+
* (retryResumeMode). */
|
|
537
680
|
async function handleNodeLiveness(row, now, revivedThisTick) {
|
|
538
681
|
const id = row.node_id;
|
|
539
682
|
const pid = row.pi_pid;
|
|
@@ -545,6 +688,7 @@ async function handleNodeLiveness(row, now, revivedThisTick) {
|
|
|
545
688
|
unhealthySince.delete(id);
|
|
546
689
|
handleYieldStall(row, pid, now);
|
|
547
690
|
handleWedgeDetection(id, pid, now);
|
|
691
|
+
handleFatalFault(id, now);
|
|
548
692
|
return;
|
|
549
693
|
}
|
|
550
694
|
if (pid == null) {
|
|
@@ -594,9 +738,12 @@ async function handleNodeLiveness(row, now, revivedThisTick) {
|
|
|
594
738
|
unhealthySince.delete(id);
|
|
595
739
|
if (meta.pi_session_id != null) {
|
|
596
740
|
// Grace elapsed with a session but still no pid → a relaunch that died after
|
|
597
|
-
// the pid-clear, before re-record. Resume it on the saved session
|
|
741
|
+
// the pid-clear, before re-record. Resume it on the saved session — unless
|
|
742
|
+
// that dead relaunch was itself a cycling attempt (cycle_pending), in which
|
|
743
|
+
// case retry it AS a cycle (retryResumeMode) instead of stranding it as a
|
|
744
|
+
// strict resume into an empty marker branch.
|
|
598
745
|
process.stderr.write(`[crtrd] revive ${id} (stranded relaunch — pid never re-recorded)\n`);
|
|
599
|
-
reviveNode(id, { resume:
|
|
746
|
+
reviveNode(id, { resume: retryResumeMode(meta) });
|
|
600
747
|
revivedThisTick.add(id); // third-pass bare double-spawn guard (Maj-4)
|
|
601
748
|
return;
|
|
602
749
|
}
|
|
@@ -629,9 +776,13 @@ async function handleNodeLiveness(row, now, revivedThisTick) {
|
|
|
629
776
|
unhealthySince.delete(id);
|
|
630
777
|
return;
|
|
631
778
|
}
|
|
632
|
-
// Any other intent → a crash: grace-revive RESUME on the saved session
|
|
633
|
-
//
|
|
634
|
-
//
|
|
779
|
+
// Any other intent → a crash: grace-revive RESUME on the saved session —
|
|
780
|
+
// unless the dead engine's last launch attempt was itself a cycling revive
|
|
781
|
+
// that never reached session_start (cycle_pending), in which case retry it AS
|
|
782
|
+
// a cycle instead of stranding it as a strict resume into an empty marker
|
|
783
|
+
// branch. reviveNode clears pi_pid until the fresh engine re-records it, so
|
|
784
|
+
// the next tick won't re-fire on this stale pid.
|
|
785
|
+
const meta = getNode(id);
|
|
635
786
|
const since = unhealthySince.get(id);
|
|
636
787
|
const verdict = livenessVerdict(false, since === undefined ? null : now - since);
|
|
637
788
|
if (verdict === 'pending') {
|
|
@@ -641,7 +792,7 @@ async function handleNodeLiveness(row, now, revivedThisTick) {
|
|
|
641
792
|
}
|
|
642
793
|
unhealthySince.delete(id);
|
|
643
794
|
process.stderr.write(`[crtrd] revive ${id} (engine dead, intent=${String(row.intent)})\n`);
|
|
644
|
-
reviveNode(id, { resume:
|
|
795
|
+
reviveNode(id, { resume: retryResumeMode(meta) });
|
|
645
796
|
revivedThisTick.add(id); // third-pass bare double-spawn guard (Maj-4)
|
|
646
797
|
}
|
|
647
798
|
/** Fail loud for a drifted/broken trigger (design §6.6/Q5): wake the ARMER
|
package/dist/index.d.ts
CHANGED
|
@@ -17,3 +17,6 @@ export { ViewSocketClient, BrokerUnavailableError } from './clients/attach/view-
|
|
|
17
17
|
export { encodeFrame, FrameDecoder, FrameOverflowError, CLIENT_READ_CAPS, BROKER_READ_CAPS, } from './core/runtime/broker-protocol.js';
|
|
18
18
|
export type { FrameDecoderCaps, BrokerSnapshot, ClientRole, WelcomeFrame, ControlChangedFrame, AckFrame, ErrorFrame, ExtensionUIRequestFrame, BrokerToClient, HelloFrame, PromptFrame, SteerFrame, FollowUpFrame, AbortFrame, RequestControlFrame, ReleaseControlFrame, ByeFrame, ShutdownFrame, SetModelFrame, CycleModelFrame, SetThinkingLevelFrame, SetAutoRetryFrame, SetAutoCompactionFrame, CompactFrame, NewSessionFrame, SwitchSessionFrame, ForkFrame, SetSessionNameFrame, GetCommandsFrame, NavigateTreeFrame, ReloadFrame, ExportFrame, ClientToBroker, RpcExtensionUIRequest, RpcExtensionUIResponse, ExtensionUIResponseFrame, } from './core/runtime/broker-protocol.js';
|
|
19
19
|
export type { SessionStats } from '@earendil-works/pi-coding-agent';
|
|
20
|
+
export { general } from './core/errors.js';
|
|
21
|
+
export type { CrtrError } from './core/errors.js';
|
|
22
|
+
export { nowIso } from './core/fs-utils.js';
|
package/dist/index.js
CHANGED
|
@@ -25,3 +25,10 @@ export { readTelemetry, readContextTokens } from './core/canvas/telemetry.js';
|
|
|
25
25
|
export { ViewSocketClient, BrokerUnavailableError } from './clients/attach/view-socket.js';
|
|
26
26
|
// ── Broker wire protocol (codec + frame/protocol types) ──────────────────
|
|
27
27
|
export { encodeFrame, FrameDecoder, FrameOverflowError, CLIENT_READ_CAPS, BROKER_READ_CAPS, } from './core/runtime/broker-protocol.js';
|
|
28
|
+
// ── Utility exports ───────────────────────────────────────────────────────
|
|
29
|
+
// Small side-effect-free helpers consumed by external callers that assemble
|
|
30
|
+
// on top of crtr primitives (e.g. a guest-side process reading crtr's error
|
|
31
|
+
// shape and timestamp convention). Intentionally narrow: only `general` (not
|
|
32
|
+
// `CrtrError`/`notFound`/`usage`/the other error helpers) and `nowIso`.
|
|
33
|
+
export { general } from './core/errors.js';
|
|
34
|
+
export { nowIso } from './core/fs-utils.js';
|
|
@@ -24,18 +24,23 @@
|
|
|
24
24
|
// thing a terminal worker's bearings drop). The prose lives in
|
|
25
25
|
// core/runtime/bearings.ts (shared with the promotion guidance dump).
|
|
26
26
|
//
|
|
27
|
-
// IDEMPOTENT across resumes, but FORK-AWARE
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
27
|
+
// IDEMPOTENT across resumes, but FORK-AWARE and BRANCH-AWARE: the idempotency
|
|
28
|
+
// scan walks only the ACTIVE BRANCH (root-first from the current leaf), not
|
|
29
|
+
// every entry in the file. A `--session` relaunch that is a true resume keeps
|
|
30
|
+
// the same branch, so the scan finds OUR prior bearings (named by OUR node id)
|
|
31
|
+
// and skips — it never accumulates. A post-yield CYCLE roots a fresh branch (the
|
|
32
|
+
// `crtr-cycle` marker), so the scan correctly finds nothing on that fresh branch
|
|
33
|
+
// and injects a new intro, even though an OLDER cycle's intro still sits
|
|
34
|
+
// elsewhere in the same `.jsonl`. A `--fork-from` boot COPIES the source node's
|
|
35
|
+
// whole conversation onto ITS OWN branch (bearings naming the SOURCE's node id,
|
|
36
|
+
// not ours), so the handler must NOT treat that inherited block as ours; it only
|
|
37
|
+
// skips when a bearings block belonging to OUR node is already present on the
|
|
38
|
+
// active branch (exact `details.nodeId` stamp, content-match fallback),
|
|
39
|
+
// otherwise it injects ours — whose identity assertion (leading the
|
|
40
|
+
// <crtr-bearings> block) reasserts the fork's identity over the inherited
|
|
41
|
+
// persona. This is ONE of two reinforcing channels: spawn.ts ALSO prepends the
|
|
42
|
+
// same identity assertion to a fork's kickoff prompt (the turn-triggering
|
|
43
|
+
// message), so the override does not rest on a single trailing custom_message.
|
|
39
44
|
//
|
|
40
45
|
// COLLAPSED BY DEFAULT: a `registerMessageRenderer` keyed to our customType
|
|
41
46
|
// renders the block as a single one-line stub; the full body only appears when
|
|
@@ -166,22 +171,28 @@ export function registerCanvasContextIntro(pi) {
|
|
|
166
171
|
const nodeId = process.env['CRTR_NODE_ID'];
|
|
167
172
|
if (nodeId === undefined || nodeId.trim() === '')
|
|
168
173
|
return; // not a canvas node
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
174
|
+
// Branch-aware idempotency: scan only the ACTIVE branch (root-first from
|
|
175
|
+
// the current leaf), not every entry in the file. A post-yield cycle roots a
|
|
176
|
+
// FRESH branch (the crtr-cycle marker), so scanning the whole file would
|
|
177
|
+
// find a PRIOR cycle's intro and wrongly skip injecting this cycle's own —
|
|
178
|
+
// the node's first turn would lack its crtr-context bearings entirely.
|
|
179
|
+
// Falls back to a whole-file scan only if the engine doesn't expose
|
|
180
|
+
// getBranch (older/degraded engine). Not fooled by a FORK either way: a
|
|
181
|
+
// `--fork-from` boot copies the source's whole conversation onto ITS OWN
|
|
182
|
+
// branch (bearings naming the SOURCE's node id, not ours), so a naive
|
|
183
|
+
// "any crtr-context present?" check would suppress our own intro and let
|
|
184
|
+
// the fork inherit — and impersonate — the source. So we only skip when a
|
|
185
|
+
// bearings block belonging to OUR node is already present on the scanned
|
|
186
|
+
// branch. Primary discriminator: the EXACT `details.nodeId` stamp (machine-
|
|
177
187
|
// readable, copied on fork, never sent to the LLM). Fallback: a substring
|
|
178
188
|
// match on the block text (our id appears in the identity line AND the
|
|
179
189
|
// context-dir path `…/nodes/<nodeId>/context`) — covers legacy blocks
|
|
180
190
|
// persisted before the stamp existed. Either match ⇒ this is our own
|
|
181
191
|
// resume, not an inherited fork block, so skip.
|
|
182
|
-
const
|
|
183
|
-
.
|
|
184
|
-
.
|
|
192
|
+
const scan = typeof ctx.sessionManager.getBranch === 'function'
|
|
193
|
+
? ctx.sessionManager.getBranch()
|
|
194
|
+
: ctx.sessionManager.getEntries();
|
|
195
|
+
const ours = scan.some((e) => e.type === 'custom_message' &&
|
|
185
196
|
e.customType === CONTEXT_INTRO_CUSTOM_TYPE &&
|
|
186
197
|
(e.details?.nodeId === nodeId || entryText(e).includes(nodeId)));
|
|
187
198
|
if (ours)
|
|
@@ -314,16 +314,22 @@ export function registerCanvasStophook(pi) {
|
|
|
314
314
|
}
|
|
315
315
|
// Boot / startup / resume / reload / fork → (re)bind this process to its
|
|
316
316
|
// session id, record our OS pid (the daemon's broker-liveness signal), and
|
|
317
|
-
// CONFIRM any pending refresh-yield. Reaching session_start
|
|
318
|
-
// engine actually booted, so it is now safe to clear
|
|
319
|
-
// refresh shuts the broker down and the daemon
|
|
320
|
-
// leaves intent='refresh' set until this boot
|
|
321
|
-
// relaunch worked; otherwise a failed relaunch
|
|
317
|
+
// CONFIRM any pending refresh-yield / pending cycle. Reaching session_start
|
|
318
|
+
// proves a fresh engine actually booted, so it is now safe to clear
|
|
319
|
+
// intent='refresh' — a refresh shuts the broker down and the daemon
|
|
320
|
+
// relaunches it FRESH, which leaves intent='refresh' set until this boot
|
|
321
|
+
// clears it (the only proof the relaunch worked; otherwise a failed relaunch
|
|
322
|
+
// would look like a success). Clearing `cycle_pending` here is the same
|
|
323
|
+
// proof for reviveNode's cycling-attempt marker (revive.ts) — it is always
|
|
324
|
+
// safe to clear on ANY confirmed boot (cycling, resuming, or fresh), since a
|
|
325
|
+
// pre-session_start crash-retry (crtrd.ts) only needs the flag while boot is
|
|
326
|
+
// still unconfirmed.
|
|
322
327
|
const existing = getNode(nodeId);
|
|
323
328
|
// Identity (session id/file) → meta; runtime (pid, intent) → atomic row setters.
|
|
324
329
|
updateNode(nodeId, {
|
|
325
330
|
pi_session_id: id,
|
|
326
331
|
pi_session_file: sessionFile,
|
|
332
|
+
cycle_pending: false,
|
|
327
333
|
});
|
|
328
334
|
recordPid(nodeId, process.pid);
|
|
329
335
|
if (existing?.intent === 'refresh')
|