tmux-ide 2.9.0-beta.19 → 2.9.0-beta.21

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 (90) hide show
  1. package/bin/cli.js +1979 -783
  2. package/package.json +4 -2
  3. package/packages/contracts/src/__tests__/daemon-wire.test.ts +32 -0
  4. package/packages/contracts/src/daemon-events.ts +12 -12
  5. package/packages/contracts/src/daemon-wire.ts +30 -0
  6. package/packages/daemon/dist/command-center/agent-status-watch.js +9 -2
  7. package/packages/daemon/dist/command-center/diagnostics.js +65 -0
  8. package/packages/daemon/dist/command-center/log-stream.js +9 -0
  9. package/packages/daemon/dist/command-center/resources/fleet-preview-route.js +22 -6
  10. package/packages/daemon/dist/command-center/server.js +7 -0
  11. package/packages/daemon/dist/doctor.js +62 -0
  12. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +400 -0
  13. package/packages/daemon/dist/lib/app-config.js +4 -2
  14. package/packages/daemon/dist/lib/canonical-daemon.js +1 -0
  15. package/packages/daemon/dist/lib/daemon-embed.js +24 -0
  16. package/packages/daemon/dist/lib/daemon-provenance.js +575 -0
  17. package/packages/daemon/dist/lib/headless-daemon.js +1 -0
  18. package/packages/daemon/dist/lib/log-sanitize.js +199 -0
  19. package/packages/daemon/dist/lib/log.js +123 -13
  20. package/packages/daemon/dist/lib/soak-diagnostics.js +124 -0
  21. package/packages/daemon/dist/lib/soak-verdict.js +472 -0
  22. package/packages/daemon/dist/lib/terminal-host-color.js +33 -0
  23. package/packages/daemon/dist/lib/tmux-external-interaction-observer.js +187 -39
  24. package/packages/daemon/dist/lib/tmux-interaction-retention.js +21 -0
  25. package/packages/daemon/dist/lib/workspace-promotion.js +55 -37
  26. package/packages/daemon/dist/terminal/mirror/session-channel.js +2 -1
  27. package/packages/daemon/dist/tui/mirror/automatic-contrast.js +161 -0
  28. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +9 -3
  29. package/packages/daemon/dist/tui/mirror/pane-surface.jsx +7 -5
  30. package/packages/daemon/dist/tui/mirror/resize-transaction.js +48 -20
  31. package/packages/daemon/dist/tui/mirror/runtime/application-appearance-owner.js +40 -6
  32. package/packages/daemon/dist/tui/mirror/runtime/application-fleet-preview.js +4 -1
  33. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +63 -42
  34. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +138 -67
  35. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-palette-owner.js +44 -28
  36. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +49 -11
  37. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +140 -2
  38. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +3 -1
  39. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +2 -1
  40. package/packages/daemon/dist/tui/mirror/theme.js +4 -31
  41. package/packages/daemon/dist/tui/mirror/workspace/terminal-pane-header.jsx +15 -8
  42. package/packages/daemon/dist/tui/team/wait-receipts.js +112 -33
  43. package/packages/daemon/src/command-center/agent-status-watch.ts +8 -2
  44. package/packages/daemon/src/command-center/diagnostics.ts +75 -0
  45. package/packages/daemon/src/command-center/log-stream.ts +8 -0
  46. package/packages/daemon/src/command-center/resources/fleet-preview-route.ts +27 -5
  47. package/packages/daemon/src/command-center/server.ts +8 -0
  48. package/packages/daemon/src/doctor.ts +70 -0
  49. package/packages/daemon/src/lib/app-config.ts +11 -3
  50. package/packages/daemon/src/lib/canonical-daemon.ts +1 -0
  51. package/packages/daemon/src/lib/daemon-embed.ts +30 -0
  52. package/packages/daemon/src/lib/daemon-provenance.ts +769 -0
  53. package/packages/daemon/src/lib/fleet-preview-model.ts +1 -0
  54. package/packages/daemon/src/lib/headless-daemon.ts +1 -0
  55. package/packages/daemon/src/lib/log-sanitize.ts +248 -0
  56. package/packages/daemon/src/lib/log.ts +165 -14
  57. package/packages/daemon/src/lib/soak-diagnostics.ts +183 -0
  58. package/packages/daemon/src/lib/soak-verdict.ts +785 -0
  59. package/packages/daemon/src/lib/terminal-host-color.ts +43 -0
  60. package/packages/daemon/src/lib/tmux-external-interaction-observer.ts +233 -37
  61. package/packages/daemon/src/lib/tmux-interaction-retention.ts +24 -0
  62. package/packages/daemon/src/lib/workspace-promotion.ts +74 -45
  63. package/packages/daemon/src/terminal/mirror/session-channel.ts +2 -1
  64. package/packages/daemon/src/tui/mirror/automatic-contrast.ts +180 -0
  65. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +26 -5
  66. package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -8
  67. package/packages/daemon/src/tui/mirror/resize-transaction.ts +46 -22
  68. package/packages/daemon/src/tui/mirror/runtime/application-appearance-owner.ts +45 -6
  69. package/packages/daemon/src/tui/mirror/runtime/application-fleet-preview.ts +4 -0
  70. package/packages/daemon/src/tui/mirror/runtime/application-fleet-switcher.tsx +2 -1
  71. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +99 -75
  72. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +13 -3
  73. package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +125 -0
  74. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +2 -1
  75. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +199 -157
  76. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +2 -0
  77. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +148 -69
  78. package/packages/daemon/src/tui/mirror/runtime/application-terminal-palette-owner.ts +55 -28
  79. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +59 -17
  80. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +151 -3
  81. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +3 -1
  82. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +2 -1
  83. package/packages/daemon/src/tui/mirror/theme.ts +4 -39
  84. package/packages/daemon/src/tui/mirror/workspace/terminal-pane-header.tsx +21 -8
  85. package/packages/daemon/src/tui/team/wait-receipts.ts +115 -40
  86. package/packages/daemon-client/src/terminal-fast-lane.test.ts +18 -0
  87. package/packages/daemon-client/src/terminal-fast-lane.ts +7 -2
  88. package/packages/tmux-bridge/src/index.ts +2 -0
  89. package/packages/tmux-bridge/src/runner.test.ts +41 -0
  90. package/packages/tmux-bridge/src/runner.ts +41 -2
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Private fleet fixture for the installed-artifact recovery and authority
3
+ * live tests. One fixture owns one private tmux server, one private state
4
+ * home and every CLI child it spawns, and retires all of them on `cleanup`.
5
+ *
6
+ * Socket unification: the CLI's plain `tmux` paths (adopt, restore, polling
7
+ * wait) resolve `$TMUX_TMPDIR/tmux-<uid>/default`, while the daemon pins its
8
+ * server by `TMUX_IDE_TMUX_SOCKET_PATH` and strips `TMUX_TMPDIR` from its
9
+ * pinned client. Pointing the pinned path at exactly the resolved default
10
+ * path makes both paths reach the same private server, which the fixture
11
+ * proves with `assertUnifiedSocket` before any scenario runs.
12
+ *
13
+ * CLI under test: `TMUX_IDE_QUALIFY_CLI` (absolute path to an installed
14
+ * `tmux-ide` executable or its `cli.js`) qualifies a packed installation;
15
+ * otherwise the fixture bundles `bin/cli.ts` from this checkout.
16
+ */
17
+ import { build } from "esbuild";
18
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
19
+ import { randomUUID } from "node:crypto";
20
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
21
+ import { userInfo } from "node:os";
22
+ import { dirname, join } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import WsWebSocket from "ws";
25
+ export const repoRoot = fileURLToPath(new URL("../../../../../", import.meta.url));
26
+ export const tmuxAvailable = spawnSync("tmux", ["-V"], { stdio: "ignore" }).status === 0;
27
+ function evidencePath() {
28
+ const dir = process.env.TMUX_IDE_STAGE6_EVIDENCE_DIR;
29
+ if (!dir)
30
+ return null;
31
+ mkdirSync(dir, { recursive: true });
32
+ return join(dir, "evidence.jsonl");
33
+ }
34
+ async function resolveCli(root) {
35
+ const configured = process.env.TMUX_IDE_QUALIFY_CLI;
36
+ if (configured) {
37
+ const path = realpathSync(configured);
38
+ return { path, label: `installed:${path}` };
39
+ }
40
+ const bundle = join(root, "cli.mjs");
41
+ await build({
42
+ entryPoints: [join(repoRoot, "bin/cli.ts")],
43
+ outfile: bundle,
44
+ bundle: true,
45
+ platform: "node",
46
+ format: "esm",
47
+ target: "node20",
48
+ logLevel: "silent",
49
+ plugins: [
50
+ {
51
+ name: "external-dependencies",
52
+ setup(builder) {
53
+ builder.onResolve({ filter: /.*/ }, (args) => {
54
+ if (args.kind === "entry-point" ||
55
+ args.path.startsWith(".") ||
56
+ args.path.startsWith("/") ||
57
+ args.path.startsWith("@tmux-ide/") ||
58
+ args.path === "@xterm/addon-unicode11")
59
+ return;
60
+ return { external: true };
61
+ });
62
+ },
63
+ },
64
+ ],
65
+ });
66
+ return { path: bundle, label: "checkout-bundle" };
67
+ }
68
+ export async function createPrivateFleet(prefix) {
69
+ // A short root keeps the unix socket path well under the platform limit.
70
+ const root = mkdtempSync(`/tmp/ti-${prefix}-`);
71
+ const stateDir = join(root, "state");
72
+ const home = join(root, "home");
73
+ const tmuxTmpDir = join(root, "tmux");
74
+ const socketDir = join(tmuxTmpDir, `tmux-${userInfo().uid}`);
75
+ const socketPath = join(socketDir, "default");
76
+ const binDir = join(root, "bin");
77
+ for (const dir of [stateDir, home, tmuxTmpDir, socketDir, binDir])
78
+ mkdirSync(dir, { mode: 0o700 });
79
+ chmodSync(tmuxTmpDir, 0o700);
80
+ chmodSync(socketDir, 0o700);
81
+ if (!process.env.TMUX_IDE_QUALIFY_CLI) {
82
+ // The checkout bundle resolves its externals from this repo's node_modules.
83
+ execFileSync("ln", ["-s", join(repoRoot, "node_modules"), join(root, "node_modules")]);
84
+ }
85
+ const cliResolved = await resolveCli(root);
86
+ const tmuxPath = realpathSync(execFileSync("/bin/sh", ["-c", "command -v tmux"], { encoding: "utf8" }).trim());
87
+ // The chrome updater session runs `exec tmux-ide chrome-updater` from the
88
+ // server's PATH; this shim routes it to the CLI under test.
89
+ writeFileSync(join(binDir, "tmux-ide"), `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(cliResolved.path)} "$@"\n`, { mode: 0o755 });
90
+ const env = {
91
+ ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("TMUX_IDE_") &&
92
+ key !== "TMUX" &&
93
+ key !== "TMUX_PANE" &&
94
+ key !== "TMUX_TMPDIR")),
95
+ PATH: `${binDir}:${dirname(process.execPath)}:${process.env.PATH ?? ""}`,
96
+ HOME: home,
97
+ TMUX: "",
98
+ TMUX_TMPDIR: tmuxTmpDir,
99
+ TMUX_IDE_HOME: stateDir,
100
+ TMUX_IDE_DAEMON_INFO_DIR: stateDir,
101
+ TMUX_IDE_REGISTRY_DIR: stateDir,
102
+ TMUX_IDE_SETTINGS_DIR: stateDir,
103
+ TMUX_IDE_CONFIG: join(stateDir, "config.json"),
104
+ TMUX_IDE_TMUX_BIN: tmuxPath,
105
+ TMUX_IDE_TMUX_SOCKET_PATH: socketPath,
106
+ NO_COLOR: "1",
107
+ };
108
+ const infoPath = join(stateDir, "daemon.json");
109
+ const children = new Set();
110
+ const exits = new Map();
111
+ const sockets = new Set();
112
+ const tmux = (...args) => execFileSync(tmuxPath, ["-S", socketPath, ...args], {
113
+ env,
114
+ encoding: "utf8",
115
+ stdio: ["ignore", "pipe", "pipe"],
116
+ }).trimEnd();
117
+ const tmuxStatus = (...args) => spawnSync(tmuxPath, ["-S", socketPath, ...args], { env, stdio: "ignore" }).status ?? -1;
118
+ const cli = (args) => {
119
+ const child = spawn(process.execPath, [cliResolved.path, ...args], {
120
+ cwd: root,
121
+ env,
122
+ stdio: ["ignore", "pipe", "pipe"],
123
+ });
124
+ children.add(child);
125
+ let stdout = "";
126
+ let stderr = "";
127
+ child.stdout.on("data", (chunk) => {
128
+ stdout = (stdout + chunk).slice(-64000);
129
+ });
130
+ child.stderr.on("data", (chunk) => {
131
+ stderr = (stderr + chunk).slice(-64000);
132
+ });
133
+ exits.set(child, new Promise((resolve, reject) => {
134
+ child.once("error", reject);
135
+ child.once("close", (code, signal) => resolve({ code, signal, stdout, stderr }));
136
+ }));
137
+ return child;
138
+ };
139
+ const exit = (child) => exits.get(child);
140
+ const bounded = async (promise, label, ms = 20000) => {
141
+ let timer;
142
+ try {
143
+ return await Promise.race([
144
+ promise,
145
+ new Promise((_resolve, reject) => {
146
+ timer = setTimeout(() => reject(new Error(`${label} exceeded ${ms}ms`)), ms);
147
+ }),
148
+ ]);
149
+ }
150
+ finally {
151
+ clearTimeout(timer);
152
+ }
153
+ };
154
+ const until = async (read, label, ms = 15000) => {
155
+ const deadline = Date.now() + ms;
156
+ while (Date.now() < deadline) {
157
+ const value = read();
158
+ if (value !== null)
159
+ return value;
160
+ await new Promise((resolve) => setTimeout(resolve, 25));
161
+ }
162
+ throw new Error(`Timed out after ${ms}ms: ${label}`);
163
+ };
164
+ const info = () => {
165
+ try {
166
+ return JSON.parse(readFileSync(infoPath, "utf8"));
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ };
172
+ const startDaemon = async () => {
173
+ const child = cli(["--headless", "--json"]);
174
+ const published = await Promise.race([
175
+ until(() => {
176
+ const value = info();
177
+ return value?.pid === child.pid ? value : null;
178
+ }, "daemon record publication"),
179
+ exit(child).then((result) => {
180
+ throw new Error(`Daemon exited before publication: ${result.stdout} ${result.stderr}`);
181
+ }),
182
+ ]);
183
+ // The record publishes before the HTTP accept loop is guaranteed up.
184
+ await until(() => {
185
+ const probe = spawnSync("curl", [
186
+ "-s",
187
+ "-o",
188
+ "/dev/null",
189
+ "-w",
190
+ "%{http_code}",
191
+ `http://127.0.0.1:${published.port}/health`,
192
+ ], { encoding: "utf8" });
193
+ return probe.stdout === "200" ? true : null;
194
+ }, "daemon health");
195
+ return { child, info: published };
196
+ };
197
+ const headers = (info) => info.authToken ? { authorization: `Bearer ${info.authToken}` } : {};
198
+ const fetchJson = async (info, path, init = {}) => {
199
+ const response = await fetch(`http://127.0.0.1:${info.port}${path}`, {
200
+ ...init,
201
+ headers: { ...headers(info), "content-type": "application/json", ...(init.headers ?? {}) },
202
+ signal: AbortSignal.timeout(5000),
203
+ });
204
+ return response.json();
205
+ };
206
+ const eventsClient = (info) => {
207
+ const socket = new WsWebSocket(`ws://127.0.0.1:${info.port}/ws/events`, {
208
+ headers: headers(info),
209
+ });
210
+ sockets.add(socket);
211
+ const frames = [];
212
+ let resolveReady;
213
+ let rejectReady;
214
+ const ready = new Promise((resolve, reject) => {
215
+ resolveReady = resolve;
216
+ rejectReady = reject;
217
+ });
218
+ ready.catch(() => undefined);
219
+ const closed = new Promise((resolve) => {
220
+ socket.once("close", (code) => {
221
+ sockets.delete(socket);
222
+ rejectReady(new Error(`events socket closed (${code}) before ready`));
223
+ resolve(code);
224
+ });
225
+ });
226
+ socket.on("error", (error) => rejectReady(error));
227
+ socket.on("message", (data) => {
228
+ let frame;
229
+ try {
230
+ frame = JSON.parse(String(data));
231
+ }
232
+ catch {
233
+ return;
234
+ }
235
+ frames.push(frame);
236
+ if (frame.type === "hello") {
237
+ socket.send(JSON.stringify({
238
+ type: "subscribe",
239
+ sessions: [],
240
+ legacyEvents: true,
241
+ interests: [{ resource: "fleet-catalog", workspaceName: null }],
242
+ interestRevision: 1,
243
+ }));
244
+ }
245
+ else if (frame.type === "resource.interests-ack") {
246
+ resolveReady();
247
+ }
248
+ });
249
+ return {
250
+ frames,
251
+ ready,
252
+ closed,
253
+ close: () => {
254
+ try {
255
+ socket.close();
256
+ }
257
+ catch {
258
+ // already gone
259
+ }
260
+ },
261
+ };
262
+ };
263
+ const logBackfill = async (info) => {
264
+ const controller = new AbortController();
265
+ const timer = setTimeout(() => controller.abort(), 4000);
266
+ const entries = [];
267
+ try {
268
+ const response = await fetch(`http://127.0.0.1:${info.port}/api/logs/daemon`, {
269
+ headers: headers(info),
270
+ signal: controller.signal,
271
+ });
272
+ const reader = response.body.getReader();
273
+ let buffer = "";
274
+ let sawBookmark = false;
275
+ while (!sawBookmark) {
276
+ const { value, done } = await reader.read();
277
+ if (done)
278
+ break;
279
+ buffer += new TextDecoder().decode(value);
280
+ let index = buffer.indexOf("\n\n");
281
+ while (index !== -1) {
282
+ const block = buffer.slice(0, index);
283
+ buffer = buffer.slice(index + 2);
284
+ const event = /^event: (.*)$/mu.exec(block)?.[1];
285
+ const data = block
286
+ .split("\n")
287
+ .filter((line) => line.startsWith("data:"))
288
+ .map((line) => line.slice(5).trimStart())
289
+ .join("\n");
290
+ if (event === "bookmark")
291
+ sawBookmark = true;
292
+ else if (data) {
293
+ try {
294
+ const parsed = JSON.parse(data);
295
+ if (Array.isArray(parsed))
296
+ entries.push(...parsed);
297
+ else
298
+ entries.push(parsed);
299
+ }
300
+ catch {
301
+ entries.push({ raw: data, event });
302
+ }
303
+ }
304
+ index = buffer.indexOf("\n\n");
305
+ }
306
+ }
307
+ controller.abort();
308
+ }
309
+ catch {
310
+ // aborted after the bookmark or on the deadline; return what arrived
311
+ }
312
+ finally {
313
+ clearTimeout(timer);
314
+ }
315
+ return entries;
316
+ };
317
+ const stampAgent = (paneId, state) => {
318
+ tmux("set-option", "-p", "-t", paneId, "@agent_state", `${state}:${Math.floor(Date.now() / 1000)}`);
319
+ };
320
+ const evidenceFile = evidencePath();
321
+ const evidence = (record) => {
322
+ if (!evidenceFile)
323
+ return;
324
+ appendFileSync(evidenceFile, JSON.stringify({ at: new Date().toISOString(), cli: cliResolved.label, ...record }) + "\n");
325
+ };
326
+ const cleanup = async () => {
327
+ for (const socket of sockets) {
328
+ try {
329
+ socket.terminate();
330
+ }
331
+ catch {
332
+ // already closed
333
+ }
334
+ }
335
+ const current = info();
336
+ if (current) {
337
+ await fetch(`http://127.0.0.1:${current.port}/api/v2/action/daemon.shutdown`, {
338
+ method: "POST",
339
+ headers: { ...headers(current), "content-type": "application/json" },
340
+ body: JSON.stringify({
341
+ reason: "live fixture cleanup",
342
+ expectedInstanceId: current.instanceId,
343
+ }),
344
+ signal: AbortSignal.timeout(5000),
345
+ }).catch(() => undefined);
346
+ }
347
+ for (const child of children)
348
+ if (child.exitCode === null && child.signalCode === null)
349
+ child.kill("SIGTERM");
350
+ await Promise.all([...children].map(async (child) => {
351
+ try {
352
+ await bounded(exit(child), "fixture child exit", 10000);
353
+ }
354
+ catch {
355
+ child.kill("SIGKILL");
356
+ await exit(child).catch(() => undefined);
357
+ }
358
+ }));
359
+ spawnSync(tmuxPath, ["-S", socketPath, "kill-server"], { env, stdio: "ignore" });
360
+ rmSync(root, { recursive: true, force: true });
361
+ if (existsSync(root))
362
+ throw new Error(`fixture root survived cleanup: ${root}`);
363
+ };
364
+ return {
365
+ root,
366
+ stateDir,
367
+ socketPath,
368
+ cliPath: cliResolved.path,
369
+ cliLabel: cliResolved.label,
370
+ env,
371
+ tmux,
372
+ tmuxStatus,
373
+ cli,
374
+ exit,
375
+ bounded,
376
+ until,
377
+ info,
378
+ startDaemon,
379
+ eventsClient,
380
+ fetchJson,
381
+ logBackfill,
382
+ stampAgent,
383
+ evidence,
384
+ cleanup,
385
+ };
386
+ }
387
+ /**
388
+ * Prove the plain-`tmux` CLI path and the daemon's pinned socket path name
389
+ * the same server. Requires a running server on the fixture socket.
390
+ */
391
+ export function assertUnifiedSocket(fleet) {
392
+ const plain = execFileSync("tmux", ["display-message", "-p", "#{socket_path}"], {
393
+ env: fleet.env,
394
+ encoding: "utf8",
395
+ }).trim();
396
+ const pinned = fleet.tmux("display-message", "-p", "#{socket_path}");
397
+ if (plain !== fleet.socketPath || pinned !== fleet.socketPath)
398
+ throw new Error(`socket split: plain=${plain} pinned=${pinned} expected=${fleet.socketPath}`);
399
+ }
400
+ export const uniqueName = (prefix) => `${prefix}-${randomUUID().slice(0, 8)}`;
@@ -39,7 +39,8 @@ export const DEFAULT_APP_CONFIG = {
39
39
  panels: { explorer: "M-e", changes: "M-g", config: "M-," },
40
40
  },
