@timurproko/a1 0.2.7-dev.754 → 0.2.7-dev.772

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 (217) hide show
  1. package/README.md +30 -0
  2. package/bin/cli.js +4 -0
  3. package/bin/guardian.js +5 -0
  4. package/bin/tab-launch-context.d.ts +26 -0
  5. package/bin/tab-launch-context.js +94 -0
  6. package/bin/ui.js +24 -6
  7. package/dist/app/session-shell/helper-pool.d.ts +3 -5
  8. package/dist/app/session-shell/helper-pool.js +3 -5
  9. package/dist/app/session-shell/session-shell-root.d.ts +28 -1
  10. package/dist/app/session-shell/session-shell-root.js +4 -3
  11. package/dist/app/session-shell/session-shell.js +149 -31
  12. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -1
  13. package/dist/app/session-shell/session-viewport-controller.js +3 -3
  14. package/dist/app/session-shell/shell-host.d.ts +2 -0
  15. package/dist/app/session-shell/shell-host.js +20 -1
  16. package/dist/cli/dispatch.d.ts +11 -1
  17. package/dist/cli/dispatch.js +79 -5
  18. package/dist/cli/tabs-request.d.ts +7 -0
  19. package/dist/cli/tabs-request.js +1 -0
  20. package/dist/cli/tabs.d.ts +14 -0
  21. package/dist/cli/tabs.js +140 -0
  22. package/dist/composition/owned-ui.d.ts +26 -0
  23. package/dist/composition/owned-ui.js +176 -6
  24. package/dist/contracts/owned-ui/session-backend.d.ts +1 -0
  25. package/dist/features/launch/index.d.ts +1 -1
  26. package/dist/features/launch/intent.d.ts +3 -1
  27. package/dist/features/launch/intent.js +7 -4
  28. package/dist/features/owned-ui/settings-app.js +8 -35
  29. package/dist/features/resident-tab/auto-name.d.ts +11 -0
  30. package/dist/features/resident-tab/auto-name.js +42 -0
  31. package/dist/features/resident-tab/index.d.ts +3 -0
  32. package/dist/features/resident-tab/index.js +3 -0
  33. package/dist/features/resident-tab/shortcuts.d.ts +10 -0
  34. package/dist/features/resident-tab/shortcuts.js +35 -0
  35. package/dist/features/resident-tab/status-deriver.d.ts +9 -0
  36. package/dist/features/resident-tab/status-deriver.js +15 -0
  37. package/dist/foundation/launch-context/index.d.ts +10 -0
  38. package/dist/foundation/launch-context/index.js +25 -0
  39. package/dist/foundation/launch-guardian/main.d.ts +5 -0
  40. package/dist/foundation/launch-guardian/main.js +2 -1
  41. package/dist/foundation/release/bootstrap.d.ts +6 -0
  42. package/dist/foundation/release/bootstrap.js +51 -10
  43. package/dist/foundation/release/cohort-state.d.ts +1 -1
  44. package/dist/foundation/release/cohort-state.js +1 -1
  45. package/dist/foundation/release/dependency-certification.js +54 -19
  46. package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
  47. package/dist/foundation/release/progress-palette.generated.js +2 -2
  48. package/dist/foundation/release/release-gc.d.ts +3 -0
  49. package/dist/foundation/release/release-gc.js +44 -8
  50. package/dist/foundation/resident-tabs/bounded-host-command.d.ts +16 -0
  51. package/dist/foundation/resident-tabs/bounded-host-command.js +35 -0
  52. package/dist/foundation/resident-tabs/bridge-client.d.ts +56 -0
  53. package/dist/foundation/resident-tabs/bridge-client.js +279 -0
  54. package/dist/foundation/resident-tabs/doctor.d.ts +29 -0
  55. package/dist/foundation/resident-tabs/doctor.js +166 -0
  56. package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
  57. package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
  58. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +31 -0
  59. package/dist/foundation/resident-tabs/ensure-terminal-host.js +147 -0
  60. package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
  61. package/dist/foundation/resident-tabs/fault-injection.js +26 -0
  62. package/dist/foundation/resident-tabs/index.d.ts +12 -0
  63. package/dist/foundation/resident-tabs/index.js +6 -0
  64. package/dist/foundation/resident-tabs/maintenance.d.ts +42 -0
  65. package/dist/foundation/resident-tabs/maintenance.js +78 -0
  66. package/dist/foundation/resident-tabs/prompt-journal.d.ts +65 -0
  67. package/dist/foundation/resident-tabs/prompt-journal.js +214 -0
  68. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  69. package/dist/foundation/resident-tabs/protocol.js +195 -0
  70. package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
  71. package/dist/foundation/resident-tabs/recovery.js +205 -0
  72. package/dist/foundation/resident-tabs/release-holds.d.ts +27 -0
  73. package/dist/foundation/resident-tabs/release-holds.js +77 -0
  74. package/dist/foundation/resident-tabs/routing.d.ts +11 -0
  75. package/dist/foundation/resident-tabs/routing.js +10 -0
  76. package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
  77. package/dist/foundation/resident-tabs/session-sync.js +37 -0
  78. package/dist/foundation/resident-tabs/settings.d.ts +14 -0
  79. package/dist/foundation/resident-tabs/settings.js +66 -0
  80. package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +18 -0
  81. package/dist/foundation/resident-tabs/terminal-host-artifact.js +53 -0
  82. package/dist/foundation/resident-tabs/zip-archive.d.ts +9 -0
  83. package/dist/foundation/resident-tabs/zip-archive.js +56 -0
  84. package/dist/foundation/session-writer-lock/index.d.ts +32 -0
  85. package/dist/foundation/session-writer-lock/index.js +201 -0
  86. package/dist/foundation/startup/startup-descriptor.js +2 -2
  87. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  88. package/dist/foundation/terminal-cleanup/index.js +1 -1
  89. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +1 -0
  90. package/dist/foundation/terminal-cleanup/terminal-reset.js +1 -0
  91. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  92. package/dist/integrations/pi/components/owned-editor-ux.js +2 -2
  93. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
  94. package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
  95. package/dist/integrations/pi/components/shell-hotkey-sections.js +1 -1
  96. package/dist/integrations/pi/components/shell-presenters-info.d.ts +4 -2
  97. package/dist/integrations/pi/components/shell-presenters-info.js +38 -4
  98. package/dist/integrations/pi/components/shell-shared-facade.d.ts +1 -1
  99. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  100. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
  101. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  102. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  103. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  104. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  105. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  106. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  107. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  108. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  109. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
  110. package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
  111. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  112. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  113. package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
  114. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  115. package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
  116. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  117. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  118. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
  119. package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
  120. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  121. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  122. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  123. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
  124. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +0 -2
  125. package/dist/integrations/pi/components/upstream/components/tool-execution.js +41 -20
  126. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  127. package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
  128. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  129. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  130. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
  131. package/dist/integrations/pi/components/upstream/history/editor-core.js +5 -5
  132. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  133. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  134. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  135. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  136. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  137. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  138. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  139. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  140. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  141. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  142. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +3 -0
  143. package/dist/integrations/pi/components/upstream/theme/theme.js +24 -1
  144. package/dist/integrations/pi/engine/adapter.d.ts +1 -0
  145. package/dist/integrations/pi/engine/adapter.js +4 -0
  146. package/dist/integrations/pi/engine/pi-settings-metadata.json +2 -2
  147. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
  148. package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
  149. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  150. package/dist/integrations/pi/engine/runtime-integration.js +102 -29
  151. package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
  152. package/dist/integrations/pi/engine/session-runtime.js +8 -3
  153. package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
  154. package/dist/integrations/pi/engine/session-selection.js +45 -9
  155. package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
  156. package/dist/integrations/pi/startup-public.js +152 -222
  157. package/dist/integrations/pi/startup-public.manifest.json +266 -177
  158. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  159. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  160. package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
  161. package/dist/native/darwin-arm64/manifest.json +1 -1
  162. package/dist/native/darwin-arm64/terminal-host +0 -0
  163. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
  164. package/dist/native/darwin-arm64/terminal-host.manifest.json +44 -0
  165. package/dist/native/darwin-x64/a1_session_lock.node +0 -0
  166. package/dist/native/darwin-x64/manifest.json +20 -0
  167. package/dist/native/darwin-x64/process-guardian +0 -0
  168. package/dist/native/darwin-x64/terminal-host +0 -0
  169. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
  170. package/dist/native/darwin-x64/terminal-host.manifest.json +44 -0
  171. package/dist/native/linux-x64/a1_session_lock.node +0 -0
  172. package/dist/native/linux-x64/manifest.json +1 -1
  173. package/dist/native/linux-x64/terminal-host +0 -0
  174. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
  175. package/dist/native/linux-x64/terminal-host.manifest.json +44 -0
  176. package/dist/native/win32-x64/a1_session_lock.node +0 -0
  177. package/dist/native/win32-x64/manifest.json +2 -2
  178. package/dist/native/win32-x64/process-guardian.exe +0 -0
  179. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
  180. package/dist/native/win32-x64/terminal-host.exe +0 -0
  181. package/dist/native/win32-x64/terminal-host.manifest.json +44 -0
  182. package/dist/product-identity.d.ts +1 -1
  183. package/dist/product-identity.js +1 -1
  184. package/dist/product-identity.json +5 -0
  185. package/dist/runtime-payload-inventory.json +5349 -6035
  186. package/dist/ui/components/line-input.d.ts +1 -1
  187. package/dist/ui/components/prompt-input.d.ts +7 -5
  188. package/dist/ui/components/prompt-input.js +7 -7
  189. package/dist/ui/components/transcript-viewport.d.ts +6 -3
  190. package/dist/ui/components/transcript-viewport.js +13 -15
  191. package/dist/ui/settings/declarations.d.ts +25 -1
  192. package/dist/ui/settings/declarations.js +21 -1
  193. package/dist/ui/settings/index.d.ts +4 -2
  194. package/dist/ui/settings/index.js +2 -1
  195. package/dist/ui/settings/manager.d.ts +10 -3
  196. package/dist/ui/settings/manager.js +90 -30
  197. package/dist/ui/settings/migrations.d.ts +6 -0
  198. package/dist/ui/settings/migrations.js +44 -0
  199. package/dist/ui/settings/profile-document.d.ts +26 -0
  200. package/dist/ui/settings/profile-document.js +164 -0
  201. package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
  202. package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
  203. package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
  204. package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
  205. package/docs/architecture/internal-naming.md +3 -1
  206. package/docs/architecture/process-guardian-provenance.md +2 -0
  207. package/docs/architecture/resident-tabs-requirements.md +18 -0
  208. package/docs/architecture/resident-tabs.md +594 -0
  209. package/docs/architecture/terminal-host-proof-gate.md +2 -0
  210. package/docs/architecture/terminal-host-provenance.md +13 -3
  211. package/docs/architecture/toolchain.md +1 -1
  212. package/docs/ci-release-runbook.md +2 -2
  213. package/docs/features/launch-profiles.md +11 -8
  214. package/docs/manual-resident-tabs-acceptance.md +51 -0
  215. package/docs/openspec-archive-automation.md +17 -16
  216. package/docs/validation.md +9 -1
  217. package/package.json +10 -4
