@crouton-kit/crouter 0.3.67 → 0.3.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/build-root.js +1 -0
  2. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +31 -31
  3. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +28 -8
  4. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +11 -11
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +1 -0
  7. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +8 -4
  8. package/dist/clients/attach/attach-cmd.js +668 -668
  9. package/dist/clients/attach/auth-pickers.d.ts +12 -0
  10. package/dist/clients/attach/auth-pickers.js +11 -0
  11. package/dist/clients/attach/pickers.js +2 -0
  12. package/dist/clients/attach/slash-commands.js +1 -0
  13. package/dist/commands/capture.d.ts +2 -0
  14. package/dist/commands/capture.js +28 -0
  15. package/dist/commands/node.js +3 -3
  16. package/dist/commands/profile/new.js +30 -5
  17. package/dist/commands/sys/setup-core.js +2 -0
  18. package/dist/commands/sys/setup.js +132 -16
  19. package/dist/core/__tests__/full/broker-pane-resolution.test.js +9 -6
  20. package/dist/core/__tests__/full/cascade-close.test.js +5 -2
  21. package/dist/core/__tests__/full/dead-pane-regression.test.js +6 -3
  22. package/dist/core/__tests__/full/detach-focus.test.js +10 -5
  23. package/dist/core/__tests__/full/human-new-window-regression.test.js +6 -3
  24. package/dist/core/__tests__/full/review-render-pane-regression.test.js +2 -2
  25. package/dist/core/__tests__/helpers/harness.d.ts +1 -0
  26. package/dist/core/__tests__/helpers/harness.js +28 -4
  27. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -4
  28. package/dist/core/__tests__/review-model-floor.test.js +1 -1
  29. package/dist/core/__tests__/revive.test.js +70 -1
  30. package/dist/core/canvas/__tests__/remote-event-stream.test.js +11 -9
  31. package/dist/core/canvas/__tests__/render-remote.test.js +7 -4
  32. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +30 -13
  33. package/dist/core/canvas/pid.d.ts +14 -10
  34. package/dist/core/canvas/pid.js +14 -10
  35. package/dist/core/command.d.ts +15 -0
  36. package/dist/core/command.js +72 -0
  37. package/dist/core/profiles/select.d.ts +4 -1
  38. package/dist/core/profiles/select.js +356 -81
  39. package/dist/core/runtime/broker.js +2 -0
  40. package/dist/core/runtime/launch.js +1 -1
  41. package/dist/core/runtime/revive.js +187 -140
  42. package/dist/core/runtime/tmux.js +4 -1
  43. package/dist/core/view/__tests__/transport-remote.test.js +9 -6
  44. package/dist/core/view/transport-local.js +12 -3
  45. package/dist/daemon/crtrd-cli.d.ts +1 -1
  46. package/dist/daemon/crtrd-cli.js +1 -0
  47. package/dist/suppress-experimental-warnings.d.ts +1 -0
  48. package/dist/suppress-experimental-warnings.js +15 -0
  49. package/dist/types.js +10 -10
  50. package/dist/web-client/assets/index-BUdm9s9s.css +2 -0
  51. package/dist/web-client/assets/{index-BvzxXXGU.js → index-DiFuLcp6.js} +19 -18
  52. package/dist/web-client/index.html +3 -3
  53. package/dist/web-client/sw.js +1 -1
  54. package/package.json +10 -7
  55. package/scripts/postinstall.mjs +1 -1
  56. package/dist/web-client/assets/index-BnmSLNLa.css +0 -2
@@ -23,13 +23,13 @@
23
23
  //
24
24
  // reviveNode remains the ONLY sanctioned launcher of the node engine.
25
25
  import { existsSync } from 'node:fs';
26
- import { getNode, updateNode, clearPid, recordPid, fullName, cancelDeadlinesFor, subscribersOf, } from '../canvas/index.js';
26
+ import { getNode, updateNode, clearPid, recordPid, fullName, cancelDeadlinesFor, subscribersOf, openDb, } from '../canvas/index.js';
27
27
  import { transition } from './lifecycle.js';
28
28
  import { fanDoctrineWake } from './close.js';
29
29
  import { buildPiArgv } from './launch.js';
30
30
  import { buildReviveKickoff, drainBearings } from './kickoff.js';
31
31
  import { headlessBrokerHost } from './host.js';
32
- import { recordedPidLiveness } from '../canvas/pid.js';
32
+ import { isRecordedPidAlive } from '../canvas/pid.js';
33
33
  import { reconcileBootLiveness } from '../canvas/boot.js';
