faberun 0.14.0 → 0.16.0

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.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Packet repetition between a node's successive attempts — the R16
3
+ * measurement the deduplication decision waits for, and nothing more: this
4
+ * module counts bytes; it never deduplicates or reshapes a payload.
5
+ *
6
+ * It is a module of its own because render.mjs sits at the repository's
7
+ * 800-line ceiling (measured 2026-09-21: render.mjs 799, progress.mjs 724,
8
+ * final.mjs 202): the renderers gain a single line each, the computation
9
+ * lives here.
10
+ *
11
+ * A node's packet, per attempt, is the prompt file its worker invocation was
12
+ * dispatched with (`invocation.promptPath`) — the only per-attempt packet
13
+ * bytes a run persists. Paths resolve against the run directory; an absolute
14
+ * path is taken as written. "Repeated" between two successive attempts is
15
+ * their common byte prefix: the share a retry re-sends identically, the
16
+ * number a deduplication or prefix-cache decision would act on. A packet
17
+ * file that cannot be read leaves its attempt absent from the measurement —
18
+ * absent is never zero — and `packetsRead` beside `attempts` is how a reader
19
+ * sees the gap.
20
+ */
21
+ import { readFileSync } from "node:fs";
22
+ import { resolve } from "node:path";
23
+ import { compactTokens } from "../util.mjs";
24
+
25
+ /**
26
+ * @typedef {{attempts: number, packetsRead: number, packetBytes: number|null, repeatedBytes: number|null}} PacketRepetition
27
+ * `packetBytes` sums the packets that were readable; `repeatedBytes` sums the
28
+ * common prefix of each *adjacent* worker-invocation pair whose both packets
29
+ * were readable — `null` when no pair was measurable. When `packetsRead` is
30
+ * smaller than `attempts`, the totals cover only the packets still on disk.
31
+ */
32
+
33
+ /**
34
+ * Per node, from the invocation ledger alone, in contract order.
35
+ *
36
+ * @param {string} runDir
37
+ * @param {{id: string, invocations?: unknown}[]} nodes
38
+ * @returns {Map<string, PacketRepetition>}
39
+ */
40
+ export function packetRepetitionByNode(runDir, nodes) {
41
+ return new Map(nodes.map((node) => [node.id, packetRepetition(runDir, node.invocations)]));
42
+ }
43
+
44
+ /**
45
+ * @param {string} runDir
46
+ * @param {unknown} invocations
47
+ * @returns {PacketRepetition}
48
+ */
49
+ function packetRepetition(runDir, invocations) {
50
+ const ledger = Array.isArray(invocations) ? /** @type {Record<string, unknown>[]} */ (invocations) : [];
51
+ const workerPackets = ledger
52
+ .filter((invocation) => /** @type {{role?: unknown}} */ (invocation)?.role === "worker")
53
+ .map((invocation) => (typeof /** @type {{promptPath?: unknown}} */ (invocation).promptPath === "string"
54
+ ? readPacket(runDir, /** @type {string} */ (/** @type {{promptPath?: unknown}} */ (invocation).promptPath))
55
+ : null));
56
+ let repeatedBytes = /** @type {number|null} */ (null);
57
+ for (let index = 1; index < workerPackets.length; index += 1) {
58
+ const previous = workerPackets[index - 1];
59
+ const current = workerPackets[index];
60
+ // A gap in the ledger's packets breaks the pair rather than pairing
61
+ // across it: attempts i-1 and i+1 are not successive.
62
+ if (previous === null || current === null) continue;
63
+ repeatedBytes = (repeatedBytes ?? 0) + sharedPrefixBytes(previous, current);
64
+ }
65
+ const readable = workerPackets.filter((packetBytes) => packetBytes !== null);
66
+ return {
67
+ attempts: workerPackets.length,
68
+ packetsRead: readable.length,
69
+ packetBytes: readable.length ? readable.reduce((total, packet) => total + /** @type {Buffer} */ (packet).length, 0) : null,
70
+ repeatedBytes,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * @param {string} runDir
76
+ * @param {string} promptPath
77
+ * @returns {Buffer|null}
78
+ */
79
+ function readPacket(runDir, promptPath) {
80
+ try {
81
+ return readFileSync(resolve(runDir, promptPath));
82
+ } catch {
83
+ // A prompt file the run no longer carries (pruned, or a fixture's
84
+ // placeholder path) leaves that attempt absent from the measurement
85
+ // rather than counted as zero bytes; `packetsRead` shows the gap.
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /** @param {Buffer} left @param {Buffer} right @returns {number} */
91
+ function sharedPrefixBytes(left, right) {
92
+ const max = Math.min(left.length, right.length);
93
+ let index = 0;
94
+ while (index < max && left[index] === right[index]) index += 1;
95
+ return index;
96
+ }
97
+
98
+ /**
99
+ * The report's one-line exposure, appended to a totals line: per node with
100
+ * two or more attempts on record, the bytes its successive attempts re-sent
101
+ * identically of the packet bytes those attempts carried. A node with a
102
+ * single attempt has no successive pair and is rightly absent; a node whose
103
+ * packets cannot be read says `unmeasured` rather than a zero; a node whose
104
+ * measurement covers only some of its packets says `partial`.
105
+ *
106
+ * @param {Map<string, PacketRepetition>} repetition
107
+ * @returns {string} "" when no node has a successive pair to expose
108
+ */
109
+ export function packetRepetitionNote(repetition) {
110
+ const parts = [];
111
+ for (const [id, measured] of repetition) {
112
+ if (measured.attempts < 2) continue;
113
+ if (measured.repeatedBytes === null || measured.packetBytes === null) {
114
+ parts.push(`${id} unmeasured`);
115
+ continue;
116
+ }
117
+ const partial = measured.packetsRead < measured.attempts ? " (partial)" : "";
118
+ parts.push(`${id} ${compactTokens(measured.repeatedBytes)} of ${compactTokens(measured.packetBytes)}${partial}`);
119
+ }
120
+ return parts.length ? ` · packet repeat across attempts: ${parts.join(" · ")}` : "";
121
+ }
@@ -1,14 +1,8 @@
1
1
  /**
2
- * Two renderers sharing one module, so a notification and a campaign page can
3
- * never disagree about progress:
4
- *
5
- * `renderRunProgress` -- one message for a human, from a run's persisted
6
- * state plus one terminal event: campaign, phase and node with a glyph for
7
- * the node's outcome; percent and count done and left; this node's own span;
8
- * the phase's estimated remaining time, from settled spans alone; the next
9
- * node in the phase, or that the phase is complete; what the node delivered,
10
- * in the worker's own words; and cost by role plus the campaign's cumulative
11
- * cost.
2
+ * The campaign roll-up, and the numbers it shares with the notification
3
+ * message (`message.mjs`): the roll-up object itself, the remaining-time
4
+ * estimate and a node's span are exported from here and imported there, so
5
+ * the page and the message can never disagree about progress, cost or time.
12
6
  *
13
7
  * `renderCampaignProgress` -- the campaign-wide roll-up, answering the
14
8
  * owner's four questions from persisted state alone: what the campaign
@@ -36,9 +30,9 @@
36
30
  * per-role arithmetic the status payload uses for this run alone. Nothing
37
31
  * here recomputes what one of those already answers.
38
32
  */
39
- import { MARK, formatDuration, formatRole, readRunUsage, renderStatusJson, roleUsage } from "./render.mjs";
33
+ import { formatDuration, readRunUsage, renderStatusJson, roleUsage } from "./render.mjs";
40
34
  import { nodeSnapshotPath, readNodeSnapshot } from "../run/node-store.mjs";
41
- import { boundedUtf8, compactCost, finite } from "../util.mjs";
35
+ import { finite } from "../util.mjs";
42
36
  import { SETTLED, SUCCESS } from "../engine/prompts.mjs";
43
37
  import { campaignDir } from "../campaign/layout.mjs";
44
38
  import { readCampaign } from "../campaign/record.mjs";
@@ -47,66 +41,9 @@ import { existsSync, readFileSync } from "node:fs";
47
41
  import { dirname, join, resolve } from "node:path";
48
42
  import { runDirectory } from "../run/paths.mjs";
49
43
 
50
- /** @typedef {import("./render.mjs").StatusPayloadNode} StatusPayloadNode */
51
- /** @typedef {import("../notify/index.mjs").NotifyEvent} NotifyEvent */
52
-
53
- /**
54
- * The message's byte ceiling. A three-node run, both roles priced and a
55
- * two-sentence worker summary, measures ~430 bytes; this leaves headroom for
56
- * a long campaign/phase/node id triple while still bounding an adversarial
57
- * worker summary (the protocol's own 4 KiB ceiling), which is the only
58
- * unbounded block and the one truncated first.
59
- */
60
- export const PROGRESS_MESSAGE_MAX_BYTES = 2048;
61
44
 
62
45
  const NO_SUMMARY = "(no summary recorded)";
63
46
 
64
- /**
65
- * @param {string} runDir
66
- * @param {NotifyEvent} event
67
- * @returns {string}
68
- */
69
- export function renderRunProgress(runDir, event) {
70
- const payload = /** @type {{campaignId: string, usage: {costUsd: number|null}, roles: {worker: import("./render.mjs").RoleUsage, judge: import("./render.mjs").RoleUsage}, nodes: StatusPayloadNode[]}} */
71
- (JSON.parse(renderStatusJson(runDir)));
72
- const subject = subjectNode(payload.nodes, event.nodeId ?? null);
73
- const raw = readNodeSnapshot(runDir, subject.id);
74
- const phaseNodes = payload.nodes.filter((node) => node.phase === subject.phase);
75
- const total = payload.nodes.length;
76
- const done = payload.nodes.filter((node) => SETTLED.has(node.status)).length;
77
- const donePct = total ? Math.round((done / total) * 100) : 0;
78
-
79
- const lines = [
80
- `${MARK[subject.status] ?? "[?]"} campaign ${payload.campaignId} · phase ${subject.phase ?? "-"} · node ${subject.id}`,
81
- `${done}/${total} nodes done · ${donePct}% done · ${100 - donePct}% left`,
82
- `this node: ${spanOf(subject) ?? "-"}`,
83
- phaseEstimateLine(phaseNodes),
84
- nextNodeLine(phaseNodes),
85
- ];
86
- if (event.type === "attention") lines.push(attentionLine(runDir, subject, raw));
87
- const valueLineIndex = lines.push(valueLine(raw)) - 1;
88
- lines.push(costLine(runDir, payload));
89
-
90
- return boundToCeiling(lines, valueLineIndex);
91
- }
92
-
93
- /**
94
- * The event's own node when it names one and the run still has it, else the
95
- * most recently settled node (a `run.terminal` event names the run, not a
96
- * node), else the run's first node.
97
- *
98
- * @param {StatusPayloadNode[]} nodes
99
- * @param {string|null} nodeId
100
- * @returns {StatusPayloadNode}
101
- */
102
- function subjectNode(nodes, nodeId) {
103
- const named = nodeId ? nodes.find((node) => node.id === nodeId) : undefined;
104
- if (named) return named;
105
- const settled = nodes.filter((node) => SETTLED.has(node.status) && node.updatedAt);
106
- const latest = settled.sort((left, right) => String(left.updatedAt).localeCompare(String(right.updatedAt))).at(-1);
107
- return latest ?? nodes[0];
108
- }
109
-
110
47
  /** @param {{startedAt: string|null, updatedAt: string|null}} node @returns {number|null} */
111
48
  function spanMs(node) {
112
49
  if (!node.startedAt || !node.updatedAt) return null;
@@ -122,73 +59,43 @@ function spanOf(node) {
122
59
  }
123
60
 
124
61
  /**
125
- * The remaining-time estimate, shared by the single-run message and the
126
- * campaign roll-up so neither invents a second one: the mean span of the
127
- * settled nodes alone -- a running node's partial elapsed is never in
128
- * `SETTLED`, so it never drags the mean down and the estimate forward --
129
- * times how many nodes have not settled. `"complete"` when nothing is left;
130
- * `null` when nothing has settled to base the estimate on -- absent, never
131
- * zero, the same refusal this page applies to requirement coverage.
62
+ * The remaining-time estimate as a number, for a renderer that formats it its
63
+ * own way: `0` when nothing is left, `null` when nothing has settled to base
64
+ * it on, else the mean settled span times the unsettled count.
132
65
  *
133
- * @param {{status: string}[]} nodes every node the estimate covers
134
- * @param {{status: string, startedAt: string|null, updatedAt: string|null}[]} spans the nodes that carry the timestamps to average
135
- * @returns {string|null}
66
+ * @param {{status: string}[]} nodes
67
+ * @param {{status: string, startedAt: string|null, updatedAt: string|null}[]} spans
68
+ * @returns {number|null}
136
69
  */
137
- function remainingEstimate(nodes, spans) {
70
+ export function remainingEstimateMs(nodes, spans) {
138
71
  const unsettled = nodes.filter((node) => !SETTLED.has(node.status)).length;
139
- if (!unsettled) return "complete";
72
+ if (!unsettled) return 0;
140
73
  const settledSpans = spans.filter((node) => SETTLED.has(node.status)).map(spanMs).filter((ms) => ms !== null);
141
74
  if (!settledSpans.length) return null;
142
75
  const meanMs = settledSpans.reduce((total, ms) => total + /** @type {number} */ (ms), 0) / settledSpans.length;
143
- return `~${formatDuration(meanMs * unsettled)} remaining (from ${settledSpans.length} settled node${settledSpans.length === 1 ? "" : "s"})`;
144
- }
145
-
146
- /** @param {StatusPayloadNode[]} phaseNodes @returns {string} */
147
- function phaseEstimateLine(phaseNodes) {
148
- return `phase estimate: ${remainingEstimate(phaseNodes, phaseNodes) ?? "unknown (no settled node yet)"}`;
149
- }
150
-
151
- /** @param {StatusPayloadNode[]} phaseNodes @returns {string} */
152
- function nextNodeLine(phaseNodes) {
153
- const next = phaseNodes.find((node) => !SETTLED.has(node.status));
154
- return next ? `next: ${next.id}` : "next: phase complete";
76
+ return meanMs * unsettled;
155
77
  }
156
78
 
157
79
  /**
158
- * The worker's own words, never the renderer's: the raw snapshot's own
159
- * `result.summary`, labelled as such so a reader never mistakes it for the
160
- * renderer's judgment.
80
+ * The remaining-time estimate, shared by the notification message
81
+ * (`message.mjs`) and the campaign roll-up so neither invents a second one:
82
+ * the mean span of the settled nodes alone -- a running node's partial
83
+ * elapsed is never in `SETTLED`, so it never drags the mean down and the
84
+ * estimate forward -- times how many nodes have not settled. `"complete"`
85
+ * when nothing is left; `null` when nothing has settled to base the estimate
86
+ * on -- absent, never zero, the same refusal this page applies to requirement
87
+ * coverage.
161
88
  *
162
- * @param {Record<string, unknown>} raw
163
- * @returns {string}
164
- */
165
- function valueLine(raw) {
166
- const result = /** @type {{summary?: unknown}|null} */ (raw.result && typeof raw.result === "object" ? raw.result : null);
167
- const summary = typeof result?.summary === "string" && result.summary.trim() ? result.summary.trim() : null;
168
- return `worker says: ${summary ?? NO_SUMMARY}`;
169
- }
170
-
171
- /**
172
- * The attention variant names what must be decided and the exact command
173
- * that unblocks it, the way `src/report/next.mjs` does for its own items.
174
- *
175
- * @param {string} runDir
176
- * @param {StatusPayloadNode} subject
177
- * @param {Record<string, unknown>} raw
178
- * @returns {string}
89
+ * @param {{status: string}[]} nodes every node the estimate covers
90
+ * @param {{status: string, startedAt: string|null, updatedAt: string|null}[]} spans the nodes that carry the timestamps to average
91
+ * @returns {string|null}
179
92
  */
180
- function attentionLine(runDir, subject, raw) {
181
- const result = /** @type {{status?: unknown, summary?: unknown, missingContext?: unknown}|null} */ (raw.result && typeof raw.result === "object" ? raw.result : null);
182
- if (result?.status === "blocked_context") {
183
- const missing = Array.isArray(result.missingContext) ? result.missingContext.filter((entry) => typeof entry === "string") : [];
184
- const asked = missing.length ? ` (needs ${missing.join(", ")})` : "";
185
- return `decide: answer node ${subject.id}'s missing context${asked} → resume ${runDir} --answer ${subject.id}=<answer-file>`;
186
- }
187
- const gate = /** @type {{findings?: unknown}|null} */ (raw.gate && typeof raw.gate === "object" ? raw.gate : null);
188
- if (subject.status === "exhausted" && Array.isArray(gate?.findings) && gate.findings.length) {
189
- return `decide: review node ${subject.id}'s gate findings → findings ${runDir}`;
190
- }
191
- return `decide: resolve node ${subject.id} (${subject.status}) → status ${runDir}`;
93
+ export function remainingEstimate(nodes, spans) {
94
+ const ms = remainingEstimateMs(nodes, spans);
95
+ if (ms === 0) return "complete";
96
+ if (ms === null) return null;
97
+ const settled = spans.filter((node) => SETTLED.has(node.status) && spanMs(node) !== null).length;
98
+ return `~${formatDuration(ms)} remaining (from ${settled} settled node${settled === 1 ? "" : "s"})`;
192
99
  }
193
100
 
194
101
  /**
@@ -220,33 +127,6 @@ function campaignCumulativeCostUsd(runsDir, campaignId) {
220
127
  return total;
221
128
  }
222
129
 
223
- /**
224
- * @param {string} runDir
225
- * @param {{campaignId: string, roles: {worker: import("./render.mjs").RoleUsage, judge: import("./render.mjs").RoleUsage}}} payload
226
- * @returns {string}
227
- */
228
- function costLine(runDir, payload) {
229
- const cumulative = campaignCumulativeCostUsd(dirname(runDir), payload.campaignId);
230
- return `cost — worker ${formatRole(payload.roles.worker)} · judge ${formatRole(payload.roles.judge)} · campaign total ${compactCost(cumulative)}`;
231
- }
232
-
233
- /**
234
- * The value block is the only unbounded part of the message, so it is the
235
- * only one truncated when the whole message would exceed the ceiling.
236
- *
237
- * @param {string[]} lines
238
- * @param {number} valueLineIndex
239
- * @returns {string}
240
- */
241
- function boundToCeiling(lines, valueLineIndex) {
242
- const overflow = Buffer.byteLength(lines.join("\n"), "utf8") - PROGRESS_MESSAGE_MAX_BYTES;
243
- if (overflow <= 0) return lines.join("\n");
244
- const valueLineBytes = Buffer.byteLength(lines[valueLineIndex], "utf8");
245
- const trimmed = [...lines];
246
- trimmed[valueLineIndex] = boundedUtf8(lines[valueLineIndex], Math.max(0, valueLineBytes - overflow));
247
- return trimmed.join("\n");
248
- }
249
-
250
130
  /**
251
131
  * @typedef {{id: string, dependsOn: string[], status: string, attempt: number, workerRuntime: string|null, judgeRuntime: string|null, elapsedSpan: string|null, costUsd: number|null, runDir: string|null, workerLogPath: string|null, judgeLogPaths: string[], verificationRecordPath: string|null, attemptBranch: string|null, sealCommit: string|null}} RollupNode
252
132
  * `status` carries every `NodeStatus` plus two the manifest alone can
@@ -292,7 +172,7 @@ export function renderCampaignProgress(runsDir, campaignId, now = Date.now()) {
292
172
  * @param {number} now
293
173
  * @returns {Record<string, unknown>}
294
174
  */
295
- function buildCampaignProgress(runsDir, campaignId, now) {
175
+ export function buildCampaignProgress(runsDir, campaignId, now) {
296
176
  const campaignPath = campaignDir(runsDir, campaignId);
297
177
  const campaign = readCampaign(campaignPath);
298
178
  // The manifest is a queue of what is declared to run next, and the
@@ -10,6 +10,7 @@ import { compactCost, compactTokens, finite, truncateChars } from "../util.mjs";
10
10
  import { listNodeSnapshots, nodeSnapshotPath } from "../run/node-store.mjs";
11
11
  import { readHeartbeat } from "../engine/supervise.mjs";
12
12
  import { SETTLED, SUCCESS } from "../engine/prompts.mjs";
13
+ import { packetRepetitionByNode, packetRepetitionNote } from "./packet-repetition.mjs";
13
14
 
14
15
  /** Advisory ceiling for status.json (TECH-SPEC lean, rule 5); never enforced destructively. */
15
16
  const STATUS_JSON_MAX_BYTES = 200 * 1024;
@@ -396,17 +397,16 @@ export function renderReport(runDir) {
396
397
  for (const [index, node] of nodes.entries()) {
397
398
  const usage = node.usage ?? { inputTokens: null, outputTokens: null, cacheReadInputTokens: null };
398
399
  for (const key of /** @type {("inputTokens"|"outputTokens"|"cacheReadInputTokens")[]} */ (Object.keys(totals).filter((key) => key !== "costUsd"))) totals[key] = (totals[key] ?? 0) + (usage[key] ?? 0);
399
- const cost = costs[index];
400
400
  const runtime = workerRuntimeLabel(node) ?? "-";
401
401
  const planNode = contract.nodes.find((candidate) => candidate.id === node.id);
402
402
  const note = scopeFindingsNote(node.scopeFindings)
403
403
  ? nodeNote(node)
404
404
  : `phase ${planNode?.phase ?? "-"} · ${continuationMode(node)} · ${nodeNote(node)}`;
405
- lines.push(row([MARK[node.status] ?? "[?]", node.id, node.status, node.attempt ?? 0, node.revisions ?? 0, runtime, compactTokens(usage.inputTokens), compactTokens(usage.outputTokens), compactTokens(usage.cacheReadInputTokens), formatCost(cost), note]));
405
+ lines.push(row([MARK[node.status] ?? "[?]", node.id, node.status, node.attempt ?? 0, node.revisions ?? 0, runtime, compactTokens(usage.inputTokens), compactTokens(usage.outputTokens), compactTokens(usage.cacheReadInputTokens), formatCost(costs[index]), note]));
406
406
  }
407
407
  totals.costUsd = aggregateCost.costUsd;
408
408
  const roles = roleUsage(nodes);
409
- lines.push("```", "", `totals · in ${compactTokens(totals.inputTokens)} · out ${compactTokens(totals.outputTokens)} · cache ${compactTokens(totals.cacheReadInputTokens)} · worker ${formatRole(roles.worker)} · judge ${formatRole(roles.judge)} · cost ${formatCost(aggregateCost)}`);
409
+ lines.push("```", "", `totals · in ${compactTokens(totals.inputTokens)} · out ${compactTokens(totals.outputTokens)} · cache ${compactTokens(totals.cacheReadInputTokens)} · worker ${formatRole(roles.worker)} · judge ${formatRole(roles.judge)} · cost ${formatCost(aggregateCost)}${packetRepetitionNote(packetRepetitionByNode(runDir, nodes))}`);
410
410
  return `${lines.join("\n")}\n`;
411
411
  }
412
412
 
package/src/seat/tmux.mjs CHANGED
@@ -8,6 +8,7 @@
8
8
  * command working on a host without tmux, where only reattaching is lost.
9
9
  */
10
10
  import { execFileSync } from "node:child_process";
11
+ import { NOTIFY_SESSION_ENV } from "../notify/session.mjs";
11
12
  import { errorCode, exitStatus } from "../util.mjs";
12
13
 
13
14
  /** The single seat session every campaign window lives in. */
@@ -16,6 +17,14 @@ export const SEAT_SESSION = "faberun-seat";
16
17
  /** Window option recording which harness launched the window. */
17
18
  const HARNESS_OPTION = "@faberun-harness";
18
19
 
20
+ /**
21
+ * The one variable a seat window sets for the harness it launches: every
22
+ * controller started from that harness inherits it and wakes the session on
23
+ * a terminal node (`notify/session.mjs`). The seat is the opt-in, so a test
24
+ * suite or a plain shell never wakes anything by accident.
25
+ */
26
+ const SESSION_WAKE_ENV = `${NOTIFY_SESSION_ENV}=auto`;
27
+
19
28
  /** `list-windows` format: name, index, harness option, pane command, tab-separated. */
20
29
  const WINDOW_FORMAT = "#{window_name}\t#{window_index}\t#{@faberun-harness}\t#{pane_current_command}";
21
30
 
@@ -95,8 +104,8 @@ export function createSeatWindow(options) {
95
104
  }
96
105
  const command = shellCommand(options.argv);
97
106
  const args = existing.exists
98
- ? ["new-window", "-t", options.session, "-n", options.window, "-c", options.cwd, command]
99
- : ["new-session", "-d", "-s", options.session, "-n", options.window, "-c", options.cwd, command];
107
+ ? ["new-window", "-t", options.session, "-n", options.window, "-c", options.cwd, "-e", SESSION_WAKE_ENV, command]
108
+ : ["new-session", "-d", "-s", options.session, "-n", options.window, "-c", options.cwd, "-e", SESSION_WAKE_ENV, command];
100
109
  const result = runTmux(args);
101
110
  if (!result.ok) {
102
111
  return { available: result.available, ok: false, created: false, session: options.session, window: options.window, command, reason: result.reason, stderr: result.stderr };
@@ -123,7 +132,7 @@ function shellCommand(argv) {
123
132
  */
124
133
  export function respawnSeatWindow(options) {
125
134
  const command = shellCommand(options.argv);
126
- const result = runTmux(["respawn-window", "-k", "-t", `${options.session}:${options.window}`, "-c", options.cwd, command]);
135
+ const result = runTmux(["respawn-window", "-k", "-t", `${options.session}:${options.window}`, "-c", options.cwd, "-e", SESSION_WAKE_ENV, command]);
127
136
  if (!result.ok) {
128
137
  return { available: result.available, ok: false, respawned: false, session: options.session, window: options.window, command, reason: result.reason, stderr: result.stderr };
129
138
  }