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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.19",
3
+ "version": "2.9.0-beta.21",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,6 +39,7 @@
39
39
  "typecheck": "echo \"root typecheck deferred to per-package turbo run\"",
40
40
  "dev": "node bin/cli.js",
41
41
  "dev:instance": "node scripts/development-instance.mjs",
42
+ "dev:pilotty": "node scripts/pilotty-dev.mjs",
42
43
  "dev:web": "node scripts/dev-web.mjs",
43
44
  "test": "pnpm -r --workspace-concurrency=1 --filter @tmux-ide/daemon --filter @tmux-ide/contracts --filter @tmux-ide/core --filter @tmux-ide/daemon-client --filter @tmux-ide/sdk --filter @tmux-ide/desktop-renderer --filter @tmux-ide/web-workspace --filter @tmux-ide/electron-shell run test",
44
45
  "test:unit": "pnpm -r --workspace-concurrency=1 --filter @tmux-ide/daemon --filter @tmux-ide/contracts --filter @tmux-ide/core --filter @tmux-ide/daemon-client --filter @tmux-ide/sdk --filter @tmux-ide/desktop-renderer --filter @tmux-ide/web-workspace --filter @tmux-ide/electron-shell run test",
@@ -63,11 +64,12 @@
63
64
  "postinstall": "node scripts/postinstall.js",
64
65
  "docs": "turbo run dev --filter=@tmux-ide/docs",
65
66
  "demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
66
- "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
67
+ "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
67
68
  "test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
68
69
  "test:tui-live": "node scripts/tui-testdrive.mjs smoke",
69
70
  "test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
70
71
  "test:performance-qualification": "node scripts/performance-qualification.mjs",
72
+ "qualify:installed-latency": "node packages/daemon/scripts/qualify-installed-latency.mjs",
71
73
  "benchmark:comparative": "node scripts/comparative-terminal.mjs",
72
74
  "test:contracts-initializer-purity": "node --test scripts/lib/contracts-initializer-purity.test.mjs",
73
75
  "test:benchmark-comparative": "node --test scripts/comparative-terminal-support.test.mjs scripts/lib/terminal-scroll-observations.test.mjs scripts/lib/comparative-terminal-resources.test.mjs scripts/lib/comparative-terminal-report.test.mjs",
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
4
4
  import {
5
5
  CanonicalDaemonInfoSchema,
6
6
  DAEMON_WIRE_PROTOCOL_VERSION,
7
+ DaemonProvenanceSchema,
7
8
  DaemonHealthSchema,
8
9
  DaemonHealthzSchema,
9
10
  DaemonIdentitySchema,
@@ -115,3 +116,34 @@ describe("environment identity (additive, both directions)", () => {
115
116
  expect(withId.daemon.startedAt).toBe(identityFields.startedAt);
116
117
  });
117
118
  });
119
+
120
+ describe("daemon provenance (additive)", () => {
121
+ const fixture = JSON.parse(readFileSync(fixturePath, "utf8")) as {
122
+ canonical: Record<string, unknown>;
123
+ };
124
+ const provenance = {
125
+ launcher: "headless",
126
+ supervisor: "systemd",
127
+ parentPid: 1,
128
+ stdout: { kind: "file", path: "/var/log/tmux-ide.out", dev: 1, ino: 2 },
129
+ stderr: { kind: "socket" },
130
+ warnings: ["stderr: path unresolved"],
131
+ };
132
+
133
+ it("parses records with and without provenance", () => {
134
+ expect(CanonicalDaemonInfoSchema.parse(fixture.canonical).provenance).toBeUndefined();
135
+ expect(
136
+ CanonicalDaemonInfoSchema.parse({ ...fixture.canonical, provenance }).provenance,
137
+ ).toEqual(provenance);
138
+ expect(DaemonProvenanceSchema.parse(provenance)).toEqual(provenance);
139
+ });
140
+
141
+ it("rejects unknown supervisors and stream kinds", () => {
142
+ expect(DaemonProvenanceSchema.safeParse({ ...provenance, supervisor: "cron" }).success).toBe(
143
+ false,
144
+ );
145
+ expect(
146
+ DaemonProvenanceSchema.safeParse({ ...provenance, stdout: { kind: "journal" } }).success,
147
+ ).toBe(false);
148
+ });
149
+ });
@@ -268,21 +268,21 @@ export const DaemonEventFleetChangedFrameSchemaZ = z
268
268
  .strict();