34
34
  import { clearFault } from './fault.js';
35
35
  import { clearInjectedDocs } from '../substrate/injected-store.js';
@@ -89,147 +89,194 @@ export function reviveNode(nodeId, opts) {
89
89
  // actually matters, so it's safe to call on every revive regardless of
90
90
  // entry point (daemon tick, or a direct/manual `node lifecycle revive`).
91
91
  reconcileBootLiveness();
92
- // Double-revive guard: identity-aware liveness on pi_pid, requiring
93
- // CONFIRMED alive not the fail-open-to-alive boolean adapter. A node
94
- // whose broker pid is CONFIRMED still running as that same broker (not a
95
- // reused pid) was already revived by another path — re-launching would put
96
- // a SECOND broker on the same session file. No-op. A bare isPidAlive would
97
- // misread a reused pid (heavy forking, esp. Linux) as "still alive" and
98
- // skip the relaunch forever; conversely, treating an INDETERMINATE read
99
- // (can't confirm either way, e.g. a transient `ps` probe failure) as
100
- // "alive" here would risk the opposite failure silently skipping a
101
- // needed relaunch and stranding the node with no engine. So this guard
102
- // blocks ONLY on a positively confirmed-alive read; `dead` AND
103
- // `indeterminate` both proceed to relaunch. We accept that tradeoff: an
104
- // `indeterminate` read CAN come from a genuinely live broker whose identity
105
- // `ps` probe transiently failed, so proceeding risks a rare double-launch —
106
- // but stranding a truly-dead node with no engine is the worse failure, and
107
- // recovering from it is far harder, so we bias toward relaunching.
108
- const live = getNode(nodeId) ?? meta;
109
- if (recordedPidLiveness(live.pi_pid, live.pi_pid_identity) === 'alive') {
110
- return {
111
- window: null,
112
- session: live.tmux_session ?? null,
113
- resumed: false,
114
- };
115
- }
116
- // Every (re)launch is a new cycle — bump the counter so CRTR_CYCLES and the
117
- // footer stay in sync. Mutate the in-memory meta too so buildPiArgv below
118
- // launches with the incremented count.
119
- meta.cycles = (meta.cycles ?? 0) + 1;
120
- updateNode(nodeId, { cycles: meta.cycles });
121
- // Decide whether to wake the saved pi conversation, cycle it in place (a
122
- // refresh-yield reusing the same `.jsonl`), or start fresh. resumeArgs
123
- // resumes/cycles ONLY by the absolute session-file path (cwd-immune) and ONLY
124
- // when that .jsonl actually exists; there is no bare-id fallback (the broker
125
- // preflight rejects a bare `--session`). No file on disk yet ⇒ a fresh launch.
126
- const resume = resumeArgs(meta, opts.resume);
127
- // hasSessionPath: there is a real `.jsonl` to hand pi via `--session`, whether
128
- // as a true resume OR a cycling refresh-yield (resumeArgs sets `newCycle` in
129
- // the latter case). cycling and resuming are mutually exclusive; `resuming`
130
- // keeps the STRICT true-resume-only meaning `ReviveResult.resumed` and the
131
- // launch-placement hint below have always carried.
132
- const hasSessionPath = resume.resumeSessionPath !== undefined;
133
- const cycling = hasSessionPath && resume.newCycle === true;
134
- const resuming = hasSessionPath && !cycling;
135
- // Mirror THIS launch attempt's cycling-ness durably, BEFORE launching, so a
136
- // pre-session_start crash-retry (crtrd.ts) can tell "was the attempt I'm
137
- // retrying a cycle" apart from an ordinary resume/crash. Cleared by
138
- // canvas-stophook's session_start handler the instant this boot is confirmed.
139
- // Written unconditionally (true for cycling, false otherwise) so it never
140
- // goes stale across launch kinds.
141
- updateNode(nodeId, { cycle_pending: cycling });
142
- meta.cycle_pending = cycling;
143
- // A truly fresh launch — no `.jsonl` to resume OR cycle from — starts a NEW
144
- // pi session, so the old session identity is dead. Clear it from meta NOW,
145
- // before launch, so the recorded state matches what we're about to do. This
146
- // is load-bearing for boot-failure classification: if this fresh broker dies
147
- // BEFORE session_start (recording no new pid and no new session), the
148
- // daemon's pid-null branch keys on pi_session_id — a leftover stale id would
149
- // be misread as a "stranded relaunch" and retried forever instead of surfaced
150
- // via transition('crash') + surfaceBootFailure. Clearing it makes the failure
151
- // show as pid=NULL + session=NULL → boot failure, the correct terminal
152
- // outcome. A cycling refresh keeps BOTH ids — the `.jsonl` and its recorded
153
- // session id are exactly what's being reused — so a cycling broker that dies
154
- // pre-session_start reads as a "stranded relaunch" and is retried, same class
155
- // as a resume revive today.
156
- if (!hasSessionPath && (meta.pi_session_id != null || meta.pi_session_file != null)) {
157
- updateNode(nodeId, { pi_session_id: null, pi_session_file: null });
158
- meta.pi_session_id = null;
159
- meta.pi_session_file = null;
160
- }
161
- // A true resume needs no kickoff — the saved conversation just continues. A
162
- // fresh revive OR a cycling refresh-yield both get one so the (re-rooted)
163
- // window re-reads its roadmap and continues: drainBearings is the one-shot
164
- // consuming step (yield note + feed cursor + persona ack); the builder is
165
- // then pure. `resume` carries `resumeSessionPath`/`newCycle` through
166
- // unchanged (empty for true-fresh, both set for cycling) so buildPiArgv emits
167
- // the right argv either way.
168
- let inv;
169
- if (resuming) {
170
- inv = buildPiArgv(meta, resume);
171
- }
172
- else {
173
- // Fresh or cycling: drain the one-shot bearings, then build the kickoff.
174
- // When a scheduled bare self-alarm drove this revive, opts.wakeReason
175
- // carries the wake provenance so the kickoff leads with a <crtr-wake> block
176
- // ("a timer woke you"); every other reviveNode caller passes nothing → no
177
- // block.
178
- const bearings = drainBearings(meta);
179
- inv = buildPiArgv(meta, {
180
- ...resume,
181
- prompt: buildReviveKickoff(meta, bearings, opts.wakeReason),
182
- });
183
- // Fresh or cycling revive starts a NEW LLM context — reset the on-read doc
184
- // dedup so it surfaces docs from scratch (a true resume above instead KEEPS
185
- // the persisted set, continuing the same transcript).
186
- clearInjectedDocs(nodeId);
187
- }
188
- // The broker host launches the detached engine. reviveNode keeps
189
- // transition+clearPid around it (the crash-safety ordering, unchanged). It
190
- // opens NO viewer — engine-only; existing viewers reconnect, and a viewer-less
191
- // node is brought on screen by the next `focus`.
192
- transition(nodeId, 'revive');
193
- // Clear any active fault the instant this node is (re)launched — don't wait
194
- // for the fresh agent_start. A revive (auto or on-demand) is the recovery
195
- // this marker advertised.
196
- clearFault(nodeId);
197
- // Cancel-on-wake (design §6.4, AC-E1): every revive-for-any-reason (an inbox
198
- // event, a different wake, a manual focus) drops this node's pending deadline,
199
- // so the deadline always belongs to the dormancy being left. Writes only the
200
- // wakeups table, after the atomic transition above.
201
- cancelDeadlinesFor(nodeId);
202
- let launched;
92
+ // Cross-invocation revive lock (the duplicate-broker leak this fixes — live
93
+ // incident: repeated revive attempts against one node under socket thrash
94
+ // left MULTIPLE live `broker-cli` processes for the same node id, with
95
+ // canvas `pi_pid` overwritten and the older brokers untracked/leaked). The
96
+ // guard below reads `pi_pid` liveness and then, several statements later,
97
+ // launches + records a NEW pid a classic check-then-act race. Two
98
+ // `reviveNode` calls for the SAME node (typically two separate OS processes:
99
+ // a retried `crtr node lifecycle revive`/web-relay bridge call racing the
100
+ // daemon's own auto-revive tick, or a burst of reconnect retries) can both
101
+ // read the SAME stale not-alive pid before either one's `recordPid` commits,
102
+ // both launch a broker, and the second `recordPid` silently overwrites the
103
+ // first's pid leaking the first broker as an untracked live process
104
+ // forever. A `BEGIN IMMEDIATE` write transaction around the guard-check
105
+ // through `recordPid` serializes every `reviveNode` call sharing this
106
+ // canvas.db (same process or a concurrent one SQLite's write lock plus the
107
+ // WAL/`busy_timeout` already set on every connection, see db.ts): whichever
108
+ // call wins the lock finishes its guard check, launch, and `recordPid`
109
+ // BEFORE the next contender's guard check ever runs, so a second contender
110
+ // always sees the freshly-recorded ALIVE pid and no-ops instead of
111
+ // double-launching. Committed UNCONDITIONALLY in `finally` — never rolled
112
+ // back because every write inside (the cycle bump, the crash-path
113
+ // `transition('crash')`/`clearPid` on a launch failure, etc.) must persist
114
+ // exactly as it would with no transaction at all (each statement
115
+ // auto-committing today); this adds only the missing mutual exclusion, it
116
+ // changes no write's durability or the function's existing throw/return
117
+ // semantics.
118
+ const db = openDb();
119
+ db.exec('BEGIN IMMEDIATE');
203
120
  try {
204
- launched = headlessBrokerHost.launch(nodeId, inv, {
205
- cwd: meta.cwd,
206
- name: fullName(meta),
207
- resuming,
208
- });
209
- }
210
- catch (error) {
121
+ // Double-revive guard: identity-aware liveness on pi_pid. Only a
122
+ // CONFIRMED-dead read proceeds to relaunch; `alive` AND `indeterminate`
123
+ // both no-op. A node whose broker pid is still running as that same broker
124
+ // was already revived by another path — re-launching would put a SECOND
125
+ // broker on the same session file.
126
+ //
127
+ // Why `indeterminate` must NOT relaunch (live incident, 2026-07-10, 4GB
128
+ // Hearth home): the `dead` verdict comes from the fork-free signal-0
129
+ // syscall (`isPidAlive`), which cannot fail under resource pressure — a
130
+ // truly dead broker ALWAYS reads `dead`. `indeterminate` therefore means
131
+ // "the pid EXISTS but the `ps` identity probe failed" — and the probe is a
132
+ // fork+exec, so it fails exactly when the host is out of memory (ENOMEM)
133
+ // or thrashing (>2s timeout). Those failures are CORRELATED and
134
+ // persistent, not transient: under sustained OOM every guard read went
135
+ // `indeterminate`, every daemon grace window relaunched a broker whose
136
+ // predecessor was still alive, and `recordPid` below overwrote the row's
137
+ // pid — leaking each older broker untracked. That compounded to 7 live
138
+ // engines on one node (35 total on the home), which itself sustained the
139
+ // OOM that kept the probes failing. Relaunching on uncertainty is
140
+ // self-amplifying in precisely the regime where uncertainty occurs.
141
+ //
142
+ // The cost of no-opping on `indeterminate` is bounded and small: the only
143
+ // dead-but-indeterminate cases are a zombie or a reused pid whose `ps`
144
+ // probe failed — both re-read next daemon tick, and read `dead` the
145
+ // moment a probe succeeds. `isRecordedPidAlive` is the exact adapter for
146
+ // this direction (false ONLY on confirmed `dead`).
147
+ const live = getNode(nodeId) ?? meta;
148
+ if (isRecordedPidAlive(live.pi_pid, live.pi_pid_identity)) {
149
+ return {
150
+ window: null,
151
+ session: live.tmux_session ?? null,
152
+ resumed: false,
153
+ };
154
+ }
155
+ // Every (re)launch is a new cycle — bump the counter so CRTR_CYCLES and the
156
+ // footer stay in sync. Mutate the in-memory meta too so buildPiArgv below
157
+ // launches with the incremented count.
158
+ meta.cycles = (meta.cycles ?? 0) + 1;
159
+ updateNode(nodeId, { cycles: meta.cycles });
160
+ // Decide whether to wake the saved pi conversation, cycle it in place (a
161
+ // refresh-yield reusing the same `.jsonl`), or start fresh. resumeArgs
162
+ // resumes/cycles ONLY by the absolute session-file path (cwd-immune) and ONLY
163
+ // when that .jsonl actually exists; there is no bare-id fallback (the broker
164
+ // preflight rejects a bare `--session`). No file on disk yet ⇒ a fresh launch.
165
+ const resume = resumeArgs(meta, opts.resume);
166
+ // hasSessionPath: there is a real `.jsonl` to hand pi via `--session`, whether
167
+ // as a true resume OR a cycling refresh-yield (resumeArgs sets `newCycle` in
168
+ // the latter case). cycling and resuming are mutually exclusive; `resuming`
169
+ // keeps the STRICT true-resume-only meaning `ReviveResult.resumed` and the
170
+ // launch-placement hint below have always carried.
171
+ const hasSessionPath = resume.resumeSessionPath !== undefined;
172
+ const cycling = hasSessionPath && resume.newCycle === true;
173
+ const resuming = hasSessionPath && !cycling;
174
+ // Mirror THIS launch attempt's cycling-ness durably, BEFORE launching, so a
175
+ // pre-session_start crash-retry (crtrd.ts) can tell "was the attempt I'm
176
+ // retrying a cycle" apart from an ordinary resume/crash. Cleared by
177
+ // canvas-stophook's session_start handler the instant this boot is confirmed.
178
+ // Written unconditionally (true for cycling, false otherwise) so it never
179
+ // goes stale across launch kinds.
180
+ updateNode(nodeId, { cycle_pending: cycling });
181
+ meta.cycle_pending = cycling;
182
+ // A truly fresh launch — no `.jsonl` to resume OR cycle from — starts a NEW
183
+ // pi session, so the old session identity is dead. Clear it from meta NOW,
184
+ // before launch, so the recorded state matches what we're about to do. This
185
+ // is load-bearing for boot-failure classification: if this fresh broker dies
186
+ // BEFORE session_start (recording no new pid and no new session), the
187
+ // daemon's pid-null branch keys on pi_session_id — a leftover stale id would
188
+ // be misread as a "stranded relaunch" and retried forever instead of surfaced
189
+ // via transition('crash') + surfaceBootFailure. Clearing it makes the failure
190
+ // show as pid=NULL + session=NULL → boot failure, the correct terminal
191
+ // outcome. A cycling refresh keeps BOTH ids — the `.jsonl` and its recorded
192
+ // session id are exactly what's being reused — so a cycling broker that dies
193
+ // pre-session_start reads as a "stranded relaunch" and is retried, same class
194
+ // as a resume revive today.
195
+ if (!hasSessionPath && (meta.pi_session_id != null || meta.pi_session_file != null)) {
196
+ updateNode(nodeId, { pi_session_id: null, pi_session_file: null });
197
+ meta.pi_session_id = null;
198
+ meta.pi_session_file = null;
199
+ }
200
+ // A true resume needs no kickoff — the saved conversation just continues. A
201
+ // fresh revive OR a cycling refresh-yield both get one so the (re-rooted)
202
+ // window re-reads its roadmap and continues: drainBearings is the one-shot
203
+ // consuming step (yield note + feed cursor + persona ack); the builder is
204
+ // then pure. `resume` carries `resumeSessionPath`/`newCycle` through
205
+ // unchanged (empty for true-fresh, both set for cycling) so buildPiArgv emits
206
+ // the right argv either way.
207
+ let inv;
208
+ if (resuming) {
209
+ inv = buildPiArgv(meta, resume);
210
+ }
211
+ else {
212
+ // Fresh or cycling: drain the one-shot bearings, then build the kickoff.
213
+ // When a scheduled bare self-alarm drove this revive, opts.wakeReason
214
+ // carries the wake provenance so the kickoff leads with a <crtr-wake> block
215
+ // ("a timer woke you"); every other reviveNode caller passes nothing → no
216
+ // block.
217
+ const bearings = drainBearings(meta);
218
+ inv = buildPiArgv(meta, {
219
+ ...resume,
220
+ prompt: buildReviveKickoff(meta, bearings, opts.wakeReason),
221
+ });
222
+ // Fresh or cycling revive starts a NEW LLM context — reset the on-read doc
223
+ // dedup so it surfaces docs from scratch (a true resume above instead KEEPS
224
+ // the persisted set, continuing the same transcript).
225
+ clearInjectedDocs(nodeId);
226
+ }
227
+ // The broker host launches the detached engine. reviveNode keeps
228
+ // transition+clearPid around it (the crash-safety ordering, unchanged). It
229
+ // opens NO viewer — engine-only; existing viewers reconnect, and a viewer-less
230
+ // node is brought on screen by the next `focus`.
231
+ transition(nodeId, 'revive');
232
+ // Clear any active fault the instant this node is (re)launched — don't wait
233
+ // for the fresh agent_start. A revive (auto or on-demand) is the recovery
234
+ // this marker advertised.
235
+ clearFault(nodeId);
236
+ // Cancel-on-wake (design §6.4, AC-E1): every revive-for-any-reason (an inbox
237
+ // event, a different wake, a manual focus) drops this node's pending deadline,
238
+ // so the deadline always belongs to the dormancy being left. Writes only the
239
+ // wakeups table, after the atomic transition above.
240
+ cancelDeadlinesFor(nodeId);
241
+ let launched;
211
242
  try {
212
- transition(nodeId, 'crash');
213
- // Doctrine wake: a launch-refusal here marks the node dead with NOTHING
214
- // else fanning that out — the daemon's own auto-revive is the most common
215
- // caller (superviseTick, best-effort try/catch, stderr-only on failure), so
216
- // without this a manager waiting dormant on this child hangs forever,
217
- // never told it will not come back on its own. Mirrors closeNode's step-4
218
- // fan-out (same subscribers table, same active/passive split).
219
- fanDoctrineWake(nodeId, subscribersOf(nodeId), `Child crashed — ${fullName(meta)} (${nodeId}) failed to relaunch and is now dead: ${error.message}\n\n` +
220
- `It stays dead until you revive it — \`crtr node lifecycle revive ${nodeId}\`.`, { reason: 'child-crashed', child: nodeId });
243
+ launched = headlessBrokerHost.launch(nodeId, inv, {
244
+ cwd: meta.cwd,
245
+ name: fullName(meta),
246
+ resuming,
247
+ });
221
248
  }
222
- catch {
223
- /* best-effort cleanup */
249
+ catch (error) {
250
+ try {
251
+ transition(nodeId, 'crash');
252
+ // Doctrine wake: a launch-refusal here marks the node dead with NOTHING
253
+ // else fanning that out — the daemon's own auto-revive is the most common
254
+ // caller (superviseTick, best-effort try/catch, stderr-only on failure), so
255
+ // without this a manager waiting dormant on this child hangs forever,
256
+ // never told it will not come back on its own. Mirrors closeNode's step-4
257
+ // fan-out (same subscribers table, same active/passive split).
258
+ fanDoctrineWake(nodeId, subscribersOf(nodeId), `Child crashed — ${fullName(meta)} (${nodeId}) failed to relaunch and is now dead: ${error.message}\n\n` +
259
+ `It stays dead until you revive it — \`crtr node lifecycle revive ${nodeId}\`.`, { reason: 'child-crashed', child: nodeId });
260
+ }
261
+ catch {
262
+ /* best-effort cleanup */
263
+ }
264
+ clearPid(nodeId);
265
+ throw new Error(`[broker] refusing to revive ${nodeId}: ${error.message}`);
224
266
  }
225
- clearPid(nodeId);
226
- throw new Error(`[broker] refusing to revive ${nodeId}: ${error.message}`);
267
+ // Record the real broker pid immediately, still inside the lock — this is
268
+ // the write that makes the guard's next reader (a contender unblocked from
269
+ // its own BEGIN IMMEDIATE the instant this transaction commits) see an
270
+ // ALIVE pid instead of racing past the same stale one this call just
271
+ // relaunched from. session_start will re-record the same pid after
272
+ // extension bind, but the daemon must supervise the process during the
273
+ // cold-boot window too; otherwise a slow-but-alive broker reads as
274
+ // pid=null+session=null and can be falsely crashed by the boot-grace path.
275
+ if (launched.pid !== null)
276
+ recordPid(nodeId, launched.pid);
277
+ return { window: null, session: meta.tmux_session ?? null, resumed: resuming, launch: launched };
278
+ }
279
+ finally {
280
+ db.exec('COMMIT');
227
281
  }
228
- // Record the real broker pid immediately. session_start will re-record the
229
- // same pid after extension bind, but the daemon must supervise the process
230
- // during the cold-boot window too; otherwise a slow-but-alive broker reads as
231
- // pid=null+session=null and can be falsely crashed by the boot-grace path.
232
- if (launched.pid !== null)
233
- recordPid(nodeId, launched.pid);
234
- return { window: null, session: meta.tmux_session ?? null, resumed: resuming, launch: launched };
235
282
  }
@@ -67,7 +67,10 @@ function ensureBracketedPaste() {
67
67
  const current = tmux(['show-options', '-s', 'terminal-overrides']).stdout;
68
68
  if (current.includes('Enbp='))
69
69
  return;
70
- tmux(['set-option', '-sga', 'terminal-overrides', '*:Enbp=\E[?2004h:Dsbp=\E[?2004l']);
70
+ // Emit a LITERAL backslash-E (`\\E`) so tmux's option parser resolves it to ESC.
71
+ // A single `\E` in this JS source collapses to the letter `E`, which stores a
72
+ // broken `Enbp=E[?2004h` (no ESC) and silently disables bracketed paste.
73
+ tmux(['set-option', '-sga', 'terminal-overrides', '*:Enbp=\\E[?2004h:Dsbp=\\E[?2004l']);
71
74
  }
72
75
  /** Create a detached session rooted at `cwd` if it doesn't exist. The session
73
76
  * name is a root's tmux home; every node under that root is a window in it. */
@@ -25,8 +25,12 @@ function startStalledServer() {
25
25
  });
