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

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 (149) hide show
  1. package/bin/guardian.js +5 -0
  2. package/bin/tab-launch-context.d.ts +23 -0
  3. package/bin/tab-launch-context.js +45 -0
  4. package/bin/ui.js +20 -5
  5. package/dist/app/session-shell/helper-pool.d.ts +3 -5
  6. package/dist/app/session-shell/helper-pool.js +3 -5
  7. package/dist/app/session-shell/session-shell-root.d.ts +29 -1
  8. package/dist/app/session-shell/session-shell-root.js +3 -2
  9. package/dist/app/session-shell/session-shell.js +153 -30
  10. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -1
  11. package/dist/app/session-shell/session-viewport-controller.js +3 -3
  12. package/dist/composition/owned-ui.d.ts +20 -0
  13. package/dist/composition/owned-ui.js +76 -3
  14. package/dist/contracts/owned-ui/session-backend.d.ts +1 -0
  15. package/dist/features/owned-ui/settings-app.js +8 -35
  16. package/dist/foundation/launch-context/index.d.ts +6 -0
  17. package/dist/foundation/launch-context/index.js +15 -0
  18. package/dist/foundation/launch-guardian/main.d.ts +5 -0
  19. package/dist/foundation/launch-guardian/main.js +2 -1
  20. package/dist/foundation/release/bootstrap.d.ts +4 -0
  21. package/dist/foundation/release/bootstrap.js +44 -6
  22. package/dist/foundation/release/dependency-certification.js +54 -19
  23. package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
  24. package/dist/foundation/release/progress-palette.generated.js +2 -2
  25. package/dist/foundation/resident-tabs/bridge-client.d.ts +41 -0
  26. package/dist/foundation/resident-tabs/bridge-client.js +199 -0
  27. package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
  28. package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
  29. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +30 -0
  30. package/dist/foundation/resident-tabs/ensure-terminal-host.js +121 -0
  31. package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
  32. package/dist/foundation/resident-tabs/fault-injection.js +26 -0
  33. package/dist/foundation/resident-tabs/index.d.ts +6 -0
  34. package/dist/foundation/resident-tabs/index.js +3 -0
  35. package/dist/foundation/resident-tabs/prompt-journal.d.ts +65 -0
  36. package/dist/foundation/resident-tabs/prompt-journal.js +214 -0
  37. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  38. package/dist/foundation/resident-tabs/protocol.js +195 -0
  39. package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
  40. package/dist/foundation/resident-tabs/recovery.js +205 -0
  41. package/dist/foundation/resident-tabs/routing.d.ts +10 -0
  42. package/dist/foundation/resident-tabs/routing.js +8 -0
  43. package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
  44. package/dist/foundation/resident-tabs/session-sync.js +37 -0
  45. package/dist/foundation/resident-tabs/settings.d.ts +9 -0
  46. package/dist/foundation/resident-tabs/settings.js +65 -0
  47. package/dist/foundation/session-writer-lock/index.d.ts +27 -0
  48. package/dist/foundation/session-writer-lock/index.js +197 -0
  49. package/dist/foundation/startup/startup-descriptor.js +2 -2
  50. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  51. package/dist/foundation/terminal-cleanup/index.js +1 -1
  52. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +1 -0
  53. package/dist/foundation/terminal-cleanup/terminal-reset.js +1 -0
  54. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  55. package/dist/integrations/pi/components/owned-editor-ux.js +2 -2
  56. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
  57. package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
  58. package/dist/integrations/pi/components/shell-shared-facade.d.ts +1 -1
  59. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  60. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
  61. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  62. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  63. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  64. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  65. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  66. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  67. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  68. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  69. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
  70. package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
  71. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  72. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  73. package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
  74. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  75. package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
  76. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  77. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  78. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
  79. package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
  80. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  81. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  82. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  83. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
  84. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +0 -2
  85. package/dist/integrations/pi/components/upstream/components/tool-execution.js +41 -20
  86. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  87. package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
  88. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  89. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  90. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
  91. package/dist/integrations/pi/components/upstream/history/editor-core.js +5 -5
  92. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  93. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  94. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  95. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  96. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  97. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  98. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  99. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  100. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  101. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  102. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +3 -0
  103. package/dist/integrations/pi/components/upstream/theme/theme.js +24 -1
  104. package/dist/integrations/pi/engine/adapter.js +1 -0
  105. package/dist/integrations/pi/engine/pi-settings-metadata.json +2 -2
  106. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  107. package/dist/integrations/pi/engine/runtime-integration.js +102 -29
  108. package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
  109. package/dist/integrations/pi/engine/session-runtime.js +8 -3
  110. package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
  111. package/dist/integrations/pi/engine/session-selection.js +45 -9
  112. package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
  113. package/dist/integrations/pi/startup-public.js +152 -222
  114. package/dist/integrations/pi/startup-public.manifest.json +266 -177
  115. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  116. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  117. package/dist/native/darwin-arm64/manifest.json +1 -1
  118. package/dist/native/linux-x64/manifest.json +1 -1
  119. package/dist/native/win32-x64/manifest.json +2 -2
  120. package/dist/native/win32-x64/process-guardian.exe +0 -0
  121. package/dist/product-identity.d.ts +1 -1
  122. package/dist/product-identity.js +1 -1
  123. package/dist/product-identity.json +5 -0
  124. package/dist/runtime-payload-inventory.json +5349 -6035
  125. package/dist/ui/components/line-input.d.ts +1 -1
  126. package/dist/ui/components/prompt-input.d.ts +7 -5
  127. package/dist/ui/components/prompt-input.js +7 -7
  128. package/dist/ui/components/transcript-viewport.d.ts +6 -3
  129. package/dist/ui/components/transcript-viewport.js +13 -15
  130. package/dist/ui/settings/declarations.d.ts +49 -1
  131. package/dist/ui/settings/declarations.js +37 -1
  132. package/dist/ui/settings/index.d.ts +5 -3
  133. package/dist/ui/settings/index.js +3 -2
  134. package/dist/ui/settings/manager.d.ts +9 -3
  135. package/dist/ui/settings/manager.js +81 -29
  136. package/dist/ui/settings/migrations.d.ts +6 -0
  137. package/dist/ui/settings/migrations.js +32 -0
  138. package/dist/ui/settings/profile-document.d.ts +26 -0
  139. package/dist/ui/settings/profile-document.js +164 -0
  140. package/dist/ui/settings/resolution.d.ts +2 -0
  141. package/dist/ui/settings/resolution.js +13 -1
  142. package/dist/ui/settings/sections.js +4 -0
  143. package/docs/architecture/internal-naming.md +1 -1
  144. package/docs/architecture/resident-tabs-requirements.md +580 -0
  145. package/docs/architecture/resident-tabs.md +522 -0
  146. package/docs/architecture/terminal-host-proof-gate.md +2 -0
  147. package/docs/openspec-archive-automation.md +17 -16
  148. package/docs/validation.md +1 -1
  149. package/package.json +6 -4