@@ -0,0 +1,166 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { homedir, release as osRelease } from "node:os";
4
+ import { basename, dirname, resolve, sep } from "node:path";
5
+ import { resolveTerminalHostPath } from "./ensure-terminal-host.js";
6
+ import { TERMINAL_HOST_MANIFEST_FILENAME, verifyTerminalHostArtifact } from "./terminal-host-artifact.js";
7
+ import { encodeZip } from "./zip-archive.js";
8
+ export const DOCTOR_MAX_BYTES = 25 * 1024 * 1024;
9
+ const LOG_NAME = /^terminal-host-[a-z0-9-]+\.log(?:\.(\d{1,2}))?$/u;
10
+ const CRASH_NAME = /^crash-[a-z0-9-]+\.json$/u;
11
+ /**
12
+ * Writes an owner-only diagnostic bundle by allowlist: structured logs, crash records, host
13
+ * status, a redacted registry, the owner marker without secrets, artifact verification, platform
14
+ * facts, and the Tabs settings. Secrets, tokens, environment values, prompt journals, drafts,
15
+ * last screens, the private child-stderr sink, and terminal content are never read.
16
+ */
17
+ export async function writeDoctorBundle(options) {
18
+ const home = options.home ?? homedir();
19
+ const redact = (value) => homeRelative(value, home);
20
+ const now = options.now ?? new Date();
21
+ const maxBytes = options.maxBytes ?? DOCTOR_MAX_BYTES;
22
+ const diagnostics = resolve(options.stateDir, "diagnostics");
23
+ const names = await readdir(diagnostics).catch(() => []);
24
+ const fixed = [
25
+ json("host-status.json", redactHostStatus(options.hostStatus, redact)),
26
+ json("registry.json", await redactedRegistry(resolve(options.stateDir, "registry.json"), redact)),
27
+ json("server-marker.json", await redactedMarker(resolve(options.stateDir, "server.json"), redact)),
28
+ json("artifacts.json", await artifactFacts(options.releaseRoots, options.platform ?? process.platform, options.architecture ?? process.arch, redact)),
29
+ json("platform.json", {
30
+ platform: options.platform ?? process.platform,
31
+ architecture: options.architecture ?? process.arch,
32
+ osRelease: osRelease(),
33
+ node: process.version,
34
+ detachmentMode: options.hostStatus.result?.detachmentMode ?? null,
35
+ }),
36
+ json("settings.json", { tabsMax: options.settings.tabsMax, tabsAutoName: options.settings.tabsAutoName }),
37
+ ];
38
+ const crashes = [];
39
+ for (const name of names.filter(name => CRASH_NAME.test(name)).sort()) {
40
+ crashes.push({ name: `crashes/${name}`, data: await readFile(resolve(diagnostics, name)) });
41
+ }
42
+ // Rationale: when the bundle would exceed its cap, the oldest log generations go first.
43
+ const logs = names
44
+ .filter(name => LOG_NAME.test(name))
45
+ .map(name => ({ name, generation: Number(LOG_NAME.exec(name)?.[1] ?? 0) }))
46
+ .sort((left, right) => left.generation - right.generation || left.name.localeCompare(right.name));
47
+ const dropped = [];
48
+ const included = [...fixed, ...crashes];
49
+ let budget = maxBytes - 64 * 1024 - included.reduce((total, entry) => total + entry.data.length, 0);
50
+ for (const log of logs) {
51
+ const size = (await stat(resolve(diagnostics, log.name))).size;
52
+ if (size > budget) {
53
+ dropped.push(log.name);
54
+ continue;
55
+ }
56
+ budget -= size;
57
+ included.push({ name: `logs/${log.name}`, data: await readFile(resolve(diagnostics, log.name)) });
58
+ }
59
+ const manifest = [
60
+ "a1 tabs doctor bundle",
61
+ `created: ${now.toISOString()}`,
62
+ ...included.map(entry => `included: ${entry.name}`),
63
+ ...dropped.map(name => `dropped to stay within ${maxBytes} bytes: logs/${name}`),
64
+ "",
65
+ ].join("\n");
66
+ const archive = encodeZip([{ name: "manifest.txt", data: Buffer.from(manifest, "utf8") }, ...included], now);
67
+ const directory = resolve(options.stateDir, "doctor");
68
+ await mkdir(directory, { recursive: true, mode: 0o700 });
69
+ const path = resolve(directory, `a1-tabs-doctor-${now.toISOString().replace(/[:.]/gu, "-")}.zip`);
70
+ await writeFile(path, archive, { mode: 0o600, flag: "wx" });
71
+ return { path, bytes: archive.length, dropped };
72
+ }
73
+ function json(name, value) {
74
+ return { name, data: Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8") };
75
+ }
76
+ function stableHash(value) {
77
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
78
+ }
79
+ function homeRelative(value, home) {
80
+ const normalized = resolve(value);
81
+ const root = resolve(home);
82
+ const comparable = (path) => process.platform === "win32" ? path.toLowerCase() : path;
83
+ if (comparable(normalized) === comparable(root))
84
+ return "~";
85
+ return comparable(normalized).startsWith(comparable(`${root}${sep}`)) ? `~${normalized.slice(root.length).replaceAll("\\", "/")}` : normalized;
86
+ }
87
+ function redactHostStatus(report, redact) {
88
+ const result = report.result ? { ...report.result } : undefined;
89
+ if (result && Array.isArray(result.logs))
90
+ result.logs = result.logs.map(path => typeof path === "string" ? basename(path) : null);
91
+ if (result && typeof result.release === "string")
92
+ result.release = redact(result.release);
93
+ return {
94
+ host: report.host,
95
+ pid: report.pid ?? null,
96
+ exitCode: report.exitCode,
97
+ result: result ?? null,
98
+ };
99
+ }
100
+ async function redactedRegistry(path, redact) {
101
+ let document;
102
+ try {
103
+ document = JSON.parse(await readFile(path, "utf8"));
104
+ }
105
+ catch (error) {
106
+ return { readable: false, reason: error instanceof Error && "code" in error ? String(error.code) : "unparseable" };
107
+ }
108
+ const tabs = Array.isArray(document.tabs) ? document.tabs : [];
109
+ const text = (value) => typeof value === "string" ? value : null;
110
+ return {
111
+ readable: true,
112
+ registryRevision: document.registryRevision ?? null,
113
+ epoch: document.epoch ?? null,
114
+ tabs: tabs.map(tab => {
115
+ const sessionFile = text(tab.sessionFile);
116
+ return {
117
+ tabId: text(tab.tabId),
118
+ // Security: names can be generated from prompts, so only a stable hash is kept.
119
+ nameHash: text(tab.displayName) === null ? null : stableHash(text(tab.displayName)),
120
+ nameSource: text(tab.nameSource),
121
+ order: tab.order ?? null,
122
+ cwd: text(tab.cwd) === null ? null : redact(text(tab.cwd)),
123
+ sessionDirectory: sessionFile === null ? null : redact(dirname(sessionFile)),
124
+ sessionFileHash: sessionFile === null ? null : stableHash(basename(sessionFile)),
125
+ desired: text(tab.desired),
126
+ lifecycle: text(tab.lifecycle),
127
+ incarnation: tab.incarnation ?? null,
128
+ holderPid: tab.holderPid ?? null,
129
+ childPid: tab.childPid ?? null,
130
+ release: text(tab.release) === null ? null : redact(text(tab.release)),
131
+ lastExit: text(tab.lastExit),
132
+ restarts: tab.restarts ?? null,
133
+ };
134
+ }),
135
+ };
136
+ }
137
+ async function redactedMarker(path, redact) {
138
+ try {
139
+ const marker = JSON.parse(await readFile(path, "utf8"));
140
+ return {
141
+ present: true,
142
+ pid: marker.pid ?? null,
143
+ generation: marker.generation ?? null,
144
+ verified: marker.verified ?? null,
145
+ detachmentMode: marker.detachmentMode ?? null,
146
+ releaseRoot: typeof marker.releaseRoot === "string" ? redact(marker.releaseRoot) : null,
147
+ };
148
+ }
149
+ catch {
150
+ return { present: false };
151
+ }
152
+ }
153
+ async function artifactFacts(roots, platform, architecture, redact) {
154
+ const facts = [];
155
+ for (const root of roots) {
156
+ const executable = resolveTerminalHostPath(root, {}, platform, architecture);
157
+ const manifest = await readFile(resolve(dirname(executable), TERMINAL_HOST_MANIFEST_FILENAME), "utf8")
158
+ .then(text => JSON.parse(text))
159
+ .catch(() => null);
160
+ const verification = await verifyTerminalHostArtifact(executable, platform, architecture)
161
+ .then(() => "verified")
162
+ .catch((error) => error instanceof Error && "code" in error ? String(error.code) : "failed");
163
+ facts.push({ releaseRoot: redact(root), verification, manifest });
164
+ }
165
+ return facts;
166
+ }
@@ -0,0 +1,27 @@
1
+ import { ResidentTabFaultInjection } from "./fault-injection.js";
2
+ export interface DraftCheckpointState {
3
+ readonly text: string;
4
+ readonly savedAt: string;
5
+ }
6
+ export interface DraftCheckpointOptions {
7
+ readonly intervalMs?: number;
8
+ readonly maximumDirtyMs?: number;
9
+ readonly faults?: ResidentTabFaultInjection;
10
+ readonly now?: () => number;
11
+ readonly onDegraded?: (degraded: boolean) => void;
12
+ readonly write?: (path: string, text: string) => Promise<void>;
13
+ readonly move?: (from: string, to: string) => Promise<void>;
14
+ readonly schedule?: (callback: () => void | Promise<void>, delayMs: number) => unknown;
15
+ readonly cancel?: (timer: unknown) => void;
16
+ }
17
+ /** Periodic rather than inactivity-debounced: continuous input still checkpoints every interval. */
18
+ export declare class DraftCheckpointer {
19
+ #private;
20
+ readonly path: string;
21
+ constructor(path: string, options?: DraftCheckpointOptions);
22
+ get degraded(): boolean;
23
+ change(text: string): void;
24
+ restore(): Promise<DraftCheckpointState | null>;
25
+ flush(): Promise<void>;
26
+ close(): Promise<void>;
27
+ }
@@ -0,0 +1,145 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { ResidentTabFaultInjection } from "./fault-injection.js";
5
+ /** Periodic rather than inactivity-debounced: continuous input still checkpoints every interval. */
6
+ export class DraftCheckpointer {
7
+ path;
8
+ #intervalMs;
9
+ #maximumDirtyMs;
10
+ #faults;
11
+ #now;
12
+ #onDegraded;
13
+ #write;
14
+ #move;
15
+ #scheduleTimer;
16
+ #cancelTimer;
17
+ #text = "";
18
+ #dirtySince = null;
19
+ #timer = null;
20
+ #degradedTimer = null;
21
+ #writing = null;
22
+ #closed = false;
23
+ #degraded = false;
24
+ constructor(path, options = {}) {
25
+ this.path = path;
26
+ this.#intervalMs = options.intervalMs ?? 500;
27
+ this.#maximumDirtyMs = options.maximumDirtyMs ?? 1_000;
28
+ this.#faults = options.faults ?? new ResidentTabFaultInjection();
29
+ this.#now = options.now ?? Date.now;
30
+ this.#onDegraded = options.onDegraded ?? (() => { });
31
+ this.#write = options.write ?? (async (target, text) => writeFile(target, text, { encoding: "utf8", mode: 0o600, flag: "wx" }));
32
+ this.#move = options.move ?? rename;
33
+ this.#scheduleTimer = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs));
34
+ this.#cancelTimer = options.cancel ?? (timer => clearTimeout(timer));
35
+ }
36
+ get degraded() { return this.#degraded; }
37
+ change(text) {
38
+ if (this.#closed)
39
+ return;
40
+ this.#text = text;
41
+ this.#dirtySince ??= this.#now();
42
+ this.#schedule();
43
+ this.#armDegradedDeadline();
44
+ }
45
+ async restore() {
46
+ try {
47
+ const value = JSON.parse(await readFile(this.path, "utf8"));
48
+ if (typeof value !== "object" || value === null || typeof value.text !== "string"
49
+ || typeof value.savedAt !== "string")
50
+ return null;
51
+ return value;
52
+ }
53
+ catch (error) {
54
+ if (error.code === "ENOENT" || error instanceof SyntaxError)
55
+ return null;
56
+ throw error;
57
+ }
58
+ }
59
+ async flush() {
60
+ if (this.#timer !== null) {
61
+ this.#cancelTimer(this.#timer);
62
+ this.#timer = null;
63
+ }
64
+ await this.#writing;
65
+ if (this.#dirtySince !== null)
66
+ await this.#checkpoint();
67
+ }
68
+ async close() {
69
+ await this.flush();
70
+ this.#closed = true;
71
+ }
72
+ #armDegradedDeadline() {
73
+ if (this.#degradedTimer !== null || this.#dirtySince === null || this.#closed)
74
+ return;
75
+ const delay = Math.max(0, this.#maximumDirtyMs - (this.#now() - this.#dirtySince));
76
+ this.#degradedTimer = this.#scheduleTimer(() => {
77
+ this.#degradedTimer = null;
78
+ if (this.#dirtySince !== null && this.#now() - this.#dirtySince >= this.#maximumDirtyMs) {
79
+ this.#setDegraded(true);
80
+ }
81
+ else if (this.#dirtySince !== null) {
82
+ this.#armDegradedDeadline();
83
+ }
84
+ }, delay);
85
+ }
86
+ #clearDegradedDeadline() {
87
+ if (this.#degradedTimer !== null)
88
+ this.#cancelTimer(this.#degradedTimer);
89
+ this.#degradedTimer = null;
90
+ }
91
+ #schedule() {
92
+ if (this.#timer !== null || this.#writing !== null || this.#closed)
93
+ return;
94
+ const age = this.#dirtySince === null ? 0 : this.#now() - this.#dirtySince;
95
+ if (age > this.#maximumDirtyMs)
96
+ this.#setDegraded(true);
97
+ this.#timer = this.#scheduleTimer(async () => {
98
+ this.#timer = null;
99
+ this.#writing = this.#checkpoint().finally(() => {
100
+ this.#writing = null;
101
+ if (this.#dirtySince !== null)
102
+ this.#schedule();
103
+ });
104
+ await this.#writing.catch(() => undefined);
105
+ }, this.#intervalMs);
106
+ }
107
+ async #checkpoint() {
108
+ if (this.#dirtySince === null)
109
+ return;
110
+ const snapshot = this.#text;
111
+ const temporary = `${this.path}.tmp-${process.pid}-${randomUUID()}`;
112
+ try {
113
+ await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
114
+ if (process.platform !== "win32")
115
+ await chmod(dirname(this.path), 0o700);
116
+ this.#faults.at("draft-during-temp-write");
117
+ await this.#write(temporary, `${JSON.stringify({ text: snapshot, savedAt: new Date(this.#now()).toISOString() })}\n`);
118
+ this.#faults.at("draft-before-rename");
119
+ await this.#move(temporary, this.path);
120
+ this.#faults.at("draft-after-rename");
121
+ if (process.platform !== "win32")
122
+ await chmod(this.path, 0o600);
123
+ if (this.#text === snapshot)
124
+ this.#dirtySince = null;
125
+ else
126
+ this.#dirtySince = this.#now();
127
+ this.#clearDegradedDeadline();
128
+ if (this.#dirtySince !== null)
129
+ this.#armDegradedDeadline();
130
+ this.#setDegraded(false);
131
+ }
132
+ catch (error) {
133
+ await unlink(temporary).catch(() => undefined);
134
+ this.#clearDegradedDeadline();
135
+ this.#setDegraded(true);
136
+ throw error;
137
+ }
138
+ }
139
+ #setDegraded(value) {
140
+ if (this.#degraded === value)
141
+ return;
142
+ this.#degraded = value;
143
+ this.#onDegraded(value);
144
+ }
145
+ }
@@ -0,0 +1,31 @@
1
+ export type TerminalHostFallbackReason = "unsupported platform" | "no interactive terminal" | "terminal host not installed" | "terminal host not verified" | "host probe failed" | "incompatible generation" | "registry unreadable" | "recovery blocked" | "launch not verified" | "host startup timed out";
2
+ export type EnsureTerminalHostResult = {
3
+ readonly status: "ready";
4
+ readonly executable: string;
5
+ readonly endpoint: string;
6
+ } | {
7
+ readonly status: "fallback";
8
+ readonly reason: TerminalHostFallbackReason;
9
+ };
10
+ interface HostCommandResult {
11
+ readonly code: number;
12
+ readonly stdout: string;
13
+ readonly stderr: string;
14
+ }
15
+ export interface EnsureTerminalHostOptions {
16
+ readonly releaseRoot: string;
17
+ readonly profileRoot: string;
18
+ readonly dataDir: string;
19
+ readonly runtimeDir: string;
20
+ readonly environment: NodeJS.ProcessEnv;
21
+ readonly platform?: NodeJS.Platform;
22
+ readonly architecture?: string;
23
+ readonly timeoutMs?: number;
24
+ readonly maxConcurrentStarts?: number;
25
+ readonly unresponsiveRestartSeconds?: number;
26
+ readonly run?: (executable: string, arguments_: readonly string[], environment: NodeJS.ProcessEnv, timeoutMs: number) => Promise<HostCommandResult>;
27
+ readonly verify?: (executable: string, platform: NodeJS.Platform, architecture: string) => Promise<unknown>;
28
+ }
29
+ export declare function resolveTerminalHostPath(releaseRoot: string, environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, architecture?: string): string;
30
+ export declare function ensureTerminalHost(options: EnsureTerminalHostOptions): Promise<EnsureTerminalHostResult>;
31
+ export {};
@@ -0,0 +1,147 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import { constants } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
6
+ import { TerminalHostArtifactError, verifyTerminalHostArtifact } from "./terminal-host-artifact.js";
7
+ export function resolveTerminalHostPath(releaseRoot, environment = process.env, platform = process.platform, architecture = process.arch) {
8
+ const override = developmentOverride(environment);
9
+ if (override)
10
+ return resolve(override);
11
+ const executable = platform === "win32" ? `${PRODUCT_IDENTITY.artifacts.nativeExecutable}.exe` : PRODUCT_IDENTITY.artifacts.nativeExecutable;
12
+ return resolve(releaseRoot, "dist", "native", `${platform}-${architecture}`, executable);
13
+ }
14
+ /** The terminal-host override is a development aid; installed releases always run their own verified artifact. */
15
+ function developmentOverride(environment) {
16
+ if (!environment[PRODUCT_IDENTITY.environment.developmentInstanceId])
17
+ return undefined;
18
+ return environment[PRODUCT_IDENTITY.environment.terminalHostPath]?.trim() || undefined;
19
+ }
20
+ export async function ensureTerminalHost(options) {
21
+ const platform = options.platform ?? process.platform;
22
+ const architecture = options.architecture ?? process.arch;
23
+ if (!supported(platform, architecture))
24
+ return { status: "fallback", reason: "unsupported platform" };
25
+ const executable = resolveTerminalHostPath(options.releaseRoot, options.environment, platform, architecture);
26
+ if (developmentOverride(options.environment)) {
27
+ try {
28
+ await access(executable, constants.R_OK | (platform === "win32" ? 0 : constants.X_OK));
29
+ }
30
+ catch {
31
+ return { status: "fallback", reason: "terminal host not installed" };
32
+ }
33
+ }
34
+ else {
35
+ try {
36
+ await (options.verify ?? verifyTerminalHostArtifact)(executable, platform, architecture);
37
+ }
38
+ catch (error) {
39
+ return { status: "fallback", reason: verificationReason(error) };
40
+ }
41
+ }
42
+ const run = options.run ?? runHostCommand;
43
+ const timeoutMs = options.timeoutMs ?? 8_000;
44
+ const common = ["--profile", options.profileRoot, "--data-dir", options.dataDir, "--runtime-dir", options.runtimeDir];
45
+ const environment = { ...options.environment };
46
+ try {
47
+ const probe = await run(executable, ["probe", ...common], environment, Math.min(4_000, timeoutMs));
48
+ const existing = parseResult(probe.stdout);
49
+ if (probe.code === 0) {
50
+ if (existing?.generation === 1 && typeof existing.endpoint === "string") {
51
+ return { status: "ready", executable, endpoint: existing.endpoint };
52
+ }
53
+ return {
54
+ status: "fallback",
55
+ reason: typeof existing?.generation === "number" ? "incompatible generation" : "host probe failed",
56
+ };
57
+ }
58
+ if (probe.code === 4 || existing?.outcome === "incompatible-generation")
59
+ return { status: "fallback", reason: "incompatible generation" };
60
+ if (existing?.outcome !== "not-running" && probe.code !== 3)
61
+ return { status: "fallback", reason: classifiedReason(existing?.reason, "host probe failed") };
62
+ const maximum = Math.max(1, Math.min(8, Math.trunc(options.maxConcurrentStarts ?? 2)));
63
+ const unresponsiveRestartSeconds = Math.max(30, Math.min(600, Math.trunc(options.unresponsiveRestartSeconds ?? 120)));
64
+ const start = await run(executable, [
65
+ "server", "--detach", ...common,
66
+ "--max-concurrent-starts", String(maximum),
67
+ "--unresponsive-restart-seconds", String(unresponsiveRestartSeconds),
68
+ ], environment, timeoutMs);
69
+ const started = parseResult(start.stdout);
70
+ if (start.code !== 0)
71
+ return { status: "fallback", reason: classifiedReason(started?.reason, "launch not verified") };
72
+ if (started?.verified !== true || typeof started.endpoint !== "string")
73
+ return { status: "fallback", reason: "launch not verified" };
74
+ const deadline = Date.now() + timeoutMs;
75
+ while (Date.now() < deadline) {
76
+ const result = await run(executable, ["probe", ...common], environment, Math.min(1_000, Math.max(1, deadline - Date.now())));
77
+ const ready = parseResult(result.stdout);
78
+ if (result.code === 0) {
79
+ if (ready?.generation === 1 && typeof ready.endpoint === "string") {
80
+ return { status: "ready", executable, endpoint: ready.endpoint };
81
+ }
82
+ return {
83
+ status: "fallback",
84
+ reason: typeof ready?.generation === "number" ? "incompatible generation" : "host probe failed",
85
+ };
86
+ }
87
+ if (result.code === 4 || ready?.outcome === "incompatible-generation")
88
+ return { status: "fallback", reason: "incompatible generation" };
89
+ await new Promise(resolvePromise => setTimeout(resolvePromise, 40));
90
+ }
91
+ return { status: "fallback", reason: "host startup timed out" };
92
+ }
93
+ catch (error) {
94
+ return { status: "fallback", reason: isTimeout(error) ? "host startup timed out" : "host probe failed" };
95
+ }
96
+ }
97
+ function supported(platform, architecture) {
98
+ return (platform === "win32" || platform === "linux") && architecture === "x64"
99
+ || platform === "darwin" && (architecture === "arm64" || architecture === "x64");
100
+ }
101
+ function verificationReason(error) {
102
+ if (!(error instanceof TerminalHostArtifactError))
103
+ return "terminal host not verified";
104
+ if (error.code === "TERMINAL_HOST_ARTIFACT_MISSING")
105
+ return "terminal host not installed";
106
+ if (error.code === "TERMINAL_HOST_ARTIFACT_UNSUPPORTED")
107
+ return "unsupported platform";
108
+ return "terminal host not verified";
109
+ }
110
+ function parseResult(output) {
111
+ const line = output.trim().split(/\r?\n/).filter(Boolean).at(-1);
112
+ if (!line)
113
+ return null;
114
+ try {
115
+ const parsed = JSON.parse(line);
116
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ function classifiedReason(value, fallback) {
123
+ if (value === "registry-unreadable")
124
+ return "registry unreadable";
125
+ if (value === "blocked")
126
+ return "recovery blocked";
127
+ if (value === "incompatible-generation")
128
+ return "incompatible generation";
129
+ return fallback;
130
+ }
131
+ function isTimeout(error) { return error instanceof Error && "code" in error && error.code === "ETIMEDOUT"; }
132
+ async function runHostCommand(executable, arguments_, environment, timeoutMs) {
133
+ return await new Promise((resolvePromise, rejectPromise) => {
134
+ const child = spawn(executable, [...arguments_], { env: environment, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
135
+ const stdout = [];
136
+ const stderr = [];
137
+ child.stdout.on("data", chunk => stdout.push(Buffer.from(chunk)));
138
+ child.stderr.on("data", chunk => stderr.push(Buffer.from(chunk)));
139
+ child.once("error", rejectPromise);
140
+ child.once("close", code => { clearTimeout(timer); resolvePromise({ code: code ?? 1, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") }); });
141
+ const timer = setTimeout(() => {
142
+ child.kill("SIGKILL");
143
+ rejectPromise(Object.assign(new Error("terminal host command timed out"), { code: "ETIMEDOUT" }));
144
+ }, timeoutMs);
145
+ timer.unref();
146
+ });
147
+ }
@@ -0,0 +1,8 @@
1
+ export declare const RESIDENT_TAB_FAULT_ENVIRONMENT: readonly ["A1_TABS_FAULT_CRASH_POINT", "A1_TABS_HERMETIC_TEST"];
2
+ export declare function residentTabFaultEnvironment(point: string, hermetic?: boolean): NodeJS.ProcessEnv;
3
+ /** Test-only crash points are inert unless the caller explicitly marks a hermetic process. */
4
+ export declare class ResidentTabFaultInjection {
5
+ #private;
6
+ constructor(environment?: NodeJS.ProcessEnv, crash?: (point: string) => never);
7
+ at(point: string): void;
8
+ }
@@ -0,0 +1,26 @@
1
+ const CRASH_POINT = "A1_TABS_FAULT_CRASH_POINT";
2
+ const HERMETIC = "A1_TABS_HERMETIC_TEST";
3
+ export const RESIDENT_TAB_FAULT_ENVIRONMENT = Object.freeze([CRASH_POINT, HERMETIC]);
4
+ export function residentTabFaultEnvironment(point, hermetic = true) {
5
+ return Object.fromEntries(hermetic ? [[CRASH_POINT, point], [HERMETIC, "1"]] : [[CRASH_POINT, point]]);
6
+ }
7
+ /** Test-only crash points are inert unless the caller explicitly marks a hermetic process. */
8
+ export class ResidentTabFaultInjection {
9
+ #point;
10
+ #crash;
11
+ constructor(environment = process.env, crash = point => {
12
+ process.stderr.write(`resident tab injected crash: ${point}\n`);
13
+ process.abort();
14
+ }) {
15
+ const point = environment[CRASH_POINT];
16
+ if (point !== undefined && environment[HERMETIC] !== "1") {
17
+ throw new Error(`${CRASH_POINT} is available only in a hermetic resident-tab test`);
18
+ }
19
+ this.#point = point;
20
+ this.#crash = crash;
21
+ }
22
+ at(point) {
23
+ if (this.#point === point)
24
+ this.#crash(point);
25
+ }
26
+ }
@@ -0,0 +1,12 @@
1
+ export { ensureTerminalHost, resolveTerminalHostPath } from "./ensure-terminal-host.js";
2
+ export type { EnsureTerminalHostOptions, EnsureTerminalHostResult, TerminalHostFallbackReason } from "./ensure-terminal-host.js";
3
+ export { readResidentTabLaunchSettings } from "./settings.js";
4
+ export type { ResidentTabLaunchSettings } from "./settings.js";
5
+ export { selectResidentHost } from "./routing.js";
6
+ export type { ResidentHostSelectionOptions } from "./routing.js";
7
+ export { readResidentReleaseHolds } from "./release-holds.js";
8
+ export type { ResidentReleaseHoldReport } from "./release-holds.js";
9
+ export { runTerminalHostMaintenance } from "./maintenance.js";
10
+ export type { MaintenanceAction, MaintenanceHostState, MaintenanceOptions, MaintenanceReport } from "./maintenance.js";
11
+ export { DOCTOR_MAX_BYTES, writeDoctorBundle } from "./doctor.js";
12
+ export type { DoctorBundle, DoctorOptions } from "./doctor.js";
@@ -0,0 +1,6 @@
1
+ export { ensureTerminalHost, resolveTerminalHostPath } from "./ensure-terminal-host.js";
2
+ export { readResidentTabLaunchSettings } from "./settings.js";
3
+ export { selectResidentHost } from "./routing.js";
4
+ export { readResidentReleaseHolds } from "./release-holds.js";
5
+ export { runTerminalHostMaintenance } from "./maintenance.js";
6
+ export { DOCTOR_MAX_BYTES, writeDoctorBundle } from "./doctor.js";
@@ -0,0 +1,42 @@
1
+ export type MaintenanceAction = "list" | "stop" | "stop-all" | "host-status" | "host-stop";
2
+ export type MaintenanceHostState = "none" | "running" | "stopped" | "unresponsive" | "incompatible" | "refused" | "unverifiable" | "unavailable";
3
+ /** The versioned `a1 tabs` report: `a1-tabs-maintenance-v1`, as printed by the terminal host. */
4
+ export interface MaintenanceReport {
5
+ readonly schema: "a1-tabs-maintenance-v1";
6
+ readonly command: string;
7
+ readonly host: MaintenanceHostState;
8
+ readonly stateDir?: string;
9
+ readonly result?: Record<string, unknown>;
10
+ readonly pid?: number;
11
+ readonly message?: string;
12
+ readonly releaseRoot?: string | null;
13
+ readonly terminatedUnresponsive?: number;
14
+ /** The terminal host's exit status: 0 success, 3 refused or failed, 4 another generation. */
15
+ readonly exitCode: number;
16
+ }
17
+ export interface MaintenanceOptions {
18
+ readonly action: MaintenanceAction;
19
+ readonly tabId?: string;
20
+ readonly profileRoot: string;
21
+ readonly dataDir: string;
22
+ readonly runtimeDir: string;
23
+ /** Installed release roots whose verified terminal host may run maintenance, in preference order. */
24
+ readonly releaseRoots: readonly string[];
25
+ readonly environment: NodeJS.ProcessEnv;
26
+ readonly platform?: NodeJS.Platform;
27
+ readonly architecture?: string;
28
+ readonly timeoutMs?: number;
29
+ readonly verify?: (executable: string, platform: NodeJS.Platform, architecture: string) => Promise<unknown>;
30
+ readonly run?: (executable: string, arguments_: readonly string[], timeoutMs: number) => Promise<{
31
+ readonly code: number;
32
+ readonly stdout: string;
33
+ }>;
34
+ }
35
+ /**
36
+ * Runs one maintenance form through a verified terminal host. When the running host speaks
37
+ * another protocol generation, it retries once with the binary from the release that host
38
+ * recorded, after verifying that release's manifest; it never runs an unverified binary.
39
+ */
40
+ export declare function runTerminalHostMaintenance(options: MaintenanceOptions): Promise<MaintenanceReport>;
41
+ /** The largest maintenance report accepted from the terminal host. */
42
+ export declare const MAX_MAINTENANCE_REPORT_BYTES: number;