26
26
  });
27
27
  }
28
- test('a stalled endpoint returns a timeout failure within the configured bound instead of hanging forever', async () => {
28
+ test('a stalled endpoint returns a timeout failure within the configured bound instead of hanging forever', async (t) => {
29
29
  const server = await startStalledServer();
30
+ // Register the close BEFORE any assertion runs — a listening server left
31
+ // open by a thrown assertion keeps this test file's process alive forever
32
+ // (see the CI-hang postmortem on rebuild-coalescer.test.ts).
33
+ t.after(() => server.close());
30
34
  const exec = createRemoteExec(server.url, 'tok', 100); // small timeout for the test
31
35
  const started = Date.now();
32
36
  // Race against a generous outer deadline: if the transport doesn't
@@ -40,9 +44,8 @@ test('a stalled endpoint returns a timeout failure within the configured bound i
40
44
  assert.equal(result.ok, false);
41
45
  assert.match(result.stderr, /timeout/i);
42
46
  assert.ok(elapsed < 1000, `expected the timeout to fire near the configured 100ms bound, took ${elapsed}ms`);
43
- await server.close();
44
47
  });
45
- test('a normal fast response is unaffected by the timeout wiring', async () => {
48
+ test('a normal fast response is unaffected by the timeout wiring', async (t) => {
46
49
  const server = createServer((req, res) => {
47
50
  const chunks = [];
48
51
  req.on('data', (c) => chunks.push(c));
@@ -51,6 +54,7 @@ test('a normal fast response is unaffected by the timeout wiring', async () => {
51
54
  res.end(JSON.stringify({ ok: true, exitCode: 0, stdout: JSON.stringify({ nodes: [] }), stderr: '' }));
52
55
  });
53
56
  });
57
+ t.after(() => new Promise((r) => server.close(() => r())));
54
58
  await new Promise((r) => server.listen(0, '127.0.0.1', r));
55
59
  const addr = server.address();
56
60
  const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
@@ -58,7 +62,6 @@ test('a normal fast response is unaffected by the timeout wiring', async () => {
58
62
  const result = await exec('crtr', ['--json', 'node', 'inspect', 'list']);
59
63
  assert.equal(result.ok, true);
60
64
  assert.equal(result.stdout, JSON.stringify({ nodes: [] }));
61
- await new Promise((r) => server.close(() => r()));
62
65
  });
63
66
  // --- Phase 3 review Minor 1: no token bytes may ever reach returned stderr ---
64
67
  test('an unreachable endpoint returns a flat generic network-error message, never the raw exception text', async () => {
@@ -75,7 +78,7 @@ test('a malformed token (contains a newline) is rejected up front with a generic
75
78
  assert.ok(!result.stderr.includes('bad\ntoken'));
76
79
  assert.ok(!result.stderr.includes('bad'));
77
80
  });
78
- test('a valid token containing typical bearer-token punctuation (-, _, .) still round-trips against a real server', async () => {
81
+ test('a valid token containing typical bearer-token punctuation (-, _, .) still round-trips against a real server', async (t) => {
79
82
  const server = createServer((req, res) => {
80
83
  const chunks = [];
81
84
  req.on('data', (c) => chunks.push(c));
@@ -84,6 +87,7 @@ test('a valid token containing typical bearer-token punctuation (-, _, .) still
84
87
  res.end(JSON.stringify({ ok: true, exitCode: 0, stdout: JSON.stringify({ nodes: [] }), stderr: '' }));
85
88
  });
86
89
  });
90
+ t.after(() => new Promise((r) => server.close(() => r())));
87
91
  await new Promise((r) => server.listen(0, '127.0.0.1', r));
88
92
  const addr = server.address();
89
93
  const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
@@ -91,5 +95,4 @@ test('a valid token containing typical bearer-token punctuation (-, _, .) still
91
95
  const result = await exec('crtr', ['--json', 'node', 'inspect', 'list']);
92
96
  assert.equal(result.ok, true);
93
97
  assert.equal(result.stdout, JSON.stringify({ nodes: [] }));
94
- await new Promise((r) => server.close(() => r()));
95
98
  });
@@ -38,9 +38,18 @@ function sendExec(req, baseCwd) {
38
38
  resolve({ ok: true, exitCode: code, stdout: stdout ?? '', stderr: stderr ?? '' });
39
39
  return;
40
40
  }
41
- if (err.signal) {
42
- // Killed by signal: it ran; surface as a non-zero exit.
43
- resolve({ ok: true, exitCode: 1, stdout: stdout ?? '', stderr: stderr ?? '' });
41
+ const signal = err.signal;
42
+ if (signal) {
43
+ // Killed by signal: it ran; surface as a non-zero exit. A SIGKILL with
44
+ // no stderr at all (the live-incident shape: a guest OOM killer takes
45
+ // out the child before it ever writes anything) used to come back as
46
+ // a bare `failed (1)` with nothing to explain it — the signal itself
47
+ // IS the useful diagnostic here, so surface it when stderr is empty
48
+ // rather than discarding it.
49
+ const detail = stderr && stderr.trim() !== ''
50
+ ? stderr
51
+ : `killed by signal ${signal}${signal === 'SIGKILL' ? ' (often out-of-memory)' : ''}`;
52
+ resolve({ ok: true, exitCode: 1, stdout: stdout ?? '', stderr: detail });
44
53
  return;
45
54
  }
46
55
  // Spawn-level failure (ENOENT/EACCES/maxBuffer): transport failure.
@@ -1 +1 @@
1
- export {};
1
+ import '../suppress-experimental-warnings.js';
@@ -1,4 +1,5 @@
1
1
  // crtrd entry point — spawned detached by `crtr sys daemon start` and by bin/crtrd.
2
2
  // Calls runDaemon() and never returns (the loop drives via setTimeout).
3
+ import '../suppress-experimental-warnings.js';
3
4
  import { runDaemon } from './crtrd.js';
4
5
  runDaemon();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ const originalEmit = process.emit;
2
+ process.emit = function (name, ...args) {
3
+ const [data] = args;
4
+ if (name === 'warning' &&
5
+ data instanceof Error &&
6
+ data.name === 'ExperimentalWarning' &&
7
+ data.message.includes('SQLite')) {
8
+ return false;
9
+ }
10
+ return originalEmit.apply(this, [
11
+ name,
12
+ ...args,
13
+ ]);
14
+ };
15
+ export {};
package/dist/types.js CHANGED
@@ -57,12 +57,12 @@ export function defaultKindsConfig() {
57
57
  },
58
58
  explore: {
59
59
  whenToUse: 'Map unfamiliar code or architecture — read-only orientation and code-path research with concrete file:line evidence. Anything diagnostic — investigating a failure, why something is broken or misbehaving, or live/runtime behavior — is advisor, not explore.',
60
- model: 'anthropic/light',
60
+ model: 'openai/light',
61
61
  },
62
62
  developer: {
63
63
  whenToUse: 'Implement a change — make the feature or fix genuinely work against its acceptance criteria, not merely compile.',
64
- model: 'anthropic/medium',
65
- orchestratorModel: 'anthropic/strong',
64
+ model: 'openai/medium',
65
+ orchestratorModel: 'openai/strong',
66
66
  },
67
67
  design: {
68
68
  whenToUse: 'Architect a solution — produce one design document an implementer can build from without re-deciding anything left open.',
@@ -150,15 +150,15 @@ export function defaultModelLaddersConfig() {
150
150
  return {
151
151
  anthropic: {
152
152
  ultra: 'anthropic/claude-fable-5:high',
153
- strong: 'anthropic/claude-opus-4-8:high',
154
- medium: 'anthropic/claude-sonnet-5:high',
155
- light: 'anthropic/claude-haiku-4-5:high',
153
+ strong: 'anthropic/claude-fable-5:medium',
154
+ medium: 'anthropic/claude-opus-4-8:high',
155
+ light: 'anthropic/claude-sonnet-5:high',
156
156
  },
157
157
  openai: {
158
- ultra: 'openai-codex/gpt-5.5:xhigh',
159
- strong: 'openai-codex/gpt-5.5:high',
160
- medium: 'openai-codex/gpt-5.5:medium',
161
- light: 'openai-codex/gpt-5.3-codex-spark',
158
+ ultra: 'openai-codex/gpt-5.6-sol:max',
159
+ strong: 'openai-codex/gpt-5.6-sol:high',
160
+ medium: 'openai-codex/gpt-5.6-terra:medium',
161
+ light: 'openai-codex/gpt-5.6-luna:low',
162
162
  },
163
163
  };
164
164
  }