@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,214 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { ResidentTabFaultInjection } from "./fault-injection.js";
5
+ const MAX_JOURNAL_BYTES = 8 * 1024 * 1024;
6
+ const MAX_RECORD_BYTES = 2 * 1024 * 1024;
7
+ export class PromptJournal {
8
+ path;
9
+ #faults;
10
+ #maximumBytes;
11
+ #operation = Promise.resolve();
12
+ #retired = new Set();
13
+ constructor(path, options = {}) {
14
+ this.path = path;
15
+ this.#faults = options.faults ?? new ResidentTabFaultInjection();
16
+ this.#maximumBytes = options.maximumBytes ?? MAX_JOURNAL_BYTES;
17
+ }
18
+ async initialize() {
19
+ await secureDirectory(dirname(this.path));
20
+ const handle = await open(this.path, "a", 0o600);
21
+ await handle.close();
22
+ if (process.platform !== "win32")
23
+ await chmod(this.path, 0o600);
24
+ for (const record of await this.read())
25
+ if (record.type === "retired")
26
+ this.#retired.add(record.id);
27
+ }
28
+ async submit(submission) {
29
+ const record = {
30
+ type: "submitted",
31
+ id: randomUUID(),
32
+ ...submission,
33
+ ...(submission.images === undefined ? {} : { images: submission.images }),
34
+ at: new Date().toISOString(),
35
+ };
36
+ await this.#append(record, "journal-before-append", "journal-after-append", "journal-after-sync");
37
+ return record;
38
+ }
39
+ async adopt(record) {
40
+ if ((await this.read()).some(existing => existing.type === "submitted" && existing.id === record.id))
41
+ return false;
42
+ await this.#append(record, "journal-before-append", "journal-after-append", "journal-after-sync");
43
+ return true;
44
+ }
45
+ async correlate(id, entryId) {
46
+ await this.#append({ type: "correlated", id, entryId }, "journal-before-correlation", "journal-after-correlation");
47
+ }
48
+ async settle(id, entryId) {
49
+ await this.#append({ type: "settled", id, entryId }, "journal-before-settled-record", "journal-after-settled-record");
50
+ }
51
+ async retire(id) {
52
+ if (this.#retired.has(id))
53
+ return;
54
+ await this.#append({ type: "retired", id }, "journal-during-retirement");
55
+ this.#retired.add(id);
56
+ }
57
+ async read() {
58
+ let text;
59
+ try {
60
+ text = await readFile(this.path, "utf8");
61
+ }
62
+ catch (error) {
63
+ if (error.code === "ENOENT")
64
+ return [];
65
+ throw error;
66
+ }
67
+ const records = [];
68
+ const lines = text.split("\n");
69
+ for (let index = 0; index < lines.length; index += 1) {
70
+ const line = lines[index];
71
+ if (!line)
72
+ continue;
73
+ try {
74
+ const value = JSON.parse(line);
75
+ records.push(decodeRecord(value));
76
+ }
77
+ catch (error) {
78
+ if (index === lines.length - 1 && !text.endsWith("\n"))
79
+ break;
80
+ throw error;
81
+ }
82
+ }
83
+ return records;
84
+ }
85
+ async pending(sessionEntries = []) {
86
+ const states = new Map();
87
+ const settled = new Set();
88
+ const retired = new Set();
89
+ for (const record of await this.read()) {
90
+ if (record.type === "submitted")
91
+ states.set(record.id, { submitted: record });
92
+ else if (record.type === "correlated") {
93
+ const state = states.get(record.id);
94
+ if (state !== undefined)
95
+ states.set(record.id, { ...state, correlatedEntryId: record.entryId });
96
+ }
97
+ else if (record.type === "settled")
98
+ settled.add(record.id);
99
+ else
100
+ retired.add(record.id);
101
+ }
102
+ const byId = new Map(sessionEntries.map(entry => [entry.entryId, entry]));
103
+ const pending = [];
104
+ const claimedEntries = new Set();
105
+ for (const [id, state] of states) {
106
+ if (retired.has(id) || settled.has(id)) {
107
+ await this.retire(id);
108
+ continue;
109
+ }
110
+ const correlated = state.correlatedEntryId === undefined ? undefined : byId.get(state.correlatedEntryId);
111
+ const textMatch = correlated ?? sessionEntries.find(entry => entry.text === state.submitted.text && !claimedEntries.has(entry.entryId));
112
+ if (textMatch !== undefined)
113
+ claimedEntries.add(textMatch.entryId);
114
+ if (textMatch?.settled === true)
115
+ await this.retire(id);
116
+ else
117
+ pending.push(state);
118
+ }
119
+ return pending;
120
+ }
121
+ async compactIfRetired() {
122
+ if ((await this.pending()).length !== 0)
123
+ return false;
124
+ await this.#serialize(async () => {
125
+ this.#faults.at("journal-during-compaction");
126
+ const temporary = `${this.path}.tmp-${process.pid}-${randomUUID()}`;
127
+ const handle = await open(temporary, "wx", 0o600);
128
+ try {
129
+ await handle.sync();
130
+ }
131
+ finally {
132
+ await handle.close();
133
+ }
134
+ await rename(temporary, this.path);
135
+ await syncDirectory(dirname(this.path));
136
+ });
137
+ return true;
138
+ }
139
+ async #append(record, ...points) {
140
+ const line = `${JSON.stringify(record)}\n`;
141
+ const bytes = Buffer.byteLength(line);
142
+ if (bytes > MAX_RECORD_BYTES)
143
+ throw new Error("resident prompt journal record exceeds its size limit");
144
+ await this.#serialize(async () => {
145
+ for (const point of points.slice(0, 1))
146
+ this.#faults.at(point);
147
+ const current = await stat(this.path).then(value => value.size, error => error.code === "ENOENT" ? 0 : Promise.reject(error));
148
+ if (current + bytes > this.#maximumBytes)
149
+ throw new Error("resident prompt journal is full");
150
+ const handle = await open(this.path, "a", 0o600);
151
+ try {
152
+ await handle.write(line, undefined, "utf8");
153
+ if (points[1] !== undefined)
154
+ this.#faults.at(points[1]);
155
+ await handle.sync();
156
+ if (points[2] !== undefined)
157
+ this.#faults.at(points[2]);
158
+ }
159
+ finally {
160
+ await handle.close();
161
+ }
162
+ });
163
+ }
164
+ async #serialize(operation) {
165
+ const next = this.#operation.then(operation, operation);
166
+ this.#operation = next.catch(() => undefined);
167
+ await next;
168
+ }
169
+ }
170
+ export function classifyMissingSession(record, sessionExists) {
171
+ if (sessionExists)
172
+ return "available";
173
+ return record.fileExisted ? "missing-transcript" : "first-turn";
174
+ }
175
+ function decodeRecord(value) {
176
+ if (typeof value !== "object" || value === null)
177
+ throw new Error("invalid resident prompt journal record");
178
+ const record = value;
179
+ if (typeof record.id !== "string" || record.id.length === 0)
180
+ throw new Error("invalid resident prompt journal id");
181
+ if (record.type === "submitted") {
182
+ if (typeof record.sessionId !== "string" || !(typeof record.sessionFile === "string" || record.sessionFile === null)
183
+ || typeof record.incarnation !== "number" || typeof record.fileExisted !== "boolean"
184
+ || !["prompt", "steer", "follow-up"].includes(String(record.kind)) || typeof record.text !== "string" || typeof record.at !== "string") {
185
+ throw new Error("invalid submitted prompt journal record");
186
+ }
187
+ return record;
188
+ }
189
+ if ((record.type === "correlated" || record.type === "settled") && typeof record.entryId === "string")
190
+ return record;
191
+ if (record.type === "retired")
192
+ return record;
193
+ throw new Error("invalid resident prompt journal record type");
194
+ }
195
+ async function secureDirectory(path) {
196
+ await mkdir(path, { recursive: true, mode: 0o700 });
197
+ if (process.platform !== "win32")
198
+ await chmod(path, 0o700);
199
+ }
200
+ async function syncDirectory(path) {
201
+ if (process.platform === "win32")
202
+ return;
203
+ const handle = await open(path, "r");
204
+ try {
205
+ await handle.sync();
206
+ }
207
+ finally {
208
+ await handle.close();
209
+ }
210
+ }
211
+ export async function removeJournalTemporary(path) {
212
+ await unlink(path).catch(error => { if (error.code !== "ENOENT")
213
+ throw error; });
214
+ }
@@ -0,0 +1,12 @@
1
+ export interface ResidentEnvelope<T = Readonly<Record<string, unknown>>> {
2
+ readonly type: string;
3
+ readonly requestId?: number;
4
+ readonly body: T;
5
+ }
6
+ /** Generation-1 MessagePack named-map codec. It intentionally supports only bridge-safe values. */
7
+ export declare function encodeResidentFrame(value: ResidentEnvelope): Buffer;
8
+ export declare function decodeResidentPayload(payload: Uint8Array): ResidentEnvelope;
9
+ export declare class ResidentFrameDecoder {
10
+ #private;
11
+ push(chunk: Uint8Array): readonly ResidentEnvelope[];
12
+ }
@@ -0,0 +1,195 @@
1
+ const MAX_FRAME_BYTES = 2 * 1024 * 1024;
2
+ const MAX_DEPTH = 64;
3
+ /** Generation-1 MessagePack named-map codec. It intentionally supports only bridge-safe values. */
4
+ export function encodeResidentFrame(value) {
5
+ const payload = Buffer.from(encodeValue(value, 0));
6
+ if (payload.length > MAX_FRAME_BYTES)
7
+ throw new Error("resident frame exceeds 2 MiB");
8
+ const frame = Buffer.allocUnsafe(payload.length + 4);
9
+ frame.writeUInt32LE(payload.length, 0);
10
+ payload.copy(frame, 4);
11
+ return frame;
12
+ }
13
+ export function decodeResidentPayload(payload) {
14
+ if (payload.byteLength > MAX_FRAME_BYTES)
15
+ throw new Error("resident frame exceeds 2 MiB");
16
+ const state = { bytes: payload, offset: 0 };
17
+ const value = decodeValue(state, 0);
18
+ if (state.offset !== payload.byteLength || !isRecord(value) || typeof value.type !== "string" || !isRecord(value.body)) {
19
+ throw new Error("invalid resident envelope");
20
+ }
21
+ return value;
22
+ }
23
+ export class ResidentFrameDecoder {
24
+ #buffer = Buffer.alloc(0);
25
+ push(chunk) {
26
+ this.#buffer = Buffer.concat([this.#buffer, chunk]);
27
+ const messages = [];
28
+ while (this.#buffer.length >= 4) {
29
+ const length = this.#buffer.readUInt32LE(0);
30
+ if (length > MAX_FRAME_BYTES)
31
+ throw new Error("resident frame exceeds 2 MiB");
32
+ if (this.#buffer.length < length + 4)
33
+ break;
34
+ messages.push(decodeResidentPayload(this.#buffer.subarray(4, length + 4)));
35
+ this.#buffer = this.#buffer.subarray(length + 4);
36
+ }
37
+ return messages;
38
+ }
39
+ }
40
+ function encodeValue(value, depth) {
41
+ if (depth > MAX_DEPTH)
42
+ throw new Error("resident value exceeds nesting limit");
43
+ if (value === null)
44
+ return [0xc0];
45
+ if (value === false)
46
+ return [0xc2];
47
+ if (value === true)
48
+ return [0xc3];
49
+ if (typeof value === "number" && Number.isSafeInteger(value))
50
+ return encodeInteger(value);
51
+ if (typeof value === "string") {
52
+ const bytes = [...Buffer.from(value, "utf8")];
53
+ if (bytes.length < 32)
54
+ return [0xa0 | bytes.length, ...bytes];
55
+ if (bytes.length <= 0xff)
56
+ return [0xd9, bytes.length, ...bytes];
57
+ if (bytes.length <= 0xffff)
58
+ return [0xda, bytes.length >>> 8, bytes.length & 0xff, ...bytes];
59
+ return [0xdb, ...u32(bytes.length), ...bytes];
60
+ }
61
+ if (value instanceof Uint8Array) {
62
+ const bytes = [...value];
63
+ if (bytes.length <= 0xff)
64
+ return [0xc4, bytes.length, ...bytes];
65
+ if (bytes.length <= 0xffff)
66
+ return [0xc5, bytes.length >>> 8, bytes.length & 0xff, ...bytes];
67
+ return [0xc6, ...u32(bytes.length), ...bytes];
68
+ }
69
+ if (Array.isArray(value)) {
70
+ const head = value.length < 16 ? [0x90 | value.length] : value.length <= 0xffff ? [0xdc, value.length >>> 8, value.length & 0xff] : [0xdd, ...u32(value.length)];
71
+ return [...head, ...value.flatMap(entry => encodeValue(entry, depth + 1))];
72
+ }
73
+ if (isRecord(value)) {
74
+ const entries = Object.entries(value).filter(([, child]) => child !== undefined);
75
+ const head = entries.length < 16 ? [0x80 | entries.length] : entries.length <= 0xffff ? [0xde, entries.length >>> 8, entries.length & 0xff] : [0xdf, ...u32(entries.length)];
76
+ return [...head, ...entries.flatMap(([key, child]) => [...encodeValue(key, depth + 1), ...encodeValue(child, depth + 1)])];
77
+ }
78
+ throw new Error(`unsupported resident value: ${typeof value}`);
79
+ }
80
+ function encodeInteger(value) {
81
+ if (value >= 0 && value <= 0x7f)
82
+ return [value];
83
+ if (value < 0 && value >= -32)
84
+ return [0x100 + value];
85
+ if (value >= 0 && value <= 0xff)
86
+ return [0xcc, value];
87
+ if (value >= 0 && value <= 0xffff)
88
+ return [0xcd, value >>> 8, value & 0xff];
89
+ if (value >= -0x80 && value < 0)
90
+ return [0xd0, value & 0xff];
91
+ if (value >= -0x8000 && value < 0)
92
+ return [0xd1, (value >>> 8) & 0xff, value & 0xff];
93
+ if (value >= 0 && value <= 0xffffffff)
94
+ return [0xce, ...u32(value)];
95
+ return [0xd3, ...i64(value)];
96
+ }
97
+ function decodeValue(state, depth) {
98
+ if (depth > MAX_DEPTH)
99
+ throw new Error("resident value exceeds nesting limit");
100
+ const marker = byte(state);
101
+ if (marker <= 0x7f)
102
+ return marker;
103
+ if (marker >= 0xe0)
104
+ return marker - 0x100;
105
+ if ((marker & 0xe0) === 0xa0)
106
+ return string(state, marker & 0x1f);
107
+ if ((marker & 0xf0) === 0x90)
108
+ return array(state, marker & 0x0f, depth);
109
+ if ((marker & 0xf0) === 0x80)
110
+ return map(state, marker & 0x0f, depth);
111
+ switch (marker) {
112
+ case 0xc0: return null;
113
+ case 0xc2: return false;
114
+ case 0xc3: return true;
115
+ case 0xc4: return binary(state, uint(state, 1));
116
+ case 0xc5: return binary(state, uint(state, 2));
117
+ case 0xc6: return binary(state, uint(state, 4));
118
+ case 0xcc: return uint(state, 1);
119
+ case 0xcd: return uint(state, 2);
120
+ case 0xce: return uint(state, 4);
121
+ case 0xcf: return safeInteger(state, false);
122
+ case 0xd0: return signed(state, 1);
123
+ case 0xd1: return signed(state, 2);
124
+ case 0xd2: return signed(state, 4);
125
+ case 0xd3: return safeInteger(state, true);
126
+ case 0xd9: return string(state, uint(state, 1));
127
+ case 0xda: return string(state, uint(state, 2));
128
+ case 0xdb: return string(state, uint(state, 4));
129
+ case 0xdc: return array(state, uint(state, 2), depth);
130
+ case 0xdd: return array(state, uint(state, 4), depth);
131
+ case 0xde: return map(state, uint(state, 2), depth);
132
+ case 0xdf: return map(state, uint(state, 4), depth);
133
+ default: throw new Error(`unsupported resident MessagePack marker 0x${marker.toString(16)}`);
134
+ }
135
+ }
136
+ function byte(state) {
137
+ const value = state.bytes[state.offset++];
138
+ if (value === undefined)
139
+ throw new Error("incomplete resident value");
140
+ return value;
141
+ }
142
+ function uint(state, width) {
143
+ let value = 0;
144
+ for (let index = 0; index < width; index++)
145
+ value = value * 256 + byte(state);
146
+ return value;
147
+ }
148
+ function signed(state, width) {
149
+ const value = uint(state, width);
150
+ return value >= 2 ** (width * 8 - 1) ? value - 2 ** (width * 8) : value;
151
+ }
152
+ function safeInteger(state, signedValue) {
153
+ const high = uint(state, 4);
154
+ const low = uint(state, 4);
155
+ const value = high * 0x1_0000_0000 + low;
156
+ const resolved = signedValue && high >= 0x80000000 ? value - 0x1_0000_0000_0000_0000 : value;
157
+ if (!Number.isSafeInteger(resolved))
158
+ throw new Error("resident integer exceeds JavaScript safe range");
159
+ return resolved;
160
+ }
161
+ function string(state, length) {
162
+ return Buffer.from(binary(state, length)).toString("utf8");
163
+ }
164
+ function binary(state, length) {
165
+ const end = state.offset + length;
166
+ if (end > state.bytes.byteLength)
167
+ throw new Error("incomplete resident value");
168
+ const value = state.bytes.slice(state.offset, end);
169
+ state.offset = end;
170
+ return value;
171
+ }
172
+ function array(state, length, depth) {
173
+ if (length > 65_536)
174
+ throw new Error("resident collection exceeds limit");
175
+ return Array.from({ length }, () => decodeValue(state, depth + 1));
176
+ }
177
+ function map(state, length, depth) {
178
+ if (length > 65_536)
179
+ throw new Error("resident collection exceeds limit");
180
+ const value = {};
181
+ for (let index = 0; index < length; index++) {
182
+ const key = decodeValue(state, depth + 1);
183
+ if (typeof key !== "string" || Object.hasOwn(value, key))
184
+ throw new Error("invalid resident map key");
185
+ value[key] = decodeValue(state, depth + 1);
186
+ }
187
+ return value;
188
+ }
189
+ function u32(value) { return [(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff]; }
190
+ function i64(value) {
191
+ const integer = BigInt(value);
192
+ const encoded = integer < 0 ? (1n << 64n) + integer : integer;
193
+ return Array.from({ length: 8 }, (_, index) => Number((encoded >> BigInt((7 - index) * 8)) & 0xffn));
194
+ }
195
+ function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
@@ -0,0 +1,26 @@
1
+ import { ResidentTabFaultInjection } from "./fault-injection.js";
2
+ import { type PromptSubmission, type SessionJournalEntry, type SubmittedPromptRecord } from "./prompt-journal.js";
3
+ export interface ResidentTabRecoveredInput {
4
+ readonly prompts: readonly SubmittedPromptRecord[];
5
+ readonly draft: string | null;
6
+ }
7
+ export interface ResidentTabAdmission extends Omit<PromptSubmission, "fileExisted" | "incarnation"> {
8
+ readonly incarnation: number;
9
+ }
10
+ export declare class ResidentTabRecovery {
11
+ #private;
12
+ constructor(dataDir: string, options?: {
13
+ readonly faults?: ResidentTabFaultInjection;
14
+ readonly onDegraded?: (degraded: boolean) => void;
15
+ });
16
+ get recovered(): ResidentTabRecoveredInput;
17
+ initialize(currentSessionFile?: string | null, currentSessionId?: string | null): Promise<ResidentTabRecoveredInput>;
18
+ admit(input: ResidentTabAdmission, timeoutMs?: number): Promise<SubmittedPromptRecord>;
19
+ dispatched(): void;
20
+ correlated(submissionId: string, entryId: string): Promise<void>;
21
+ checkpoint(text: string): void;
22
+ settled(sessionFile: string | null): Promise<void>;
23
+ gracefulStop(sessionFile: string | null): Promise<void>;
24
+ }
25
+ /** Reconcile only durable user/assistant ancestry; a torn final JSONL record is not authority. */
26
+ export declare function readSessionJournal(path: string): Promise<readonly SessionJournalEntry[]>;
@@ -0,0 +1,205 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { DraftCheckpointer } from "./draft-checkpoint.js";
4
+ import { ResidentTabFaultInjection } from "./fault-injection.js";
5
+ import { PromptJournal } from "./prompt-journal.js";
6
+ import { SettledTurnSynchronizer, synchronizeSessionFile } from "./session-sync.js";
7
+ export class ResidentTabRecovery {
8
+ #journal;
9
+ #draft;
10
+ #synchronizer;
11
+ #faults;
12
+ #onDegraded;
13
+ #active = [];
14
+ #recovered = { prompts: [], draft: null };
15
+ constructor(dataDir, options = {}) {
16
+ const faults = options.faults ?? new ResidentTabFaultInjection();
17
+ this.#faults = faults;
18
+ this.#onDegraded = options.onDegraded ?? (() => { });
19
+ this.#journal = new PromptJournal(join(dataDir, "journal.jsonl"), { faults });
20
+ this.#draft = new DraftCheckpointer(join(dataDir, "draft.json"), { faults, onDegraded: this.#onDegraded });
21
+ this.#synchronizer = new SettledTurnSynchronizer(this.#journal, faults);
22
+ }
23
+ get recovered() { return this.#recovered; }
24
+ async initialize(currentSessionFile = null, currentSessionId = null) {
25
+ await this.#journal.initialize();
26
+ await adoptHistoricalJournals(dirname(dirname(this.#journal.path)), this.#journal, currentSessionFile, currentSessionId);
27
+ const records = await this.#journal.read();
28
+ const sessionFiles = new Set(records.flatMap(record => record.type === "submitted" && record.sessionFile !== null ? [record.sessionFile] : []));
29
+ if (currentSessionFile !== null)
30
+ sessionFiles.add(currentSessionFile);
31
+ const sessionEntries = (await Promise.all([...sessionFiles].map(readSessionJournal))).flat();
32
+ const [pending, draft] = await Promise.all([
33
+ this.#journal.pending(sessionEntries),
34
+ this.#draft.restore(),
35
+ ]);
36
+ this.#recovered = { prompts: pending.map(value => value.submitted), draft: draft?.text ?? null };
37
+ return this.#recovered;
38
+ }
39
+ async admit(input, timeoutMs = 2_000) {
40
+ const fileExisted = input.sessionFile === null ? false : await stat(input.sessionFile).then(() => true, error => {
41
+ if (error.code === "ENOENT")
42
+ return false;
43
+ throw error;
44
+ });
45
+ let timer;
46
+ try {
47
+ return await Promise.race([
48
+ this.#journal.submit({ ...input, fileExisted }),
49
+ new Promise((_, reject) => {
50
+ timer = setTimeout(() => reject(new Error("resident prompt journal admission timed out")), timeoutMs);
51
+ timer.unref();
52
+ }),
53
+ ]);
54
+ }
55
+ finally {
56
+ if (timer !== undefined)
57
+ clearTimeout(timer);
58
+ }
59
+ }
60
+ dispatched() { this.#faults.at("journal-after-dispatch"); }
61
+ async correlated(submissionId, entryId) {
62
+ await this.#journal.correlate(submissionId, entryId);
63
+ this.#active.push({ id: submissionId, entryId });
64
+ }
65
+ checkpoint(text) { this.#draft.change(text); }
66
+ async settled(sessionFile) {
67
+ const active = this.#active.splice(0);
68
+ if (active.length === 0 || sessionFile === null)
69
+ return;
70
+ await this.#synchronizer.settleMany(sessionFile, active);
71
+ for (const entry of active)
72
+ await this.#journal.retire(entry.id);
73
+ await this.#journal.compactIfRetired();
74
+ }
75
+ async gracefulStop(sessionFile) {
76
+ await this.#draft.flush();
77
+ if (sessionFile !== null)
78
+ await synchronizeSessionFile(sessionFile);
79
+ await this.#synchronizer.flush();
80
+ }
81
+ }
82
+ const MAX_SESSION_RECONCILE_BYTES = 64 * 1024 * 1024;
83
+ const MAX_ADOPTION_AGE_MS = 7 * 24 * 60 * 60 * 1_000;
84
+ async function adoptHistoricalJournals(profileTabsRoot, target, currentSessionFile, currentSessionId) {
85
+ if (currentSessionFile === null && currentSessionId === null)
86
+ return;
87
+ const history = join(profileTabsRoot, "registry-history");
88
+ const names = await readdir(history).catch(() => []);
89
+ for (const name of names.filter(value => /^tabset-.+\.json$/.test(value)).sort().reverse()) {
90
+ const path = join(history, name);
91
+ const metadata = await stat(path).catch(() => null);
92
+ if (metadata === null || Date.now() - metadata.mtimeMs > MAX_ADOPTION_AGE_MS)
93
+ continue;
94
+ let document;
95
+ try {
96
+ document = JSON.parse(await readFile(path, "utf8"));
97
+ }
98
+ catch {
99
+ continue;
100
+ }
101
+ const tabs = typeof document === "object" && document !== null && Array.isArray(document.tabs)
102
+ ? document.tabs : [];
103
+ for (const value of tabs) {
104
+ if (typeof value !== "object" || value === null)
105
+ continue;
106
+ const tab = value;
107
+ if (typeof tab.tabId !== "string")
108
+ continue;
109
+ const fileMatches = currentSessionFile !== null && typeof tab.sessionFile === "string"
110
+ && resolve(tab.sessionFile) === resolve(currentSessionFile);
111
+ const sourcePath = join(profileTabsRoot, tab.tabId, "journal.jsonl");
112
+ if (resolve(sourcePath) === resolve(target.path))
113
+ continue;
114
+ if (!await stat(sourcePath).then(() => true, () => false))
115
+ continue;
116
+ const source = new PromptJournal(sourcePath);
117
+ await source.initialize();
118
+ const records = await source.read();
119
+ const submitted = records.filter((record) => record.type === "submitted");
120
+ const idMatches = currentSessionId !== null && submitted.some(record => record.sessionId === currentSessionId);
121
+ if (!fileMatches && !idMatches)
122
+ continue;
123
+ const pending = await source.pending(currentSessionFile === null ? [] : await readSessionJournal(currentSessionFile));
124
+ for (const entry of pending) {
125
+ await target.adopt(entry.submitted);
126
+ await source.retire(entry.submitted.id);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ /** Reconcile only durable user/assistant ancestry; a torn final JSONL record is not authority. */
132
+ export async function readSessionJournal(path) {
133
+ let text;
134
+ try {
135
+ const metadata = await stat(path);
136
+ if (metadata.size > MAX_SESSION_RECONCILE_BYTES)
137
+ throw new Error("resident session exceeds the recovery scan limit");
138
+ text = await readFile(path, "utf8");
139
+ }
140
+ catch (error) {
141
+ if (error.code === "ENOENT")
142
+ return [];
143
+ throw error;
144
+ }
145
+ const raw = [];
146
+ const lines = text.split("\n");
147
+ for (let index = 0; index < lines.length; index += 1) {
148
+ const line = lines[index];
149
+ if (line.length === 0)
150
+ continue;
151
+ let value;
152
+ try {
153
+ value = JSON.parse(line);
154
+ }
155
+ catch (error) {
156
+ if (index === lines.length - 1 && !text.endsWith("\n"))
157
+ break;
158
+ throw error;
159
+ }
160
+ if (typeof value !== "object" || value === null)
161
+ continue;
162
+ const record = value;
163
+ const message = typeof record.message === "object" && record.message !== null
164
+ ? record.message
165
+ : null;
166
+ if (record.type !== "message" || typeof record.id !== "string" || message === null
167
+ || typeof message.role !== "string")
168
+ continue;
169
+ const content = message.content;
170
+ const promptText = typeof content === "string" ? content : Array.isArray(content)
171
+ ? content.flatMap(part => typeof part === "object" && part !== null
172
+ && part.type === "text"
173
+ && typeof part.text === "string"
174
+ ? [part.text] : []).join("")
175
+ : "";
176
+ raw.push({
177
+ id: record.id,
178
+ parentId: typeof record.parentId === "string" ? record.parentId : null,
179
+ role: message.role,
180
+ text: promptText,
181
+ });
182
+ }
183
+ const byId = new Map(raw.map(record => [record.id, record]));
184
+ const settled = new Set();
185
+ for (const record of raw) {
186
+ if (record.role !== "assistant")
187
+ continue;
188
+ let parent = record.parentId;
189
+ const visited = new Set();
190
+ while (parent !== null && !visited.has(parent)) {
191
+ visited.add(parent);
192
+ const ancestor = byId.get(parent);
193
+ if (ancestor === undefined)
194
+ break;
195
+ if (ancestor.role === "user") {
196
+ settled.add(ancestor.id);
197
+ break;
198
+ }
199
+ parent = ancestor.parentId;
200
+ }
201
+ }
202
+ return raw
203
+ .filter(record => record.role === "user")
204
+ .map(record => ({ entryId: record.id, text: record.text, settled: settled.has(record.id) }));
205
+ }
@@ -0,0 +1,10 @@
1
+ import type { EnsureTerminalHostResult } from "./ensure-terminal-host.js";
2
+ export interface ResidentHostSelectionOptions {
3
+ readonly profileId: "a1" | "pi";
4
+ readonly hasSessionSelection: boolean;
5
+ readonly currentLaunchContract: boolean;
6
+ readonly readSetting: () => Promise<boolean>;
7
+ readonly ensureHost: () => Promise<EnsureTerminalHostResult>;
8
+ }
9
+ /** Invariant: every non-bare or default-off launch stays outside resident discovery and startup. */
10
+ export declare function selectResidentHost(options: ResidentHostSelectionOptions): Promise<EnsureTerminalHostResult | null>;
@@ -0,0 +1,8 @@
1
+ /** Invariant: every non-bare or default-off launch stays outside resident discovery and startup. */
2
+ export async function selectResidentHost(options) {
3
+ if (options.profileId !== "a1" || options.hasSessionSelection || !options.currentLaunchContract)
4
+ return null;
5
+ if (!await options.readSetting())
6
+ return null;
7
+ return await options.ensureHost();
8
+ }