@@ -0,0 +1,199 @@
1
+ import { connect } from "node:net";
2
+ import { encodeResidentFrame, ResidentFrameDecoder } from "./protocol.js";
3
+ /** Metadata-only advisory bridge. Terminal input and transcript bytes have no API here. */
4
+ export class TabBridgeClient {
5
+ #options;
6
+ #transport = null;
7
+ #connected = false;
8
+ #available = true;
9
+ #features = new Set();
10
+ #sequence = 0;
11
+ #request = 0;
12
+ #attachment = null;
13
+ #closed = false;
14
+ #retry = null;
15
+ #latestStatus = null;
16
+ #latestSession = null;
17
+ #heartbeat = null;
18
+ #pending = new Map();
19
+ #startWaiter = null;
20
+ constructor(options) { this.#options = options; }
21
+ get available() { return this.#available && this.#connected; }
22
+ captureAttachment() { return this.#attachment === null ? null : { ...this.#attachment }; }
23
+ async start(timeoutMs = 4_000) {
24
+ if (this.#closed || this.#transport !== null)
25
+ return this.available;
26
+ const transport = (this.#options.connect ?? (endpoint => connect(endpoint)))(this.#options.credentials.endpoint);
27
+ this.#transport = transport;
28
+ const decoder = new ResidentFrameDecoder();
29
+ transport.on("data", bytes => {
30
+ try {
31
+ for (const message of decoder.push(bytes))
32
+ this.#receive(message);
33
+ }
34
+ catch {
35
+ if (this.#transport === transport)
36
+ this.#fail();
37
+ }
38
+ });
39
+ transport.once("close", () => { if (this.#transport === transport)
40
+ this.#fail(); });
41
+ transport.once("error", () => { if (this.#transport === transport)
42
+ this.#fail(); });
43
+ return await new Promise(resolvePromise => {
44
+ let settled = false;
45
+ let timer;
46
+ const finish = (value) => {
47
+ if (settled)
48
+ return;
49
+ settled = true;
50
+ if (timer !== undefined)
51
+ clearTimeout(timer);
52
+ this.#startWaiter = null;
53
+ resolvePromise(value);
54
+ };
55
+ this.#startWaiter = finish;
56
+ transport.once("connect", () => {
57
+ this.#send("hello", {
58
+ role: "bridge", generation: 1, build: this.#options.build ?? "a1",
59
+ features: ["bridge-status", "bridge-detach", "bridge-heartbeat", "bridge-restart", "recovery-journal"],
60
+ token: [...Buffer.from(this.#options.credentials.token, "base64")], tabId: this.#options.credentials.tabId,
61
+ pid: this.#options.pid ?? process.pid, startIdentity: this.#options.startIdentity ?? `${process.pid}:node`,
62
+ }, ++this.#request);
63
+ });
64
+ transport.once("error", () => finish(false));
65
+ timer = setTimeout(() => { this.#fail(); finish(false); }, timeoutMs);
66
+ timer.unref();
67
+ });
68
+ }
69
+ status(state, degraded = false) {
70
+ this.#latestStatus = { state, degraded };
71
+ const recoveryState = state === "unresponsive" || state === "stalled" || state === "crashed" || state === "restarting" || state === "failed";
72
+ if (recoveryState && !this.#supports("recovery-status"))
73
+ return;
74
+ this.#send("bridge.status", {
75
+ seq: ++this.#sequence,
76
+ state,
77
+ ...(degraded && this.#supports("recovery-status") ? { degraded: true } : {}),
78
+ });
79
+ }
80
+ degraded(degraded) {
81
+ this.status(this.#latestStatus?.state ?? "idle", degraded);
82
+ }
83
+ session(file, name) {
84
+ this.#latestSession = { file, name };
85
+ this.#send("bridge.session", { file, name });
86
+ }
87
+ requestDetach(origin = this.captureAttachment(), timeoutMs = 1_000) {
88
+ return this.#requestAction("detach", origin, timeoutMs);
89
+ }
90
+ requestRestart(origin = this.captureAttachment(), timeoutMs = 1_000) {
91
+ return this.#requestAction("restart", origin, timeoutMs);
92
+ }
93
+ close() {
94
+ this.#closed = true;
95
+ if (this.#retry !== null)
96
+ clearTimeout(this.#retry);
97
+ this.#retry = null;
98
+ this.#stopHeartbeat();
99
+ const transport = this.#transport;
100
+ this.#transport = null;
101
+ transport?.destroy();
102
+ this.#fail(false);
103
+ }
104
+ #send(type, body, requestId) {
105
+ if (this.#transport === null || (!this.#connected && type !== "hello"))
106
+ return;
107
+ try {
108
+ this.#transport.write(encodeResidentFrame({ type, ...(requestId === undefined ? {} : { requestId }), body }));
109
+ }
110
+ catch {
111
+ this.#fail();
112
+ }
113
+ }
114
+ #receive(message) {
115
+ if (message.type === "welcome" && message.body["generation"] === 1) {
116
+ const advertised = message.body["features"];
117
+ this.#features = new Set(Array.isArray(advertised) ? advertised.filter((feature) => typeof feature === "string") : []);
118
+ this.#connected = true;
119
+ this.#available = true;
120
+ this.#startWaiter?.(true);
121
+ if (this.#supports("bridge-heartbeat"))
122
+ this.#startHeartbeat();
123
+ if (this.#latestStatus !== null)
124
+ this.status(this.#latestStatus.state, this.#latestStatus.degraded);
125
+ if (this.#latestSession !== null)
126
+ this.#send("bridge.session", this.#latestSession);
127
+ return;
128
+ }
129
+ if (message.type === "incompatible-generation") {
130
+ this.#fail();
131
+ return;
132
+ }
133
+ if (message.type === "attachment") {
134
+ const clientId = message.body["clientId"];
135
+ const revision = message.body["controllerRevision"];
136
+ if (typeof clientId === "string" && typeof revision === "number")
137
+ this.#attachment = { clientId, controllerRevision: revision };
138
+ return;
139
+ }
140
+ if ((message.type === "bridge.result" || message.type === "result") && typeof message.requestId === "number") {
141
+ const resolvePromise = this.#pending.get(message.requestId);
142
+ if (resolvePromise) {
143
+ this.#pending.delete(message.requestId);
144
+ resolvePromise(message.body["accepted"] === true);
145
+ }
146
+ }
147
+ }
148
+ async #requestAction(action, origin, timeoutMs) {
149
+ if (!this.available || origin === null || (action === "restart" && !this.#supports("bridge-restart")))
150
+ return false;
151
+ const requestId = ++this.#request;
152
+ const result = new Promise(resolvePromise => this.#pending.set(requestId, resolvePromise));
153
+ this.#send("bridge.request", { action, clientId: origin.clientId, controllerRevision: origin.controllerRevision }, requestId);
154
+ const timeout = new Promise(resolvePromise => {
155
+ const timer = setTimeout(() => { this.#pending.delete(requestId); resolvePromise(false); }, timeoutMs);
156
+ timer.unref();
157
+ });
158
+ return await Promise.race([result, timeout]);
159
+ }
160
+ #startHeartbeat() {
161
+ if (this.#heartbeat !== null)
162
+ return;
163
+ const schedule = this.#options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
164
+ this.#heartbeat = schedule(() => this.#send("bridge.heartbeat", { seq: ++this.#sequence }), this.#options.heartbeatIntervalMs ?? 5_000);
165
+ this.#heartbeat.unref?.();
166
+ }
167
+ #stopHeartbeat() {
168
+ if (this.#heartbeat === null)
169
+ return;
170
+ (this.#options.clearInterval ?? (timer => clearInterval(timer)))(this.#heartbeat);
171
+ this.#heartbeat = null;
172
+ }
173
+ #supports(feature) {
174
+ return this.#features.has("crash-recovery") || this.#features.has(feature);
175
+ }
176
+ #fail(retry = true) {
177
+ const transport = this.#transport;
178
+ this.#stopHeartbeat();
179
+ this.#available = false;
180
+ this.#connected = false;
181
+ this.#features.clear();
182
+ this.#attachment = null;
183
+ this.#transport = null;
184
+ transport?.destroy();
185
+ this.#startWaiter?.(false);
186
+ this.#startWaiter = null;
187
+ for (const resolvePromise of this.#pending.values())
188
+ resolvePromise(false);
189
+ this.#pending.clear();
190
+ if (retry && !this.#closed && this.#retry === null) {
191
+ this.#retry = setTimeout(() => {
192
+ this.#retry = null;
193
+ void this.start().then(connected => { if (!connected)
194
+ this.#fail(); });
195
+ }, 250);
196
+ this.#retry.unref();
197
+ }
198
+ }
199
+ }
@@ -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,30 @@
1
+ export type TerminalHostFallbackReason = "unsupported platform" | "terminal host not installed" | "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
+ }
28
+ export declare function resolveTerminalHostPath(releaseRoot: string, environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, architecture?: string): string;
29
+ export declare function ensureTerminalHost(options: EnsureTerminalHostOptions): Promise<EnsureTerminalHostResult>;
30
+ export {};
@@ -0,0 +1,121 @@
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
+ export function resolveTerminalHostPath(releaseRoot, environment = process.env, platform = process.platform, architecture = process.arch) {
7
+ const override = environment[PRODUCT_IDENTITY.environment.terminalHostPath]?.trim();
8
+ if (override)
9
+ return resolve(override);
10
+ const executable = platform === "win32" ? "a1-terminal-host.exe" : "a1-terminal-host";
11
+ return resolve(releaseRoot, "dist", "native", `${platform}-${architecture}`, executable);
12
+ }
13
+ export async function ensureTerminalHost(options) {
14
+ const platform = options.platform ?? process.platform;
15
+ const architecture = options.architecture ?? process.arch;
16
+ if (!supported(platform, architecture))
17
+ return { status: "fallback", reason: "unsupported platform" };
18
+ const executable = resolveTerminalHostPath(options.releaseRoot, options.environment, platform, architecture);
19
+ try {
20
+ await access(executable, constants.R_OK | (platform === "win32" ? 0 : constants.X_OK));
21
+ }
22
+ catch {
23
+ return { status: "fallback", reason: "terminal host not installed" };
24
+ }
25
+ const run = options.run ?? runHostCommand;
26
+ const timeoutMs = options.timeoutMs ?? 8_000;
27
+ const common = ["--profile", options.profileRoot, "--data-dir", options.dataDir, "--runtime-dir", options.runtimeDir];
28
+ const environment = { ...options.environment };
29
+ try {
30
+ const probe = await run(executable, ["probe", ...common], environment, Math.min(4_000, timeoutMs));
31
+ const existing = parseResult(probe.stdout);
32
+ if (probe.code === 0) {
33
+ if (existing?.generation === 1 && typeof existing.endpoint === "string") {
34
+ return { status: "ready", executable, endpoint: existing.endpoint };
35
+ }
36
+ return {
37
+ status: "fallback",
38
+ reason: typeof existing?.generation === "number" ? "incompatible generation" : "host probe failed",
39
+ };
40
+ }
41
+ if (probe.code === 4 || existing?.outcome === "incompatible-generation")
42
+ return { status: "fallback", reason: "incompatible generation" };
43
+ if (existing?.outcome !== "not-running" && probe.code !== 3)
44
+ return { status: "fallback", reason: classifiedReason(existing?.reason, "host probe failed") };
45
+ const maximum = Math.max(1, Math.min(8, Math.trunc(options.maxConcurrentStarts ?? 2)));
46
+ const unresponsiveRestartSeconds = Math.max(30, Math.min(600, Math.trunc(options.unresponsiveRestartSeconds ?? 120)));
47
+ const start = await run(executable, [
48
+ "server", "--detach", ...common,
49
+ "--max-concurrent-starts", String(maximum),
50
+ "--unresponsive-restart-seconds", String(unresponsiveRestartSeconds),
51
+ ], environment, timeoutMs);
52
+ const started = parseResult(start.stdout);
53
+ if (start.code !== 0)
54
+ return { status: "fallback", reason: classifiedReason(started?.reason, "launch not verified") };
55
+ if (started?.verified !== true || typeof started.endpoint !== "string")
56
+ return { status: "fallback", reason: "launch not verified" };
57
+ const deadline = Date.now() + timeoutMs;
58
+ while (Date.now() < deadline) {
59
+ const result = await run(executable, ["probe", ...common], environment, Math.min(1_000, Math.max(1, deadline - Date.now())));
60
+ const ready = parseResult(result.stdout);
61
+ if (result.code === 0) {
62
+ if (ready?.generation === 1 && typeof ready.endpoint === "string") {
63
+ return { status: "ready", executable, endpoint: ready.endpoint };
64
+ }
65
+ return {
66
+ status: "fallback",
67
+ reason: typeof ready?.generation === "number" ? "incompatible generation" : "host probe failed",
68
+ };
69
+ }
70
+ if (result.code === 4 || ready?.outcome === "incompatible-generation")
71
+ return { status: "fallback", reason: "incompatible generation" };
72
+ await new Promise(resolvePromise => setTimeout(resolvePromise, 40));
73
+ }
74
+ return { status: "fallback", reason: "host startup timed out" };
75
+ }
76
+ catch (error) {
77
+ return { status: "fallback", reason: isTimeout(error) ? "host startup timed out" : "host probe failed" };
78
+ }
79
+ }
80
+ function supported(platform, architecture) {
81
+ return (platform === "win32" || platform === "linux") && architecture === "x64"
82
+ || platform === "darwin" && architecture === "arm64";
83
+ }
84
+ function parseResult(output) {
85
+ const line = output.trim().split(/\r?\n/).filter(Boolean).at(-1);
86
+ if (!line)
87
+ return null;
88
+ try {
89
+ const parsed = JSON.parse(line);
90
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ function classifiedReason(value, fallback) {
97
+ if (value === "registry-unreadable")
98
+ return "registry unreadable";
99
+ if (value === "blocked")
100
+ return "recovery blocked";
101
+ if (value === "incompatible-generation")
102
+ return "incompatible generation";
103
+ return fallback;
104
+ }
105
+ function isTimeout(error) { return error instanceof Error && "code" in error && error.code === "ETIMEDOUT"; }
106
+ async function runHostCommand(executable, arguments_, environment, timeoutMs) {
107
+ return await new Promise((resolvePromise, rejectPromise) => {
108
+ const child = spawn(executable, [...arguments_], { env: environment, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
109
+ const stdout = [];
110
+ const stderr = [];
111
+ child.stdout.on("data", chunk => stdout.push(Buffer.from(chunk)));
112
+ child.stderr.on("data", chunk => stderr.push(Buffer.from(chunk)));
113
+ child.once("error", rejectPromise);
114
+ child.once("close", code => { clearTimeout(timer); resolvePromise({ code: code ?? 1, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") }); });
115
+ const timer = setTimeout(() => {
116
+ child.kill("SIGKILL");
117
+ rejectPromise(Object.assign(new Error("terminal host command timed out"), { code: "ETIMEDOUT" }));
118
+ }, timeoutMs);
119
+ timer.unref();
120
+ });
121
+ }
@@ -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,6 @@
1
+ export { ensureTerminalHost, resolveTerminalHostPath } from "./ensure-terminal-host.js";
2
+ export type { EnsureTerminalHostOptions, EnsureTerminalHostResult, TerminalHostFallbackReason } from "./ensure-terminal-host.js";
3
+ export { readResidentTabLaunchSettings, readResidentTabsSetting } from "./settings.js";
4
+ export type { ResidentTabLaunchSettings } from "./settings.js";
5
+ export { selectResidentHost } from "./routing.js";
6
+ export type { ResidentHostSelectionOptions } from "./routing.js";
@@ -0,0 +1,3 @@
1
+ export { ensureTerminalHost, resolveTerminalHostPath } from "./ensure-terminal-host.js";
2
+ export { readResidentTabLaunchSettings, readResidentTabsSetting } from "./settings.js";
3
+ export { selectResidentHost } from "./routing.js";
@@ -0,0 +1,65 @@
1
+ import { ResidentTabFaultInjection } from "./fault-injection.js";
2
+ export interface SubmittedPromptRecord {
3
+ readonly type: "submitted";
4
+ readonly id: string;
5
+ readonly sessionId: string;
6
+ readonly sessionFile: string | null;
7
+ readonly incarnation: number;
8
+ readonly fileExisted: boolean;
9
+ readonly kind: "prompt" | "steer" | "follow-up";
10
+ readonly text: string;
11
+ readonly images?: readonly unknown[];
12
+ readonly at: string;
13
+ }
14
+ export interface CorrelatedPromptRecord {
15
+ readonly type: "correlated";
16
+ readonly id: string;
17
+ readonly entryId: string;
18
+ }
19
+ export interface SettledPromptRecord {
20
+ readonly type: "settled";
21
+ readonly id: string;
22
+ readonly entryId: string;
23
+ }
24
+ export interface RetiredPromptRecord {
25
+ readonly type: "retired";
26
+ readonly id: string;
27
+ }
28
+ export type PromptJournalRecord = SubmittedPromptRecord | CorrelatedPromptRecord | SettledPromptRecord | RetiredPromptRecord;
29
+ export interface PromptSubmission {
30
+ readonly sessionId: string;
31
+ readonly sessionFile: string | null;
32
+ readonly incarnation: number;
33
+ readonly fileExisted: boolean;
34
+ readonly kind: SubmittedPromptRecord["kind"];
35
+ readonly text: string;
36
+ readonly images?: readonly unknown[];
37
+ }
38
+ export interface SessionJournalEntry {
39
+ readonly entryId: string;
40
+ readonly text: string;
41
+ readonly settled: boolean;
42
+ }
43
+ export interface PendingPrompt {
44
+ readonly submitted: SubmittedPromptRecord;
45
+ readonly correlatedEntryId?: string;
46
+ }
47
+ export declare class PromptJournal {
48
+ #private;
49
+ readonly path: string;
50
+ constructor(path: string, options?: {
51
+ readonly faults?: ResidentTabFaultInjection;
52
+ readonly maximumBytes?: number;
53
+ });
54
+ initialize(): Promise<void>;
55
+ submit(submission: PromptSubmission): Promise<SubmittedPromptRecord>;
56
+ adopt(record: SubmittedPromptRecord): Promise<boolean>;
57
+ correlate(id: string, entryId: string): Promise<void>;
58
+ settle(id: string, entryId: string): Promise<void>;
59
+ retire(id: string): Promise<void>;
60
+ read(): Promise<readonly PromptJournalRecord[]>;
61
+ pending(sessionEntries?: readonly SessionJournalEntry[]): Promise<readonly PendingPrompt[]>;
62
+ compactIfRetired(): Promise<boolean>;
63
+ }
64
+ export declare function classifyMissingSession(record: SubmittedPromptRecord, sessionExists: boolean): "available" | "first-turn" | "missing-transcript";
65
+ export declare function removeJournalTemporary(path: string): Promise<void>;