269
269
 
270
270
  /**
271
- * RECEIPT — a pane's ground-truth agent authority (`@agent_state`) completed a
272
- * turn: the daemon's agent-status watcher observed `working` transition to
273
- * `done` or `idle`. Unlike the `agent-status.changed` invalidation (a re-fetch
274
- * hint), this is a typed completion event a consumer can WAIT on without
275
- * polling: the dock chip, the fleet sidebar, and `tmux-ide wait agent-status`
276
- * all want exactly "an agent finished".
271
+ * RECEIPT — a sampled pane-state transition: the daemon's agent-status watcher
272
+ * observed `@agent_state` change from `working` to `done` or `idle` while the
273
+ * runtime pane and durable pane stamp remained equal. This does not establish
274
+ * agent process continuity or success of a submitted task. Session-status
275
+ * waits use it as a hint to re-read aggregate status, since another agent in
276
+ * the session may still be working or blocked.
277
277
  *
278
- * `agentId` is the wire-safe durable agent identity — the same
278
+ * `agentId` is the wire-safe durable pane-derived agent identity — the same
279
279
  * `agent.<digest>` id the application-shell sidebar mints from the pane's
280
280
  * durable `@tmux_ide_pane_id` stamp — or `null` when the pane carries no valid
281
- * stamp (receipts still fire; correlation is best-effort). No raw tmux
282
- * runtime id or path ever crosses this frame. `at` is the daemon's
283
- * observation time (the watcher polls, so it trails the hook stamp by at most
284
- * one poll interval). One receipt per completing pane per poll tick; the poll
285
- * interval is a hard emission floor, so a flapping pane cannot storm clients.
281
+ * stamp. Anonymous receipts provide no per-agent correlation. No raw tmux
282
+ * runtime id or path ever crosses this frame. `at` is the daemon's observation
283
+ * time, not the hook or task completion time. Transitions entirely between
284
+ * successful polling samples may be missed; this is not a durable turn log.
285
+ * At most one receipt is emitted per completing pane per poll tick.
286
286
  */
287
287
  export const DaemonEventAgentTurnCompletedFrameSchemaZ = z
