@crouton-kit/crouter 0.3.67 → 0.3.68
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/clients/attach/attach-cmd.js +124 -124
- package/dist/commands/capture.d.ts +2 -0
- package/dist/commands/capture.js +28 -0
- package/dist/core/__tests__/revive.test.js +70 -1
- package/dist/core/command.d.ts +15 -0
- package/dist/core/command.js +72 -0
- package/dist/core/profiles/select.d.ts +4 -1
- package/dist/core/profiles/select.js +356 -81
- package/dist/core/runtime/revive.js +176 -139
- package/dist/core/runtime/tmux.js +4 -1
- package/dist/core/view/transport-local.js +12 -3
- package/dist/web-client/assets/{index-BvzxXXGU.js → index-CnF5r8ky.js} +18 -18
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@
|
|
|
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';
|
|
@@ -89,147 +89,184 @@ 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
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// `
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
121
|
+
// Double-revive guard: identity-aware liveness on pi_pid, requiring
|
|
122
|
+
// CONFIRMED alive — not the fail-open-to-alive boolean adapter. A node
|
|
123
|
+
// whose broker pid is CONFIRMED still running as that same broker (not a
|
|
124
|
+
// reused pid) was already revived by another path — re-launching would put
|
|
125
|
+
// a SECOND broker on the same session file. No-op. A bare isPidAlive would
|
|
126
|
+
// misread a reused pid (heavy forking, esp. Linux) as "still alive" and
|
|
127
|
+
// skip the relaunch forever; conversely, treating an INDETERMINATE read
|
|
128
|
+
// (can't confirm either way, e.g. a transient `ps` probe failure) as
|
|
129
|
+
// "alive" here would risk the opposite failure — silently skipping a
|
|
130
|
+
// needed relaunch and stranding the node with no engine. So this guard
|
|
131
|
+
// blocks ONLY on a positively confirmed-alive read; `dead` AND
|
|
132
|
+
// `indeterminate` both proceed to relaunch. We accept that tradeoff: an
|
|
133
|
+
// `indeterminate` read CAN come from a genuinely live broker whose identity
|
|
134
|
+
// `ps` probe transiently failed, so proceeding risks a rare double-launch —
|
|
135
|
+
// but stranding a truly-dead node with no engine is the worse failure, and
|
|
136
|
+
// recovering from it is far harder, so we bias toward relaunching.
|
|
137
|
+
const live = getNode(nodeId) ?? meta;
|
|
138
|
+
if (recordedPidLiveness(live.pi_pid, live.pi_pid_identity) === 'alive') {
|
|
139
|
+
return {
|
|
140
|
+
window: null,
|
|
141
|
+
session: live.tmux_session ?? null,
|
|
142
|
+
resumed: false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Every (re)launch is a new cycle — bump the counter so CRTR_CYCLES and the
|
|
146
|
+
// footer stay in sync. Mutate the in-memory meta too so buildPiArgv below
|
|
147
|
+
// launches with the incremented count.
|
|
148
|
+
meta.cycles = (meta.cycles ?? 0) + 1;
|
|
149
|
+
updateNode(nodeId, { cycles: meta.cycles });
|
|
150
|
+
// Decide whether to wake the saved pi conversation, cycle it in place (a
|
|
151
|
+
// refresh-yield reusing the same `.jsonl`), or start fresh. resumeArgs
|
|
152
|
+
// resumes/cycles ONLY by the absolute session-file path (cwd-immune) and ONLY
|
|
153
|
+
// when that .jsonl actually exists; there is no bare-id fallback (the broker
|
|
154
|
+
// preflight rejects a bare `--session`). No file on disk yet ⇒ a fresh launch.
|
|
155
|
+
const resume = resumeArgs(meta, opts.resume);
|
|
156
|
+
// hasSessionPath: there is a real `.jsonl` to hand pi via `--session`, whether
|
|
157
|
+
// as a true resume OR a cycling refresh-yield (resumeArgs sets `newCycle` in
|
|
158
|
+
// the latter case). cycling and resuming are mutually exclusive; `resuming`
|
|
159
|
+
// keeps the STRICT true-resume-only meaning `ReviveResult.resumed` and the
|
|
160
|
+
// launch-placement hint below have always carried.
|
|
161
|
+
const hasSessionPath = resume.resumeSessionPath !== undefined;
|
|
162
|
+
const cycling = hasSessionPath && resume.newCycle === true;
|
|
163
|
+
const resuming = hasSessionPath && !cycling;
|
|
164
|
+
// Mirror THIS launch attempt's cycling-ness durably, BEFORE launching, so a
|
|
165
|
+
// pre-session_start crash-retry (crtrd.ts) can tell "was the attempt I'm
|
|
166
|
+
// retrying a cycle" apart from an ordinary resume/crash. Cleared by
|
|
167
|
+
// canvas-stophook's session_start handler the instant this boot is confirmed.
|
|
168
|
+
// Written unconditionally (true for cycling, false otherwise) so it never
|
|
169
|
+
// goes stale across launch kinds.
|
|
170
|
+
updateNode(nodeId, { cycle_pending: cycling });
|
|
171
|
+
meta.cycle_pending = cycling;
|
|
172
|
+
// A truly fresh launch — no `.jsonl` to resume OR cycle from — starts a NEW
|
|
173
|
+
// pi session, so the old session identity is dead. Clear it from meta NOW,
|
|
174
|
+
// before launch, so the recorded state matches what we're about to do. This
|
|
175
|
+
// is load-bearing for boot-failure classification: if this fresh broker dies
|
|
176
|
+
// BEFORE session_start (recording no new pid and no new session), the
|
|
177
|
+
// daemon's pid-null branch keys on pi_session_id — a leftover stale id would
|
|
178
|
+
// be misread as a "stranded relaunch" and retried forever instead of surfaced
|
|
179
|
+
// via transition('crash') + surfaceBootFailure. Clearing it makes the failure
|
|
180
|
+
// show as pid=NULL + session=NULL → boot failure, the correct terminal
|
|
181
|
+
// outcome. A cycling refresh keeps BOTH ids — the `.jsonl` and its recorded
|
|
182
|
+
// session id are exactly what's being reused — so a cycling broker that dies
|
|
183
|
+
// pre-session_start reads as a "stranded relaunch" and is retried, same class
|
|
184
|
+
// as a resume revive today.
|
|
185
|
+
if (!hasSessionPath && (meta.pi_session_id != null || meta.pi_session_file != null)) {
|
|
186
|
+
updateNode(nodeId, { pi_session_id: null, pi_session_file: null });
|
|
187
|
+
meta.pi_session_id = null;
|
|
188
|
+
meta.pi_session_file = null;
|
|
189
|
+
}
|
|
190
|
+
// A true resume needs no kickoff — the saved conversation just continues. A
|
|
191
|
+
// fresh revive OR a cycling refresh-yield both get one so the (re-rooted)
|
|
192
|
+
// window re-reads its roadmap and continues: drainBearings is the one-shot
|
|
193
|
+
// consuming step (yield note + feed cursor + persona ack); the builder is
|
|
194
|
+
// then pure. `resume` carries `resumeSessionPath`/`newCycle` through
|
|
195
|
+
// unchanged (empty for true-fresh, both set for cycling) so buildPiArgv emits
|
|
196
|
+
// the right argv either way.
|
|
197
|
+
let inv;
|
|
198
|
+
if (resuming) {
|
|
199
|
+
inv = buildPiArgv(meta, resume);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Fresh or cycling: drain the one-shot bearings, then build the kickoff.
|
|
203
|
+
// When a scheduled bare self-alarm drove this revive, opts.wakeReason
|
|
204
|
+
// carries the wake provenance so the kickoff leads with a <crtr-wake> block
|
|
205
|
+
// ("a timer woke you"); every other reviveNode caller passes nothing → no
|
|
206
|
+
// block.
|
|
207
|
+
const bearings = drainBearings(meta);
|
|
208
|
+
inv = buildPiArgv(meta, {
|
|
209
|
+
...resume,
|
|
210
|
+
prompt: buildReviveKickoff(meta, bearings, opts.wakeReason),
|
|
211
|
+
});
|
|
212
|
+
// Fresh or cycling revive starts a NEW LLM context — reset the on-read doc
|
|
213
|
+
// dedup so it surfaces docs from scratch (a true resume above instead KEEPS
|
|
214
|
+
// the persisted set, continuing the same transcript).
|
|
215
|
+
clearInjectedDocs(nodeId);
|
|
216
|
+
}
|
|
217
|
+
// The broker host launches the detached engine. reviveNode keeps
|
|
218
|
+
// transition+clearPid around it (the crash-safety ordering, unchanged). It
|
|
219
|
+
// opens NO viewer — engine-only; existing viewers reconnect, and a viewer-less
|
|
220
|
+
// node is brought on screen by the next `focus`.
|
|
221
|
+
transition(nodeId, 'revive');
|
|
222
|
+
// Clear any active fault the instant this node is (re)launched — don't wait
|
|
223
|
+
// for the fresh agent_start. A revive (auto or on-demand) is the recovery
|
|
224
|
+
// this marker advertised.
|
|
225
|
+
clearFault(nodeId);
|
|
226
|
+
// Cancel-on-wake (design §6.4, AC-E1): every revive-for-any-reason (an inbox
|
|
227
|
+
// event, a different wake, a manual focus) drops this node's pending deadline,
|
|
228
|
+
// so the deadline always belongs to the dormancy being left. Writes only the
|
|
229
|
+
// wakeups table, after the atomic transition above.
|
|
230
|
+
cancelDeadlinesFor(nodeId);
|
|
231
|
+
let launched;
|
|
211
232
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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 });
|
|
233
|
+
launched = headlessBrokerHost.launch(nodeId, inv, {
|
|
234
|
+
cwd: meta.cwd,
|
|
235
|
+
name: fullName(meta),
|
|
236
|
+
resuming,
|
|
237
|
+
});
|
|
221
238
|
}
|
|
222
|
-
catch {
|
|
223
|
-
|
|
239
|
+
catch (error) {
|
|
240
|
+
try {
|
|
241
|
+
transition(nodeId, 'crash');
|
|
242
|
+
// Doctrine wake: a launch-refusal here marks the node dead with NOTHING
|
|
243
|
+
// else fanning that out — the daemon's own auto-revive is the most common
|
|
244
|
+
// caller (superviseTick, best-effort try/catch, stderr-only on failure), so
|
|
245
|
+
// without this a manager waiting dormant on this child hangs forever,
|
|
246
|
+
// never told it will not come back on its own. Mirrors closeNode's step-4
|
|
247
|
+
// fan-out (same subscribers table, same active/passive split).
|
|
248
|
+
fanDoctrineWake(nodeId, subscribersOf(nodeId), `Child crashed — ${fullName(meta)} (${nodeId}) failed to relaunch and is now dead: ${error.message}\n\n` +
|
|
249
|
+
`It stays dead until you revive it — \`crtr node lifecycle revive ${nodeId}\`.`, { reason: 'child-crashed', child: nodeId });
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
/* best-effort cleanup */
|
|
253
|
+
}
|
|
254
|
+
clearPid(nodeId);
|
|
255
|
+
throw new Error(`[broker] refusing to revive ${nodeId}: ${error.message}`);
|
|
224
256
|
}
|
|
225
|
-
|
|
226
|
-
|
|
257
|
+
// Record the real broker pid immediately, still inside the lock — this is
|
|
258
|
+
// the write that makes the guard's next reader (a contender unblocked from
|
|
259
|
+
// its own BEGIN IMMEDIATE the instant this transaction commits) see an
|
|
260
|
+
// ALIVE pid instead of racing past the same stale one this call just
|
|
261
|
+
// relaunched from. session_start will re-record the same pid after
|
|
262
|
+
// extension bind, but the daemon must supervise the process during the
|
|
263
|
+
// cold-boot window too; otherwise a slow-but-alive broker reads as
|
|
264
|
+
// pid=null+session=null and can be falsely crashed by the boot-grace path.
|
|
265
|
+
if (launched.pid !== null)
|
|
266
|
+
recordPid(nodeId, launched.pid);
|
|
267
|
+
return { window: null, session: meta.tmux_session ?? null, resumed: resuming, launch: launched };
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
db.exec('COMMIT');
|
|
227
271
|
}
|
|
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
272
|
}
|
|
@@ -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
|
|
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. */
|
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|