41
41
  theme: {
42
- mode: "dark",
42
+ automaticContrast: true,
43
+ mode: "system",
43
44
  accent: "colour75",
44
45
  muted: "colour240",
45
46
  fg: "colour250",
@@ -160,7 +161,8 @@ export function parseAppConfig(input) {
160
161
  },
161
162
  },
162
163
  theme: {
163
- mode: pickChoice(theme.mode, ["dark", "light", "system"], D.theme.mode),
164
+ automaticContrast: pickBool(theme.automaticContrast, D.theme.automaticContrast),
165
+ mode: pickChoice(theme.mode, ["dark", "light", "system"], findVisualThemePreset(theme.preset)?.appearance ?? D.theme.mode),
164
166
  ...(findVisualThemePreset(theme.preset) ? { preset: String(theme.preset) } : {}),
165
167
  accent: pickString(theme.accent, D.theme.accent),
166
168
  muted: pickString(theme.muted, D.theme.muted),
@@ -434,6 +434,7 @@ export function writeCanonicalDaemonInfo(info, claim) {
434
434
  ...(info.environmentId !== undefined ? { environmentId: info.environmentId } : {}),
435
435
  bindHostname: info.bindHostname,
436
436
  authToken: info.authToken,
437
+ ...(info.provenance !== undefined ? { provenance: info.provenance } : {}),
437
438
  };
438
439
  writeFileSync(tmpPath, JSON.stringify(persisted, null, 2) + "\n", {
439
440
  encoding: "utf-8",
@@ -56,6 +56,8 @@ import { createSessionRuntimeMultiplexerBackend } from "../terminal/session-runt
56
56
  import { PaneSourceCredentialAuthority, reconcilePaneSourceCredentialsAtStartup, } from "./pane-source-credentials.js";
57
57
  import { setActivationBackend } from "./active-projects.js";
58
58
  import { readOrMintEnvironmentId } from "./environment-identity.js";
59
+ import { captureDaemonProvenance } from "./daemon-provenance.js";
60
+ import { registerLogSecret, setLogIdentity } from "./log.js";
59
61
  import { canonicalDaemonUrl, clearCanonicalDaemonInfoIfOwned, clearCanonicalDaemonInfoIfUnchanged, inspectCanonicalDaemonInfo, isCanonicalDaemonAlive, isCanonicalDaemonRecordOwnerProvenDead, probeCanonicalDaemonHealth, probeCanonicalDaemonIdentity, releaseCanonicalDaemonClaim, tryAcquireCanonicalDaemonClaim, writeCanonicalDaemonInfo, matchesCanonicalDaemonPredecessor, } from "./canonical-daemon.js";
60
62
  const requireFromHere = createRequire(import.meta.url);
61
63
  const DEFAULT_HOSTNAME = "127.0.0.1";
@@ -797,6 +799,9 @@ async function startEmbeddedDaemonGeneration(opts) {
797
799
  internalReadOwnerToken: localBypassToken,
798
800
  registry: workspaceRegistry,
799
801
  tmuxAuthority,
802
+ // A gap cannot reconstruct historical interactions. Refresh inventory
803
+ // facts while authored operations retain their observation deadlines.
804
+ onGap: () => terminalInventoryRuntime?.invalidate(),
800
805
  onObserved: ({ workspaceName, semanticPaneId, operationKind, operationId }) => {
801
806
  if (operationKind !== "workspace.pane.read")
802
807
  terminalInventoryRuntime?.invalidate();
@@ -1117,6 +1122,24 @@ async function startEmbeddedDaemonGeneration(opts) {
1117
1122
  console.error("[daemon] Direct terminal startup rollback reported cleanup failures.");
1118
1123
  }
1119
1124
  };
1125
+ // Provenance (launcher, supervisor, actual stdout/stderr destination) is
1126
+ // derived from this process, never from file-name conventions, so a later
1127
+ // `daemon info` reads the truth from the record. Capture never throws;
1128
+ // whatever it could not determine is reported as a warning.
1129
+ const provenance = captureDaemonProvenance({
1130
+ launcher: opts.launcher ?? "embedded",
1131
+ ...(opts.supervisionId ? { supervisionId: opts.supervisionId } : {}),
1132
+ });
1133
+ if (provenance.warnings?.length && !opts.silent) {
1134
+ for (const warning of provenance.warnings) {
1135
+ console.warn(`[daemon] log provenance degraded: ${warning}`);
1136
+ }
1137
+ }
1138
+ // Every structured record now carries this generation's identity, and the
1139
+ // local bypass / shared tokens are redacted wherever they might appear.
1140
+ setLogIdentity({ instanceId, version: productVersion });
1141
+ registerLogSecret(localBypassToken);
1142
+ registerLogSecret(opts.authToken);
1120
1143
  try {
1121
1144
  writeCanonicalDaemonInfo({
1122
1145
  ...(opts.supervisionId ? { supervisionId: opts.supervisionId } : {}),
@@ -1129,6 +1152,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1129
1152
  environmentId,
1130
1153
  bindHostname,
1131
1154
  authToken: localBypassToken,
1155
+ provenance,
1132
1156
  }, claim);
1133
1157
  const published = inspectCanonicalDaemonInfo();
1134
1158
  if (published.status !== "valid" ||