@volter/supercode-teams 0.3.150 → 0.3.152

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.
@@ -0,0 +1,350 @@
1
+ // The only reader of the retired layout. It runs before the daemon exposes mail
2
+ // operations, imports idempotently, and records completion only after every input
3
+ // was visited. Failed imports can resume; ordinary reads never consult this tree.
4
+ import { createReadStream } from 'node:fs';
5
+ import { lstat, readFile, readdir } from 'node:fs/promises';
6
+ import { createInterface } from 'node:readline';
7
+ import { basename, join, relative } from 'node:path';
8
+ import { createHash, randomBytes } from 'node:crypto';
9
+
10
+ const alive = (pid) => {
11
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
12
+ try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
13
+ };
14
+ const headerOf = ({ body, ...header }) => header;
15
+ const validId = (id) => typeof id === 'string' && /^[A-Za-z0-9_-]+$/.test(id);
16
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
17
+ const isAddress = (value) => typeof value === 'string' && /^sc:[^:\s]+:[^:\s]+:\S+$/.test(value);
18
+ const validRecordName = (value) => typeof value === 'string' && /^[A-Za-z0-9._-]+$/.test(value);
19
+
20
+ export async function importMaildir(database, root, { log = () => {}, legacyRoot = root } = {}) {
21
+ const began = performance.now();
22
+ const work = { source_reads: 0, source_bytes: 0, committed_batches: 0, max_queued_operations: 0, elapsed_ms: 0 };
23
+ const complete = database.metadata('maildir_import');
24
+ if (complete?.complete === true) return complete;
25
+ let roots;
26
+ try { roots = await readdir(root, { withFileTypes: true }); }
27
+ catch (error) {
28
+ if (error.code !== 'ENOENT') throw error;
29
+ work.elapsed_ms = Math.round(performance.now() - began);
30
+ const result = { complete: true, source: root, at_ms: Date.now(), messages: 0, records: 0, skipped: 0, work };
31
+ database.setMetadata('maildir_import', result);
32
+ return result;
33
+ }
34
+ const result = { complete: false, source: root, at_ms: Date.now(), messages: 0, records: 0, skipped: 0, work };
35
+ let pending = [];
36
+ let flushing = false;
37
+ const flush = () => {
38
+ if (!pending.length) return;
39
+ const writes = pending;
40
+ pending = [];
41
+ flushing = true;
42
+ try {
43
+ database.transaction(() => { for (const write of writes) write(); });
44
+ work.committed_batches++;
45
+ work.max_queued_operations = Math.max(work.max_queued_operations, writes.length);
46
+ }
47
+ finally { flushing = false; }
48
+ };
49
+ // Fingerprints, diagnostics and their imported rows share bounded commits.
50
+ // A diagnostic discovered while applying a batch belongs to that transaction.
51
+ const queue = (write) => {
52
+ if (flushing) { write(); return; }
53
+ pending.push(write);
54
+ if (pending.length >= 256) flush();
55
+ };
56
+ const errors = database.statement('INSERT INTO mail_import_errors(source, mailbox, detail, critical) VALUES (?, ?, ?, ?) ON CONFLICT(source) DO UPDATE SET mailbox = excluded.mailbox, detail = excluded.detail, critical = excluded.critical');
57
+ const failed = (path, error, mailbox = null) => {
58
+ const source = relative(root, path);
59
+ const parts = source.split(/[\\/]/);
60
+ const critical = ['agents', 'threads', 'native-questions', 'native-launches', 'relays', 'machines', 'sent'].includes(parts[0])
61
+ || ['claims', 'subscriptions', 'wake'].includes(parts[1])
62
+ || parts[0] === 'names.json' || parts[0] === 'relay-endpoint.json'
63
+ || (parts[0] === 'terminal-delivery' && parts.at(-1) === 'queue.json');
64
+ queue(() => errors.run(source, mailbox, String(error.message ?? error), critical ? 1 : 0));
65
+ result.skipped++;
66
+ log(`mail import: retained unreadable source ${source}: ${error.message ?? error}`);
67
+ };
68
+ const sources = database.statement('INSERT INTO mail_import_sources(source, sha256) VALUES (?, ?) ON CONFLICT(source) DO UPDATE SET sha256 = excluded.sha256');
69
+ const readSource = async (path, encoding = 'utf8') => {
70
+ if (!(await lstat(path)).isFile()) throw new Error('record is not a regular file');
71
+ const bytes = await readFile(path);
72
+ work.source_reads++; work.source_bytes += bytes.length;
73
+ const source = relative(root, path), sha256 = createHash('sha256').update(bytes).digest('hex');
74
+ queue(() => sources.run(source, sha256));
75
+ return encoding ? new TextDecoder('utf-8', { fatal: true }).decode(bytes) : bytes;
76
+ };
77
+ const json = async (path, mailbox = null, valid = record) => {
78
+ try {
79
+ const value = JSON.parse(await readSource(path));
80
+ if (!valid(value)) throw new Error('record has an unsupported JSON shape');
81
+ queue(() => database.statement('DELETE FROM mail_import_errors WHERE source = ?').run(relative(root, path)));
82
+ return value;
83
+ }
84
+ catch (error) { failed(path, error, mailbox); return null; }
85
+ };
86
+ const entries = async (dir) => {
87
+ try {
88
+ if (!(await lstat(dir)).isDirectory()) throw new Error(`mail import refuses a linked or non-directory record group: ${dir}`);
89
+ return await readdir(dir, { withFileTypes: true });
90
+ }
91
+ catch (error) { if (error.code === 'ENOENT') return []; throw error; }
92
+ };
93
+ const recordDirectory = async (dir, collection, { nested = false, text = false } = {}) => {
94
+ for (const entry of await entries(dir)) {
95
+ const path = join(dir, entry.name);
96
+ if (entry.isDirectory() && nested) {
97
+ for (const child of await entries(path)) {
98
+ if (!child.isFile()) { failed(join(path, child.name), 'not a regular record'); continue; }
99
+ const value = await json(join(path, child.name));
100
+ if (value !== null) queue(() => { database.putRecord(collection, `${entry.name}/${child.name}`, value, { create: true }); result.records++; });
101
+ }
102
+ continue;
103
+ }
104
+ if (!entry.isFile()) { if (!entry.isDirectory()) failed(path, 'not a regular record'); continue; }
105
+ if (/\.tmp(?:\.|$)|\.lock$/.test(entry.name)) continue;
106
+ let value;
107
+ if (text) {
108
+ try {
109
+ value = (await readSource(path)).trim();
110
+ if (!value || !Number.isSafeInteger(Number(value)) || Number(value) < 0) throw new Error('agent cursor is not a nonnegative integer timestamp');
111
+ }
112
+ catch (error) { failed(path, error); continue; }
113
+ } else value = await json(path, null, collection === 'native_questions' && entry.name.endsWith('.delivered')
114
+ ? Number.isSafeInteger : record);
115
+ if (value !== null) queue(() => { database.putRecord(collection, entry.name, value, { create: true }); result.records++; });
116
+ }
117
+ };
118
+
119
+ // Pass 1 imports envelopes before their claims, subscriptions and deliveries,
120
+ // so the database's foreign keys enforce the same message ownership throughout.
121
+ const boxes = [];
122
+ for (const entry of roots) {
123
+ if (!entry.isDirectory()) continue;
124
+ const dir = join(root, entry.name);
125
+ let address;
126
+ try { address = (await readSource(join(dir, 'address'))).trim(); }
127
+ catch (error) { if (error.code !== 'ENOENT') failed(join(dir, 'address'), error); continue; }
128
+ if (!/^sc:[^:\s]+:[^:\s]+:\S+$/.test(address)) { failed(join(dir, 'address'), 'invalid mailbox address'); continue; }
129
+ queue(() => database.statement('INSERT INTO mailboxes(address, folder) VALUES (?, ?) ON CONFLICT(address) DO NOTHING').run(address, entry.name));
130
+ boxes.push({ address, dir });
131
+ for (const part of ['new', 'claimed', 'cur']) {
132
+ for (const file of await entries(join(dir, part))) {
133
+ if (!file.isFile() || !file.name.endsWith('.json')) continue;
134
+ const path = join(dir, part, file.name);
135
+ const envelope = await json(path, address);
136
+ if (!envelope) continue;
137
+ if (!validId(envelope.id) || !Number.isSafeInteger(envelope.created_at_ms) || typeof envelope.body !== 'string') {
138
+ failed(path, 'invalid envelope identity, creation time or body', address); continue;
139
+ }
140
+ const pid = part === 'claimed' ? Number(file.name.split('.')[0]) : null;
141
+ const state = part === 'claimed' && !alive(pid) ? 'new' : part;
142
+ const token = state === 'claimed' ? randomBytes(16).toString('hex') : null;
143
+ const line = Array.from(envelope.body.split('\n', 1)[0]).slice(0, 400).join('');
144
+ queue(() => {
145
+ const existing = database.statement('SELECT header, body FROM messages WHERE mailbox = ? AND id = ?').get(address, envelope.id);
146
+ if (existing) {
147
+ // Retain an inconsistent duplicate for repair; never overwrite a
148
+ // previously imported original or choose from its newer mtime.
149
+ if (existing.header !== JSON.stringify(headerOf(envelope)) || existing.body !== envelope.body) failed(path, 'conflicting duplicate message id', address);
150
+ return;
151
+ }
152
+ database.statement(`INSERT INTO messages(mailbox, id, thread, sender, kind, subject, created_at_ms, state, header, body, first_line, read_at_ms, claim_pid, claim_token)
153
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
154
+ .run(address, envelope.id, envelope.thread ?? envelope.id, envelope.from ?? '', envelope.kind ?? 'peer', envelope.subject ?? null,
155
+ envelope.created_at_ms, state, JSON.stringify(headerOf(envelope)), envelope.body, line, state === 'cur' ? envelope.created_at_ms : null,
156
+ state === 'claimed' ? pid : null, token);
157
+ result.messages++;
158
+ });
159
+ }
160
+ }
161
+ }
162
+ flush();
163
+
164
+ for (const { address, dir } of boxes) {
165
+ const present = (id) => Boolean(database.statement('SELECT 1 FROM messages WHERE mailbox = ? AND id = ?').get(address, id));
166
+ for (const entry of await entries(join(dir, 'claims'))) {
167
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
168
+ const path = join(dir, 'claims', entry.name), id = basename(entry.name, '.json');
169
+ const claim = await json(path, address);
170
+ if (claim && !present(id)) { failed(path, 'claim has no imported message', address); continue; }
171
+ if (claim) queue(() => database.statement('INSERT INTO claims(mailbox, message, record) VALUES (?, ?, ?) ON CONFLICT(mailbox, message) DO NOTHING').run(address, id, JSON.stringify(claim)));
172
+ }
173
+ for (const entry of await entries(join(dir, 'subscriptions'))) {
174
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
175
+ const path = join(dir, 'subscriptions', entry.name), subscription = await json(path, address, (value) => record(value)
176
+ && validId(value.message_id) && isAddress(value.subscriber)
177
+ && (value.created_at_ms == null || Number.isSafeInteger(value.created_at_ms)));
178
+ if (!subscription?.message_id || !present(subscription.message_id)) { if (subscription) failed(path, 'subscription has no imported message', address); continue; }
179
+ queue(() => database.statement('INSERT INTO subscriptions(mailbox, message, record) VALUES (?, ?, ?) ON CONFLICT(mailbox, message) DO NOTHING').run(address, subscription.message_id, JSON.stringify(subscription)));
180
+ }
181
+ for (const entry of await entries(join(dir, 'wake'))) {
182
+ if (!entry.isFile()) continue;
183
+ const path = join(dir, 'wake', entry.name);
184
+ if (!present(entry.name)) { failed(path, 'wake has no imported message', address); continue; }
185
+ let state;
186
+ try {
187
+ const text = await readSource(path);
188
+ state = text.trim() ? JSON.parse(text) : {};
189
+ } catch (error) { failed(path, error, address); continue; }
190
+ if (!record(state) || !Number.isSafeInteger(state.next_at_ms ?? 0) || !Number.isSafeInteger(state.attempts ?? 0)) {
191
+ failed(path, 'invalid wake deadline or attempts', address); continue;
192
+ }
193
+ queue(() => database.statement('INSERT INTO deliveries(mailbox, message, due_at_ms, attempts, record) VALUES (?, ?, ?, ?, ?) ON CONFLICT(mailbox, message) DO NOTHING')
194
+ .run(address, entry.name, state.next_at_ms ?? 0, state.attempts ?? 0, JSON.stringify(state)));
195
+ }
196
+ for (const entry of await entries(join(dir, 'expired'))) {
197
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
198
+ const expiry = await json(join(dir, 'expired', entry.name), address, (value) => record(value) && validId(value.id));
199
+ if (expiry?.id) queue(() => { database.putRecord('expired', `${address}/${expiry.id}`, expiry, { create: true }); result.records++; });
200
+ }
201
+ }
202
+
203
+ const agents = join(root, 'agents');
204
+ for (const entry of await entries(agents)) {
205
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
206
+ const agent = await json(join(agents, entry.name), null, (value) => record(value)
207
+ && validRecordName(value.name) && value.name === basename(entry.name, '.json') && isAddress(value.main_session)
208
+ && (value.folder == null || typeof value.folder === 'string')
209
+ && (value.harness == null || typeof value.harness === 'string')
210
+ && (value.idle_minutes == null || Number.isFinite(value.idle_minutes)));
211
+ if (agent) queue(() => { database.putRecord('agents', basename(entry.name, '.json'), agent, { create: true }); result.records++; });
212
+ }
213
+ for (const name of ['typed', 'resumed']) await recordDirectory(join(agents, name), `agent_${name}`, { text: true });
214
+ try {
215
+ const name = (await readSource(join(agents, 'owners-account-manager'))).trim();
216
+ if (!validRecordName(name)) throw new Error('owner account-manager selector is not an agent name');
217
+ queue(() => database.putRecord('agent_settings', 'owners-account-manager', name, { create: true }));
218
+ }
219
+ catch (error) { if (error.code !== 'ENOENT') failed(join(agents, 'owners-account-manager'), error); }
220
+ for (const entry of await entries(join(root, 'threads'))) {
221
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
222
+ const thread = await json(join(root, 'threads', entry.name), null, (value) => record(value)
223
+ && validRecordName(value.id) && value.id === basename(entry.name, '.json') && validRecordName(value.agent) && isAddress(value.holder)
224
+ && Number.isSafeInteger(value.created_at_ms) && Array.isArray(value.participants)
225
+ && value.participants.every((participant) => record(participant) && isAddress(participant.address) && ['to', 'cc'].includes(participant.role))
226
+ && (value.markers == null || record(value.markers)));
227
+ if (thread) queue(() => { database.putRecord('threads', basename(entry.name, '.json'), thread, { create: true }); result.records++; });
228
+ }
229
+ await recordDirectory(join(root, 'native-questions'), 'native_questions');
230
+ await recordDirectory(join(root, 'native-launches'), 'native_launches');
231
+ await recordDirectory(join(root, 'machines'), 'machine_waits');
232
+ const endpointFile = join(root, 'relay-endpoint.json');
233
+ try { await lstat(endpointFile); const endpoint = await json(endpointFile); if (endpoint) queue(() => database.putRecord('relay_endpoint', 'port', endpoint, { create: true })); }
234
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
235
+ // Relay state is imported as rows. The runtime's settings are application
236
+ // configuration, copied to its new runtime home when that runtime is adopted.
237
+ for (const relay of await entries(join(root, 'relays'))) {
238
+ if (!relay.isDirectory()) continue;
239
+ for (const entry of await entries(join(root, 'relays', relay.name))) {
240
+ if (!entry.isFile() || !/^(?:relay|queue|receipt|sent|settings)\.json$|^turn\.txt$/.test(entry.name)) continue;
241
+ const path = join(root, 'relays', relay.name, entry.name);
242
+ let value;
243
+ if (entry.name === 'turn.txt') {
244
+ try { value = await readSource(path); } catch (error) { failed(path, error); continue; }
245
+ } else value = await json(path);
246
+ const at = (await lstat(path)).mtimeMs;
247
+ if (entry.name === 'relay.json' && value && typeof value === 'object') value.runtime_directory ??= join(legacyRoot, 'relays', relay.name);
248
+ if (value !== null) queue(() => { database.putRecord('relays', `${relay.name}/${entry.name}`, value, { create: true, at: Math.trunc(at) }); result.records++; });
249
+ }
250
+ }
251
+ const namesFile = join(root, 'names.json');
252
+ try {
253
+ const names = JSON.parse(await readSource(namesFile));
254
+ if (!record(names)) throw new Error('names ledger is not a JSON object');
255
+ for (const [name, value] of Object.entries(names)) queue(() => database.putRecord('names', name, value, { create: true }));
256
+ } catch (error) { if (error.code !== 'ENOENT') failed(namesFile, error); }
257
+
258
+ const lines = async (path, accept) => {
259
+ try {
260
+ const stat = await lstat(path);
261
+ if (!stat.isFile()) throw new Error(`mail import requires a regular record: ${path}`);
262
+ const key = `maildir_lines:${relative(root, path)}`;
263
+ const progress = database.metadata(key);
264
+ const stamp = { size: stat.size, mtime: stat.mtimeMs, inode: stat.ino };
265
+ if (progress && Object.keys(stamp).some((field) => progress[field] !== stamp[field])) {
266
+ throw new Error(`mail import source changed during an interrupted import: ${path}`);
267
+ }
268
+ let number = 0;
269
+ const hash = createHash('sha256');
270
+ const utf8 = new TextDecoder('utf-8', { fatal: true });
271
+ work.source_reads++;
272
+ const stream = createReadStream(path);
273
+ stream.on('data', (bytes) => {
274
+ hash.update(bytes);
275
+ work.source_bytes += bytes.length;
276
+ try { utf8.decode(bytes, { stream: true }); }
277
+ catch (error) { stream.destroy(error); }
278
+ });
279
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
280
+ stream.on('error', (error) => reader.close());
281
+ try {
282
+ for await (const line of reader) {
283
+ number++;
284
+ if (number <= (progress?.line ?? 0)) continue;
285
+ const position = number;
286
+ let entry = null;
287
+ if (line.trim()) {
288
+ try {
289
+ entry = JSON.parse(line);
290
+ if (!record(entry)) throw new Error('log entry is not a JSON object');
291
+ } catch (error) { entry = null; failed(path, error); }
292
+ }
293
+ queue(() => {
294
+ if (entry !== null) { try { accept(entry); } catch (error) { failed(path, error); } }
295
+ database.setMetadata(key, { ...stamp, line: position });
296
+ });
297
+ }
298
+ if (stream.errored) throw stream.errored;
299
+ utf8.decode();
300
+ const source = relative(root, path), sha256 = hash.digest('hex');
301
+ queue(() => sources.run(source, sha256));
302
+ flush();
303
+ } finally { reader.close(); stream.destroy(); }
304
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
305
+ };
306
+ for (const folder of await entries(join(root, 'terminal-delivery'))) {
307
+ if (!folder.isDirectory()) continue;
308
+ const dir = join(root, 'terminal-delivery', folder.name);
309
+ for (const entry of await entries(dir)) {
310
+ const path = join(dir, entry.name);
311
+ if (entry.name === 'queue.json') {
312
+ const state = await json(path);
313
+ if (state) queue(() => { database.putRecord('terminal_delivery', `${folder.name}/queue.json`, state, { create: true }); result.records++; });
314
+ } else if (entry.name === 'receipts.jsonl' || entry.name === 'attempts.jsonl') {
315
+ await lines(path, (record) => {
316
+ database.statement('INSERT INTO terminal_log(recipient, kind, record) VALUES (?, ?, ?)').run(folder.name, entry.name === 'receipts.jsonl' ? 'receipt' : 'attempt', JSON.stringify(record));
317
+ database.statement('DELETE FROM terminal_log WHERE sequence <= (SELECT sequence FROM terminal_log ORDER BY sequence DESC LIMIT 1 OFFSET ?)').run(database.retention.maxTerminalLogRows);
318
+ });
319
+ }
320
+ }
321
+ }
322
+ for (const sender of await entries(join(root, 'sent'))) {
323
+ if (!sender.isDirectory()) continue;
324
+ const dir = join(root, 'sent', sender.name);
325
+ await lines(join(dir, 'log.jsonl'), (entry) => {
326
+ if (!validId(entry.id) || typeof entry.to !== 'string' || !Number.isSafeInteger(entry.at_ms)) throw new Error('invalid sent message record');
327
+ database.statement('INSERT INTO sent_messages(sender_key, id, recipient, thread, at_ms, record) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(sender_key, id, recipient) DO NOTHING')
328
+ .run(sender.name, entry.id, entry.to, entry.thread ?? null, entry.at_ms, JSON.stringify(entry));
329
+ });
330
+ for (const marker of await entries(dir)) {
331
+ if (!marker.isFile() || marker.name === 'log.jsonl' || !validId(marker.name)) continue;
332
+ const value = await readSource(join(dir, marker.name));
333
+ queue(() => database.putRecord('sent_receipts', `${sender.name}/${marker.name}`, value, { create: true }));
334
+ }
335
+ }
336
+ await lines(join(root, 'carrier.jsonl'), (entry) => database.appendCarrier(entry));
337
+ flush();
338
+ result.messages = database.statement('SELECT count(*) AS count FROM messages').get().count;
339
+ result.records = database.statement('SELECT count(*) AS count FROM mail_records').get().count;
340
+ result.skipped = database.statement('SELECT count(*) AS count FROM mail_import_errors').get().count;
341
+ work.elapsed_ms = Math.round(performance.now() - began);
342
+ const blocked = database.statement('SELECT source FROM mail_import_errors WHERE critical = 1 ORDER BY source').all();
343
+ if (blocked.length) {
344
+ throw Object.assign(new Error(`mail migration cannot activate with ${blocked.length} unreadable operational records (${blocked.slice(0, 3).map(({ source }) => source).join(', ')}); original files are retained for repair`), { code: 'mail_import_incomplete' });
345
+ }
346
+ result.complete = true;
347
+ database.setMetadata('maildir_import', result);
348
+ log(`mail import complete: ${result.messages} envelopes, ${result.records} operational records, ${result.skipped} retained unreadable sources; ${work.source_reads} source reads, ${work.source_bytes} bytes, ${work.committed_batches} batches in ${work.elapsed_ms} ms`);
349
+ return result;
350
+ }
@@ -1,25 +1,23 @@
1
1
  // Native Codex questions as mail (step 4c; mail_question.rs `poll`, deleted): every POLL_MS the daemon asks each Codex
2
2
  // pane it launched (its native-launches seed: a socket of its own, its owner alive) for the threads it runs and the
3
3
  // questions waiting on them, records each thread's creator (`native-questions/binding-<blake3(address)>.json`) and files
4
- // each question once (`<q-id>.json`) in the session's mailbox, read, and in its creator's, with a wake, so the carrier
5
- // hands it over (or carries it to the creator's machine). A reply is answered by mail/send.mjs; the files are the native
6
- // hook's (`message hook`) as well, so they are kept byte for byte as mail_question.rs writes them. Native request
4
+ // each question once in the session's mailbox, read, and in its creator's, with a wake, so the carrier
5
+ // hands it over (or carries it to the creator's machine). A reply is answered by mail/send.mjs; question and binding
6
+ // records are kept by the same database as the mail. Native request
7
7
  // identities decide which question a reply answers, never screen text or terminal keys.
8
- import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, writeSync } from 'node:fs';
9
- import { join } from 'node:path';
8
+ import { existsSync } from 'node:fs';
10
9
  import { blake3Hex } from './envelope.mjs';
11
10
  import { envelope as makeEnvelope, makeAddress } from './compose.mjs';
12
11
 
13
12
  const POLL_MS = 10_000;
14
13
 
15
- const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
16
14
  const alive = (pid) => { if (!Number.isInteger(pid) || pid <= 0) return false; try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; } };
17
15
 
18
16
  export class NativeQuestions {
19
17
  /** `codexQuestions(request)` the in-process native question client (native-questions.mjs). */
20
18
  constructor({ machine, mailRoot, store, codexQuestions, log = () => {} }) {
21
19
  Object.assign(this, { machine, mailRoot, store, codexQuestions, log });
22
- this.dir = join(mailRoot, 'native-questions');
20
+ this.stopped = false;
23
21
  this.running = false;
24
22
  }
25
23
 
@@ -30,26 +28,23 @@ export class NativeQuestions {
30
28
  return this;
31
29
  }
32
30
 
33
- stop() { clearInterval(this.timer); }
31
+ stop() { this.stopped = true; clearInterval(this.timer); }
34
32
 
35
33
  /** One pass over the launched Codex panes; a pass still running is not started twice. Answers how many panes were
36
34
  * asked and how many questions they hold, or null when a pass was already running. */
37
35
  async poll() {
38
- if (this.running) return null;
36
+ if (this.stopped || this.running) return null;
39
37
  this.running = true;
40
38
  const counted = { panes: 0, questions: 0 };
41
39
  try {
42
- let names = [];
43
- try { names = readdirSync(join(this.mailRoot, 'native-launches')); } catch { return counted; }
44
- for (const name of names) {
40
+ for (const { key: name, value: seed } of this.store.records('native_launches')) {
45
41
  if (!name.endsWith('.json') || name.endsWith('.session.json')) continue;
46
- const seedPath = join(this.mailRoot, 'native-launches', name);
47
- const seed = readJson(seedPath);
48
42
  // only a socket belonging to a launched pane whose owner lives: a retained socket file is not a live endpoint,
49
43
  // and a global shared app-server or a cwd is never guessed at
50
44
  if (seed?.kind !== 'codex' || typeof seed.socketPath !== 'string' || !alive(seed.ownerPid) || !existsSync(seed.socketPath)) continue;
51
45
  let reply;
52
46
  try { reply = await this.codexQuestions({ socketPath: seed.socketPath, discover: true }); } catch { continue; }
47
+ if (this.stopped) return counted;
53
48
  counted.panes++; counted.questions += reply?.questions?.length ?? 0;
54
49
  for (const thread of reply?.threads ?? []) {
55
50
  try { this.bind(seed, { session_id: thread.id, transcript_path: thread.path ?? null }); }
@@ -64,16 +59,9 @@ export class NativeQuestions {
64
59
  } finally { this.running = false; }
65
60
  }
66
61
 
67
- // ---- the files, as mail_question.rs writes them ------------------------------------------------------------------
62
+ // ---- native question records -----------------------------------------------------------------------------------
68
63
 
69
- #bindingPath(address) { return join(this.dir, `binding-${blake3Hex(address)}.json`); }
70
-
71
- /** Written whole and synced, never over an existing file (mail_question.rs `create`). */
72
- create(path, value) {
73
- mkdirSync(this.dir, { recursive: true, mode: 0o700 });
74
- const fd = openSync(path, 'wx', 0o600);
75
- try { writeSync(fd, JSON.stringify(value)); fsyncSync(fd); } finally { closeSync(fd); }
76
- }
64
+ #bindingKey(address) { return `binding-${blake3Hex(address)}.json`; }
77
65
 
78
66
  /** A session's binding: its creator recorded at its first launch, its pane, socket and transcript kept current. */
79
67
  bind(seed, input, harness = 'codex') {
@@ -81,19 +69,18 @@ export class NativeQuestions {
81
69
  if (!session) throw new Error('the thread has no session id');
82
70
  const creator = typeof seed.by === 'string' ? seed.by : makeAddress(this.machine, 'operator', 'user');
83
71
  const binding = { session, creator, pane: seed.pane ?? '', socket_path: seed.socketPath ?? null, transcript_path: input.transcript_path ?? null };
84
- const path = this.#bindingPath(session);
85
- try { this.create(path, binding); return binding; }
86
- catch (error) { if (error.code !== 'EEXIST') throw error; }
87
- const existing = readJson(path);
88
- if (!existing) throw new Error(`its binding ${path} could not be read`);
89
- if (existing.pane !== binding.pane || existing.socket_path !== binding.socket_path || (binding.transcript_path != null && existing.transcript_path !== binding.transcript_path)) {
90
- existing.pane = binding.pane; existing.socket_path = binding.socket_path;
91
- if (binding.transcript_path != null) existing.transcript_path = binding.transcript_path;
92
- const staged = `${path.slice(0, -'.json'.length)}.${process.pid}.tmp`;
93
- this.create(staged, existing);
94
- renameSync(staged, path);
95
- }
96
- return existing;
72
+ const key = this.#bindingKey(session);
73
+ return this.store.transaction(() => {
74
+ if (this.store.putRecord('native_questions', key, binding, { create: true })) return binding;
75
+ const existing = this.store.record('native_questions', key);
76
+ if (!existing?.session || typeof existing.creator !== 'string') throw new Error(`its binding ${key} could not be read`);
77
+ if (existing.pane !== binding.pane || existing.socket_path !== binding.socket_path || (binding.transcript_path != null && existing.transcript_path !== binding.transcript_path)) {
78
+ existing.pane = binding.pane; existing.socket_path = binding.socket_path;
79
+ if (binding.transcript_path != null) existing.transcript_path = binding.transcript_path;
80
+ this.store.putRecord('native_questions', key, existing);
81
+ }
82
+ return existing;
83
+ });
97
84
  }
98
85
 
99
86
  /** A question's id, from its native identity (mail_question.rs `question_id`): each poll and hook finds the same one. */
@@ -111,21 +98,26 @@ export class NativeQuestions {
111
98
  }
112
99
  body += '\nReply with the answer text for one question, or a JSON object mapping each question id to its answer for several questions.';
113
100
  let question = { binding, native, envelope: makeEnvelope({ id, from: binding.session, from_name: binding.session, kind: 'question', reply_via: { mode: 'command' }, body }) };
114
- try { this.create(join(this.dir, `${id}.json`), question); }
115
- catch (error) { if (error.code !== 'EEXIST') throw error; question = readJson(join(this.dir, `${id}.json`)) ?? question; }
116
- this.store.deliverRead(binding.session, question.envelope);
117
- // its creator's copy, filed once with a wake: the carrier hands it over, here or on the creator's machine (whose
118
- // copy kept here is the session's timeline's mirror, marked read once carried)
119
- if (!this.store.findIn(binding.creator, id)) { this.store.deliver(binding.creator, question.envelope); this.store.requestWake(binding.creator, id); }
101
+ this.store.transaction(() => {
102
+ if (!this.store.putRecord('native_questions', `${id}.json`, question, { create: true })) question = this.store.record('native_questions', `${id}.json`);
103
+ if (!question?.envelope) throw new Error(`question ${id} could not be read`);
104
+ if (this.store.record('native_questions', `${id}.delivered`) != null) return;
105
+ this.store.deliverRead(binding.session, question.envelope);
106
+ // Its creator's copy and wake commit with the question, so interruption
107
+ // cannot publish half of a new question.
108
+ if (!this.store.findIn(binding.creator, id)) { this.store.deliver(binding.creator, question.envelope); this.store.requestWake(binding.creator, id); }
109
+ });
120
110
  }
121
111
 
122
112
  /** A reply the native harness used confirmed (mail_question.rs `confirm`): the reply read in the session's mailbox, and
123
113
  * when it was used recorded once (`<q-id>.delivered`, which a listing reads as its delivery). */
124
114
  confirm(id, reply) {
125
- const question = readJson(join(this.dir, `${id}.json`));
115
+ const question = this.store.record('native_questions', `${id}.json`);
126
116
  if (!question) throw new Error(`question ${id} is not on file`);
127
- const found = reply?.envelope?.id ? this.store.findIn(question.binding.session, reply.envelope.id) : null;
128
- if (found && found.state === 'unread') { try { this.store.markRead(question.binding.session, found.path); } catch { /* read meanwhile */ } }
129
- try { this.create(join(this.dir, `${id}.delivered`), Date.now()); } catch (error) { if (error.code !== 'EEXIST') throw error; }
117
+ this.store.transaction(() => {
118
+ const found = reply?.envelope?.id ? this.store.findIn(question.binding.session, reply.envelope.id) : null;
119
+ if (found && found.state === 'unread') this.store.markRead(question.binding.session, found.path);
120
+ this.store.putRecord('native_questions', `${id}.delivered`, Date.now(), { create: true });
121
+ });
130
122
  }
131
123
  }
package/mail/reads.mjs CHANGED
@@ -237,8 +237,8 @@ export class MailReads {
237
237
  const stored = this.#whole(found[0].stored);
238
238
  if (!stored) return { id, found: false };
239
239
  if (!transcripts.has(address)) transcripts.set(address, readTranscript(address, this.homes));
240
- const delivered = deliveries(address, [stored], await transcripts.get(address), (envelope) => questionDeliveredAt(envelope, this.homes.mailRoot));
241
- return { id, found: true, machine: this.machine, message: messageRow(address, stored, delivered, this.homes.mailRoot) };
240
+ const delivered = deliveries(address, [stored], await transcripts.get(address), (envelope) => questionDeliveredAt(envelope, this.store));
241
+ return { id, found: true, machine: this.machine, message: messageRow(address, stored, delivered, this.store) };
242
242
  }));
243
243
  }
244
244
 
@@ -304,9 +304,7 @@ export class MailReads {
304
304
  const found = this.store.find(id);
305
305
  if (found.length !== 1) return { id, found: false };
306
306
  const asked = found[0].stored.envelope;
307
- const namesIt = (reply) => reply === asked.id || (reply.length >= 10 && asked.id.startsWith(reply));
308
- const replies = this.store.inventory(asked.from).stored
309
- .filter((stored) => typeof stored.envelope.in_reply_to === 'string' && namesIt(stored.envelope.in_reply_to))
307
+ const replies = this.store.replies(asked.from, asked.id)
310
308
  .map((stored) => this.#whole(stored)).filter(Boolean)
311
309
  .map(({ envelope }) => ({ id: envelope.id, from: envelope.from, from_name: envelope.from_name, created_at_ms: envelope.created_at_ms, body: envelope.body }));
312
310
  return { id, found: true, message: asked.id, session: found[0].address, asker: asked.from, replies };
@@ -326,17 +324,14 @@ export class MailReads {
326
324
  const transcript = all ? await readTranscript(address, this.homes) : null;
327
325
  let stored;
328
326
  if (all) {
329
- stored = this.store.inventory(address).stored
330
- .filter((item) => item.state === 'read' || (item.envelope.kind === 'user' && item.part === 'claimed'))
331
- .map((item) => this.#whole(item)).filter(Boolean);
332
- if (last !== null) stored = stored.slice(Math.max(0, stored.length - last));
327
+ stored = this.store.history(address, last).map((item) => this.#whole(item)).filter(Boolean);
333
328
  stored.push(...claimed);
334
329
  stored.sort((a, b) => a.envelope.created_at_ms - b.envelope.created_at_ms);
335
330
  } else stored = claimed;
336
- const delivered = deliveries(address, stored, transcript, (envelope) => questionDeliveredAt(envelope, this.homes.mailRoot));
331
+ const delivered = deliveries(address, stored, transcript, (envelope) => questionDeliveredAt(envelope, this.store));
337
332
  const now = Date.now();
338
333
  for (const item of claimed) delivered.set(item.envelope.id, { state: 'by_this_read', at_ms: now });
339
- return { stored, deliveries: delivered, claimed, rendered: stored.map((item) => listed(item.envelope, delivered, this.homes.mailRoot)), rows: stored.map((item) => messageRow(address, item, delivered, this.homes.mailRoot)) };
334
+ return { stored, deliveries: delivered, claimed, rendered: stored.map((item) => listed(item.envelope, delivered, this.store)), rows: stored.map((item) => messageRow(address, item, delivered, this.store)) };
340
335
  }
341
336
 
342
337
  /** The live session a name or address names here (mail_route.rs `resolve`, without the ledger of earlier names). */
@@ -356,25 +351,19 @@ export class MailReads {
356
351
  * `inbox --session` rows (`inbox_rows`), the newest `last` only when given. */
357
352
  async inboxRows(address, last = null) {
358
353
  const transcript = await readTranscript(address, this.homes);
359
- const received = this.store.inventory(address).stored.filter((item) => !(item.envelope.kind === 'question' && item.envelope.from === address)).map((item) => this.#whole(item)).filter(Boolean);
360
- const delivered = deliveries(address, received, transcript, (envelope) => questionDeliveredAt(envelope, this.homes.mailRoot));
361
- const sent = [];
362
- for (const to of this.store.addresses()) {
363
- if (to === address) continue;
364
- for (const item of this.store.inventory(to).stored) if (item.envelope.from === address) { const whole = this.#whole(item); if (whole) sent.push([to, whole]); }
365
- }
366
- let timeline = [...received.map((item) => [address, item, 'in']), ...sent.map(([to, item]) => [to, item, 'out'])];
367
- timeline.sort((a, b) => a[1].envelope.created_at_ms - b[1].envelope.created_at_ms);
368
- if (last !== null) timeline = timeline.slice(Math.max(0, timeline.length - last));
354
+ const timeline = this.store.timeline(address, last)
355
+ .map((row) => [row.address, this.#whole(row.stored), row.direction]).filter(([, item]) => item);
356
+ const received = timeline.filter(([, , direction]) => direction === 'in').map(([, item]) => item);
357
+ const delivered = deliveries(address, received, transcript, (envelope) => questionDeliveredAt(envelope, this.store));
369
358
  // where a message it sent stands is read from its receiver's transcript, the newest receivers' only
370
359
  const receivers = [];
371
360
  for (const [to, , direction] of [...timeline].reverse()) if (direction === 'out' && !receivers.includes(to) && receivers.length < MAX_RECEIVERS_READ) receivers.push(to);
372
361
  for (const to of receivers) {
373
362
  const theirs = timeline.filter(([receiver, , direction]) => direction === 'out' && receiver === to).map(([, item]) => item);
374
- for (const [id, delivery] of deliveries(to, theirs, await readTranscript(to, this.homes), (envelope) => questionDeliveredAt(envelope, this.homes.mailRoot))) delivered.set(id, delivery);
363
+ for (const [id, delivery] of deliveries(to, theirs, await readTranscript(to, this.homes), (envelope) => questionDeliveredAt(envelope, this.store))) delivered.set(id, delivery);
375
364
  }
376
365
  for (const [, item, direction] of timeline) if (direction === 'out' && !delivered.has(item.envelope.id)) delivered.set(item.envelope.id, { state: 'not_read' });
377
- return timeline.map(([box, item, direction]) => ({ ...messageRow(box, item, delivered, this.homes.mailRoot), direction }));
366
+ return timeline.map(([box, item, direction]) => ({ ...messageRow(box, item, delivered, this.store), direction }));
378
367
  }
379
368
 
380
369
  // ---- the machine record's mail ---------------------------------------------------------------------------------
@@ -385,12 +374,12 @@ export class MailReads {
385
374
  const header = (item) => ({ id: item.envelope.id, from: item.envelope.from, from_name: item.envelope.from_name, kind: item.envelope.kind, subject: item.envelope.subject ?? null, thread: item.envelope.thread ?? item.envelope.id, in_reply_to: item.envelope.in_reply_to ?? null, created_at_ms: item.envelope.created_at_ms, unread: item.state === 'unread' });
386
375
  const mail = {};
387
376
  for (const address of mailboxes ?? this.store.addresses()) {
388
- const { stored, skipped } = this.store.inventory(address);
389
- const answer = { unread: stored.filter((item) => item.state === 'unread').map(header), recent: [...stored].reverse().slice(0, recent).map(header), count: stored.length };
377
+ const window = this.store.headerWindow(address, recent);
378
+ const { skipped } = window;
379
+ const answer = { unread: window.unread.map(header), recent: window.recent.map(header), count: window.count };
390
380
  if (skipped.length) answer.partial = `${skipped.length} envelope file${skipped.length === 1 ? '' : 's'} could not be read (${skipped.slice(0, 5).join(', ')}); its unread mail may be more than listed`;
391
381
  mail[address] = answer;
392
382
  }
393
383
  return { mail, unread_boxes: this.store.unread ? [this.store.unread] : [] };
394
384
  }
395
385
  }
396
-