288
288
  .object({
@@ -67,6 +67,35 @@ export const CanonicalDaemonReservationSchema = z
67
67
  .strict();
68
68
  export type CanonicalDaemonReservation = z.infer<typeof CanonicalDaemonReservationSchema>;
69
69
 
70
+ /**
71
+ * Where a daemon's stdout/stderr actually go, captured by the running process
72
+ * at startup. `dev`/`ino` identify a regular file even when its path could not
73
+ * be resolved, so a provenance report can match on-disk log files against the
74
+ * live destination instead of guessing from file names.
75
+ */
76
+ export const DaemonLogStreamSchema = z.object({
77
+ kind: z.enum(["file", "tty", "pipe", "socket", "null", "unknown"]),
78
+ path: z.string().min(1).max(4096).optional(),
79
+ dev: z.number().int().nonnegative().optional(),
80
+ ino: z.number().int().nonnegative().optional(),
81
+ detail: z.string().max(256).optional(),
82
+ });
83
+ export type DaemonLogStream = z.infer<typeof DaemonLogStreamSchema>;
84
+
85
+ export const DaemonSupervisorKindSchema = z.enum(["manual", "launchd", "systemd", "embedded"]);
86
+ export type DaemonSupervisorKind = z.infer<typeof DaemonSupervisorKindSchema>;
87
+
88
+ /** Startup provenance stamped into the daemon record; never carries credentials. */
89
+ export const DaemonProvenanceSchema = z.object({
90
+ launcher: z.enum(["headless", "embedded"]),
91
+ supervisor: DaemonSupervisorKindSchema,
92
+ parentPid: z.number().int().nonnegative(),
93
+ stdout: DaemonLogStreamSchema,
94
+ stderr: DaemonLogStreamSchema,
95
+ warnings: z.array(z.string().max(256)).max(8).optional(),
96
+ });
97
+ export type DaemonProvenance = z.infer<typeof DaemonProvenanceSchema>;
98
+
70
99
  export const CanonicalDaemonInfoSchema = z.object({
71
100
  supervisionId: DaemonSupervisionIdSchema.optional(),
72
101
  pid: z.number().int().positive(),
@@ -78,6 +107,7 @@ export const CanonicalDaemonInfoSchema = z.object({
78
107
  environmentId: EnvironmentIdSchema.optional(),
79
108
  bindHostname: z.string().trim().min(1),
80
109
  authToken: z.string().min(1).nullable(),
110
+ provenance: DaemonProvenanceSchema.optional(),
81
111
  });
82
112
  export type CanonicalDaemonInfo = z.infer<typeof CanonicalDaemonInfoSchema>;
83
113
 
@@ -74,6 +74,10 @@ export function diffChangedSessions(previous, next) {
74
74
  * Completed turns between two readings: panes present in BOTH whose state word
75
75
  * left `working` for `done` or `idle`. A pane that vanished mid-turn completed
76
76
  * nothing; a pane that appeared already-done transitioned nothing we observed.
77
+ * The durable stamp must remain equal: gaining, losing or replacing it is not
78
+ * evidence that the replacement completed the prior turn. Two unstamped reads
79
+ * retain anonymous receipts for compatibility, without process/task correlation.
80
+ * These sampled status transitions do not prove submitted-task success.
77
81
  * Sorted (session, then pane key) for deterministic emission order.
78
82
  */
79
83
  export function diffTurnCompletions(previous, next) {
@@ -87,12 +91,15 @@ export function diffTurnCompletions(previous, next) {
87
91
  const prior = before.get(paneId);
88
92
  if (prior === undefined || agentStateWord(prior.state) !== "working")
89
93
  continue;
90
- const word = agentStateWord(panes.get(paneId).state);
94
+ const current = panes.get(paneId);
95
+ if (prior.paneStamp !== current.paneStamp)
96
+ continue;
97
+ const word = agentStateWord(current.state);
91
98
  if (word !== "done" && word !== "idle")
92
99
  continue;
93
100
  completions.push({
94
101
  sessionName,
95
- paneStamp: panes.get(paneId).paneStamp,
102
+ paneStamp: current.paneStamp,
96
103
  fromStatus: "working",
97
104
  toStatus: word,
98
105
  });
@@ -0,0 +1,65 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { requireOwnerAuthority } from "./owner-authority.js";
3
+ /** One synchronous, demand-only sample. No observers, discovery, or retained baseline. */
4
+ export function sampleDaemonDiagnostics(daemon) {
5
+ const sampledAtMs = Date.now();
6
+ const memory = process.memoryUsage();
7
+ const cpu = process.cpuUsage();
8
+ const eventLoop = performance.eventLoopUtilization();
9
+ // Fixed output cardinality. Unknown runtime resource labels are never echoed.
10
+ const activeResources = {
11
+ Timeout: 0,
12
+ Immediate: 0,
13
+ TCPServerWrap: 0,
14
+ TCPSocketWrap: 0,
15
+ PipeWrap: 0,
16
+ ProcessWrap: 0,
17
+ FSEventWrap: 0,
18
+ other: 0,
19
+ };
20
+ const resourceNames = process.getActiveResourcesInfo?.();
21
+ for (const resource of resourceNames ?? []) {
22
+ if (Object.hasOwn(activeResources, resource) && resource !== "other") {
23
+ activeResources[resource]++;
24
+ }
25
+ else {
26
+ activeResources.other++;
27
+ }
28
+ }
29
+ return {
30
+ daemon: {
31
+ protocolVersion: daemon.protocolVersion,
32
+ productVersion: daemon.productVersion,
33
+ instanceId: daemon.instanceId,
34
+ startedAt: daemon.startedAt,
35
+ ...(daemon.environmentId !== undefined ? { environmentId: daemon.environmentId } : {}),
36
+ },
37
+ pid: process.pid,
38
+ uptimeMs: process.uptime() * 1000,
39
+ sampledAtMs,
40
+ memory: {
41
+ rss: memory.rss,
42
+ heapTotal: memory.heapTotal,
43
+ heapUsed: memory.heapUsed,
44
+ external: memory.external,
45
+ arrayBuffers: memory.arrayBuffers,
46
+ },
47
+ cpu: { user: cpu.user, system: cpu.system },
48
+ eventLoop: {
49
+ idle: eventLoop.idle,
50
+ active: eventLoop.active,
51
+ utilization: eventLoop.utilization,
52
+ },
53
+ activeResources: resourceNames === undefined ? null : activeResources,
54
+ };
55
+ }
56
+ export function mountDiagnosticsRoute(app, options) {
57
+ app.get("/api/diagnostics", requireOwnerAuthority(options.ownerToken, {
58
+ whenOwnerless: "unavailable",
59
+ unavailableMessage: "Diagnostics unavailable",
60
+ mismatchMessage: "Diagnostics require owner authority",
61
+ }), (c) => {
62
+ c.header("Cache-Control", "no-store");
63
+ return c.json(sampleDaemonDiagnostics(options.daemon));
64
+ });
65
+ }
@@ -8,6 +8,7 @@ export async function streamBoundedLogs(stream, options) {
8
8
  let wake = null;
9
9
  let unsubscribe = () => { };
10
10
  let timer = null;
11
+ let heartbeatTimer = null;
11
12
  let cancelWrite = null;
12
13
  const cleanup = () => {
13
14
  if (closed)
@@ -19,6 +20,9 @@ export async function streamBoundedLogs(stream, options) {
19
20
  if (timer)
20
21
  clearTimeout(timer);
21
22
  timer = null;
23
+ if (heartbeatTimer)
24
+ clearInterval(heartbeatTimer);
25
+ heartbeatTimer = null;
22
26
  wake?.();
23
27
  wake = null;
24
28
  cancelWrite?.();
@@ -51,6 +55,11 @@ export async function streamBoundedLogs(stream, options) {
51
55
  unsubscribe();
52
56
  return;
53
57
  }
58
+ heartbeatTimer = setInterval(() => {
59
+ if (!closed && queue.length === 0 && !push({ event: "heartbeat", data: "keep-alive" }))
60
+ abort();
61
+ }, options.heartbeatIntervalMs ?? 15_000);
62
+ heartbeatTimer.unref?.();
54
63
  try {
55
64
  // Keep newest bounded history, reserving room for gap/bookmark metadata.
56
65
  const retained = [];
@@ -5,7 +5,7 @@ import { ownerAuthorityGate } from "../owner-authority.js";
5
5
  import { discoverLiveSessionSummaries } from "../discovery.js";
6
6
  const cleanText = (value) => stripVTControlCharacters(value).replace(/[^\P{Cc}\n\t]/gu, "");
7
7
  export function createFleetPreviewCapture(run) {
8
- const snapshot = async (liveSessionId, signal, windowId) => {
8
+ const snapshot = async (liveSessionId, signal, windowId, paneId) => {
9
9
  const readSessions = async () => {
10
10
  const raw = await run(["list-panes", "-a", "-F", "#{pid}\t#{session_id}\t#{session_created}\t#{session_name}"], signal);
11
11
  return discoverLiveSessionSummaries(() => raw);
@@ -41,7 +41,14 @@ export function createFleetPreviewCapture(run) {
41
41
  .slice(0, 80));
42
42
  }
43
43
  const windowRows = rows.filter((row) => /^@\d+$/u.test(row[3] ?? ""));
44
- const selectedWindowId = windowId ?? windowRows.find((row) => row[1] === "1")?.[3] ?? windowRows[0]?.[3] ?? null;
44
+ const requestedPane = paneId ? windowRows.find((row) => row[0] === paneId) : undefined;
45
+ if (paneId && (!requestedPane || (windowId && requestedPane[3] !== windowId)))
46
+ return null;
47
+ const selectedWindowId = requestedPane?.[3] ??
48
+ windowId ??
49
+ windowRows.find((row) => row[1] === "1")?.[3] ??
50
+ windowRows[0]?.[3] ??
51
+ null;
45
52
  if (windowId && !windowRows.some((row) => row[3] === windowId))
46
53
  return null;
47
54
  const toWindow = (row) => ({
@@ -80,7 +87,7 @@ export function createFleetPreviewCapture(run) {
80
87
  }
81
88
  windows.sort((a, b) => a.index - b.index);
82
89
  const candidates = selectedWindowId ? rows.filter((r) => r[3] === selectedWindowId) : rows;
83
- const pane = candidates.find((r) => r[2] === "1")?.[0] ?? candidates[0]?.[0];
90
+ const pane = paneId ?? candidates.find((r) => r[2] === "1")?.[0] ?? candidates[0]?.[0];
84
91
  if (!pane || !/^%\d+$/u.test(pane))
85
92
  return null;
86
93
  // Passive capture never changes active windows, size, input or terminal ownership.
@@ -92,6 +99,7 @@ export function createFleetPreviewCapture(run) {
92
99
  return null;
93
100
  return {
94
101
  windows,
102
+ selectedPaneId: pane,
95
103
  selectedWindowId,
96
104
  text: cleanText(captured)
97
105
  .split("\n")
@@ -106,6 +114,10 @@ export function createFleetPreviewCapture(run) {
106
114
  }
107
115
  const requestSchema = z.strictObject({
108
116
  expectedInstanceId: z.uuid(),
117
+ paneId: z
118
+ .string()
119
+ .regex(/^%\d+$/u)
120
+ .optional(),
109
121
  windowId: z
110
122
  .string()
111
123
  .regex(/^@\d+$/u)
@@ -152,10 +164,10 @@ export function mountFleetPreviewRoute(app, options) {
152
164
  pending = true;
153
165
  try {
154
166
  const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]);
155
- if (input.data.windowId && !options.capture.snapshot)
167
+ if ((input.data.windowId || input.data.paneId) && !options.capture.snapshot)
156
168
  return c.json({ error: "Window preview unavailable" }, 503);
157
169
  const snapshot = options.capture.snapshot
158
- ? await options.capture.snapshot(input.data.liveSessionId, signal, input.data.windowId)
170
+ ? await options.capture.snapshot(input.data.liveSessionId, signal, input.data.windowId, input.data.paneId)
159
171
  : null;
160
172
  // Older capture providers remain valid for the original text-only contract.
161
173
  const legacy = !options.capture.snapshot
@@ -169,7 +181,11 @@ export function mountFleetPreviewRoute(app, options) {
169
181
  liveSessionId: input.data.liveSessionId,
170
182
  text,
171
183
  ...(snapshot
172
- ? { windows: snapshot.windows, selectedWindowId: snapshot.selectedWindowId }
184
+ ? {
185
+ windows: snapshot.windows,
186
+ selectedWindowId: snapshot.selectedWindowId,
187
+ selectedPaneId: snapshot.selectedPaneId,
188
+ }
173
189
  : {}),
174
190
  });
175
191
  }
@@ -63,6 +63,7 @@ import { mountWorkspaceResourceRoutes } from "./resources/workspace-resource-rou
63
63
  import { mountFleetResourceRoute } from "./resources/fleet-resource-route.js";
64
64
  import { mountWorkspaceMissionsRoute } from "./resources/workspace-missions-route.js";
65
65
  import { ownerAuthorityGate, requireOwnerAuthority } from "./owner-authority.js";
66
+ import { mountDiagnosticsRoute } from "./diagnostics.js";
66
67
  import { mountStartupReadinessRoute, } from "./resources/startup-readiness-route.js";
67
68
  import { readWidgetAsset } from "../lib/widget-asset-store.js";
68
69
  const defaultApplicationShellAppWindowBackend = {
@@ -316,6 +317,12 @@ export function createApp(options = {}) {
316
317
  const app = new Hono();
317
318
  // Allow cross-origin (Next.js dashboard, Tailscale, etc.)
318
319
  app.use("/*", cors());
320
+ // Owner-only reads use the same early routing boundary as issuance below:
321
+ // remote/project credentials neither authorize nor block the owner bearer.
322
+ mountDiagnosticsRoute(app, {
323
+ daemon: daemonInstanceIdentity,
324
+ ownerToken: options.remoteAccess?.ownerToken ?? null,
325
+ });
319
326
  // This exact route carries its own owner-only bearer and correlation gate.
320
327
  // Mount it before remote and project auth so a valid host capability is
321
328
  // neither rejected nor confused with any remotely shared user credential.
@@ -9,6 +9,7 @@ import { findCompiledTui, hasDevelopmentTuiSource, isBunAvailable } from "./tui/
9
9
  import { claudeSettingsPath } from "./tui/integrations/claude.js";
10
10
  import { readNotificationPrefs, resolveNativeMacosNotifierPath } from "./tui/chrome/notify.js";
11
11
  import { resolveConfig } from "./lib/resolved-config.js";
12
+ import { collectDaemonProvenanceReport, } from "./lib/daemon-provenance.js";
12
13
  /**
13
14
  * PURE — the "agent integrations" doctor rows, one per DISCOVERED agent (absent
14
15
  * agents produce nothing — no noise). All rows are optional (informational, they
@@ -91,6 +92,52 @@ export function notifierRow(present) {
91
92
  optional: true,
92
93
  };
93
94
  }
95
+ /**
96
+ * PURE — the "canonical daemon" row from a provenance report. Optional: a
97
+ * machine without a running daemon is healthy; the row exists so triage sees
98
+ * identity, supervisor and the real log destination without a second command.
99
+ */
100
+ export function daemonProvenanceRow(report) {
101
+ const label = "canonical daemon";
102
+ const historical = report.logFiles.filter((f) => f.status === "historical").length;
103
+ const suffix = historical
104
+ ? ` — ${historical} historical log file${historical === 1 ? "" : "s"} (see \`tmux-ide daemon info\`)`
105
+ : "";
106
+ const d = report.daemon;
107
+ switch (report.status) {
108
+ case "running":
109
+ return {
110
+ label,
111
+ pass: true,
112
+ detail: `running v${d.productVersion} pid ${d.pid} (${d.supervisor ?? "unknown supervisor"}) · ` +
113
+ `logs → ${d.logDestination}${suffix}`,
114
+ optional: true,
115
+ };
116
+ case "stale-record":
117
+ return {
118
+ label,
119
+ pass: false,
120
+ detail: `record names pid ${d.pid} (v${d.productVersion}) but it is not running${suffix}`,
121
+ optional: true,
122
+ };
123
+ case "record-invalid":
124
+ return {
125
+ label,
126
+ pass: false,
127
+ detail: `record invalid (${report.record.status === "invalid" ? report.record.reason : "unknown"})${suffix}`,
128
+ optional: true,
129
+ };
130
+ default:
131
+ return {
132
+ label,
133
+ pass: true,
134
+ detail: report.record.status === "reserved"
135
+ ? `not running (reserved for supervisor ${report.record.supervisionId})${suffix}`
136
+ : `not running${suffix}`,
137
+ optional: true,
138
+ };
139
+ }
140
+ }
94
141
  function check(label, fn, { optional = false } = {}) {
95
142
  try {
96
143
  const result = fn();
@@ -248,6 +295,21 @@ export async function doctor({ json, } = {}) {
248
295
  }
249
296
  return `in sync (v${installed})`;
250
297
  }, { optional: true }));
298
+ // Canonical daemon identity + log provenance (credential-free), so a triage
299
+ // never chases a stale, manually redirected log file.
300
+ checks.push((() => {
301
+ try {
302
+ return daemonProvenanceRow(collectDaemonProvenanceReport());
303
+ }
304
+ catch (e) {
305
+ return {
306
+ label: "canonical daemon",
307
+ pass: false,
308
+ detail: `provenance unavailable: ${e.message}`,
309
+ optional: true,
310
+ };
311
+ }
312
+ })());
251
313
  // Native branded sender: only when the macOS channel is actually on.
252
314
  if (process.platform === "darwin" && readNotificationPrefs().macos) {
253
315
  checks.push(notifierRow(resolveNativeMacosNotifierPath() !== null));