@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.
@@ -2,12 +2,14 @@
2
2
  // Supercode hosts neither the agent nor its run loop; it can attach to native
3
3
  // question requests while the stock TUI remains the session's UI.
4
4
  import { spawn } from 'node:child_process';
5
- import { readFileSync, writeFileSync, renameSync, mkdtempSync, existsSync, rmSync, appendFileSync, mkdirSync } from 'node:fs';
5
+ import { readFileSync, mkdtempSync, existsSync, rmSync, appendFileSync, mkdirSync } from 'node:fs';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { basename, dirname, join } from 'node:path';
8
+ import { nativeLaunchBinding } from './native-launch-binding.mjs';
8
9
 
9
10
  const path = process.argv[2];
10
- const seed = JSON.parse(readFileSync(path, 'utf8'));
11
+ const home = process.argv[3];
12
+ const { seed } = await nativeLaunchBinding(path, { home });
11
13
  // A local socket's path holds at most 104 bytes (macOS; 108 on Linux): under a long TMPDIR (a scratch home's) the
12
14
  // endpoint goes under /tmp, private to this user as mkdtemp makes it, or neither side could open it.
13
15
  const base = process.platform !== 'win32' && Buffer.byteLength(join(tmpdir(), 'sc-codex-XXXXXX', 'native.sock')) > 103 ? '/tmp' : tmpdir();
@@ -51,7 +53,7 @@ if(writableRoots.length)config.push('-c',`sandbox_workspace_write.writable_roots
51
53
  // maintainer off for three hours). It is kept in a folder beside the bindings' (never among them: the carrier reads
52
54
  // every file there on each poll, mail_question.rs `poll`), at most LOG_BYTES of it.
53
55
  const LOG_BYTES = 8 * 1024 * 1024;
54
- const serverLog = join(dirname(path), '..', 'native-launch-logs', `${basename(path).replace(/\.json$/, '')}.app-server.log`);
56
+ const serverLog = join(dirname(dirname(dirname(path))), 'native-launch-logs', `${basename(path).replace(/\.json$/, '')}.app-server.log`);
55
57
  try { mkdirSync(dirname(serverLog), { recursive: true, mode: 0o700 }); } catch { /* its log is best kept */ }
56
58
  let logged = 0;
57
59
  const server = spawn(seed.program, [...config, 'app-server', '--listen', endpoint], { stdio: ['ignore', 'ignore', 'pipe'] });
@@ -79,9 +81,7 @@ try {
79
81
  const deadline = Date.now() + 15_000;
80
82
  while (!existsSync(socketPath) && !ended && !serverError && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50));
81
83
  if (!existsSync(socketPath) || serverError || ended) throw serverError ?? new Error('Native Codex question endpoint did not start; no TUI input was sent.');
82
- const staged = `${path}.${process.pid}`;
83
- writeFileSync(staged, JSON.stringify({ ...seed, socketPath, ownerPid: process.pid }), { mode: 0o600 });
84
- renameSync(staged, path);
84
+ await nativeLaunchBinding(path, { socketPath, home });
85
85
  const args = [...tuiArguments, '--remote', endpoint];
86
86
  if (!args.some((arg) => arg === '-C' || arg === '--cd' || arg.startsWith('--cd='))) args.push('--cd', process.cwd());
87
87
  if (seed.initialInput) args.push('--', seed.initialInput);
@@ -1,7 +1,7 @@
1
1
  // Native CLIs accept an initial prompt before their TUI starts. Their own
2
2
  // transcript, not a collapsed paste badge or an empty editor, confirms receipt.
3
- import { open, readdir, readFile, stat } from 'node:fs/promises';
4
- import { join, win32 } from 'node:path';
3
+ import { open, readdir, stat } from 'node:fs/promises';
4
+ import { basename, join, win32 } from 'node:path';
5
5
  import { StringDecoder } from 'node:string_decoder';
6
6
  import { codexQuestions } from './native-questions.mjs';
7
7
 
@@ -52,7 +52,7 @@ function codexRolloutIsLaunch(meta, { cwd, startedAt, continuing }) {
52
52
  return Date.parse(meta.timestamp) >= startedAt - 1000;
53
53
  }
54
54
 
55
- export async function confirmNativeInitialInput({ binding, kind, text, startedAt, codexSessions, cwd, continuing = false, resumeId = null }, { signal, deadlineMs, live }) {
55
+ export async function confirmNativeInitialInput({ binding, kind, text, startedAt, codexSessions, cwd, continuing = false, resumeId = null }, { signal, deadlineMs, live, store }) {
56
56
  const readers = new Map();
57
57
  const expired = () => signal.aborted || Date.now() >= deadlineMs;
58
58
  try {
@@ -63,10 +63,10 @@ export async function confirmNativeInitialInput({ binding, kind, text, startedAt
63
63
  if (codexSessions) {
64
64
  paths = await codexRolloutsSince(codexSessions, startedAt, continuing, resumeId);
65
65
  } else if (kind === 'claude-code') {
66
- paths = [JSON.parse(await readFile(binding.replace(/\.json$/, '.session.json'), 'utf8'))];
66
+ paths = [store.record('native_launches', basename(binding).replace(/\.json$/, '.session.json')) ?? {}];
67
67
  } else {
68
- const seed = JSON.parse(await readFile(binding, 'utf8'));
69
- if (seed.socketPath) {
68
+ const seed = store.record('native_launches', basename(binding));
69
+ if (seed?.socketPath) {
70
70
  const snapshot = await codexQuestions({ socketPath: seed.socketPath, discover: true, pathsOnly: true,
71
71
  timeoutMs: Math.min(2000, Math.max(1, deadlineMs - Date.now())) });
72
72
  paths = snapshot.threads.map(({ path }) => ({ path, offset: 0 }));
@@ -0,0 +1,35 @@
1
+ // A pane bootstrap asks the daemon for its launch record. The legacy path is
2
+ // an identifier only: no helper opens, rewrites or watches the old mail tree.
3
+ import { dirname, join, resolve, sep } from 'node:path';
4
+ import { openMachine } from './machine-door.mjs';
5
+ import { nodeRpc } from './client.mjs';
6
+
7
+ export function nativeLaunchKey(binding, mailRoot) {
8
+ const root = resolve(mailRoot, 'native-launches');
9
+ const path = resolve(String(binding ?? ''));
10
+ const key = path.slice(root.length + 1);
11
+ if (!path.startsWith(root + sep) || !/^[A-Za-z0-9_-]+\.json$/.test(key)) throw Object.assign(new Error(`${path} is not a native launch binding`), { code: 400 });
12
+ return key;
13
+ }
14
+
15
+ export async function nativeLaunchBinding(binding, { socketPath = null, home = null } = {}) {
16
+ const base = dirname(dirname(dirname(resolve(binding))));
17
+ const channel = await openMachine({ home: home ?? join(base, 'teams') });
18
+ try {
19
+ const rpc = await nodeRpc(channel);
20
+ const deadline = Date.now() + 10_000;
21
+ for (;;) {
22
+ try {
23
+ return await rpc.call('harness.v1.mail.native_launch', {
24
+ binding, ...(socketPath ? { socket_path: socketPath } : {}), claim: { pid: process.pid },
25
+ }, { timeoutMs: Math.max(1, deadline - Date.now()) });
26
+ } catch (error) {
27
+ if (error.code === -32601) throw new Error('this native pane needs a daemon that serves launch bindings; upgrade or restart its owned daemon before opening this pane. No TUI was started.');
28
+ // Only a positive "receipt not published yet" answer can be retried.
29
+ // An unanswered publication is never issued again automatically.
30
+ if (error.code !== 425 || Date.now() >= deadline) throw error;
31
+ await new Promise((done) => setTimeout(done, 50));
32
+ }
33
+ }
34
+ } finally { channel.close(); }
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.150",
3
+ "version": "0.3.152",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",
@@ -62,7 +62,7 @@
62
62
  "README.md"
63
63
  ],
64
64
  "engines": {
65
- "node": ">=20.19"
65
+ "node": ">=22.13.0"
66
66
  },
67
67
  "scripts": {},
68
68
  "dependencies": {
@@ -20,8 +20,7 @@
20
20
  // - A terminal start with no recorded launch answers `person`: a person at a terminal, or a launcher that left no
21
21
  // record (another program typing into a terminal), which nothing on the machine tells apart.
22
22
  // Files only grow, so each is read until its answer is known and then from where it was left.
23
- import { open, readdir, readFile } from 'node:fs/promises';
24
- import { join } from 'node:path';
23
+ import { open } from 'node:fs/promises';
25
24
 
26
25
  const facts = new Map(); // path → { offset, rest, started, firstInput, personTurns }
27
26
  const CHUNK = 4 * 1024 * 1024;
@@ -142,13 +141,13 @@ function reopens(kind, args) {
142
141
  }
143
142
 
144
143
  /** What this daemon's launches recorded: transcript paths, native session ids and first inputs. */
145
- async function launched(launchesDir, journalPath) {
144
+ async function launched(store, journalPath) {
146
145
  if (journalPath) await readJournal(journalPath);
147
- const names = (await readdir(launchesDir).catch(() => [])).filter((name) => name.endsWith('.json'));
148
- for (const name of names) {
146
+ const records = store.records('native_launches').filter(({ key }) => key.endsWith('.json'));
147
+ const names = records.map(({ key }) => key);
148
+ for (const { key: name, value: record } of records) {
149
149
  if (bindings.has(name)) continue;
150
150
  try {
151
- const record = JSON.parse(await readFile(join(launchesDir, name), 'utf8'));
152
151
  bindings.set(name, name.endsWith('.session.json')
153
152
  ? (record.path ? { path: record.path } : null)
154
153
  : {
@@ -184,8 +183,8 @@ export function sessionPeople(sessions, options) {
184
183
  return answer;
185
184
  }
186
185
 
187
- async function answerPeople(sessions, { launchesDir, journalPath }) {
188
- const launches = await launched(launchesDir, journalPath);
186
+ async function answerPeople(sessions, { store, journalPath }) {
187
+ const launches = await launched(store, journalPath);
189
188
  const answers = [];
190
189
  for (const { harness, path, id } of sessions) {
191
190
  if ((typeof id === 'string' && launches.sessions.has(id)) || (typeof path === 'string' && launches.paths.has(path))) {
package/terminal-mail.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  // Per-recipient terminal mailbox. A retained paste is never repeated: retries
2
2
  // press Enter and look for the exact user message in the native transcript.
3
- import { mkdirSync, readFileSync, writeFileSync, renameSync, appendFileSync, statSync, openSync, readSync, closeSync, readdirSync } from 'node:fs';
4
- import { join } from 'node:path';
3
+ import { statSync, openSync, readSync, closeSync } from 'node:fs';
5
4
  import { createHash, randomUUID } from 'node:crypto';
6
5
  import { spawnSync } from 'node:child_process';
7
6
  import { StringDecoder } from 'node:string_decoder';
@@ -100,20 +99,15 @@ function confirmed(batch) {
100
99
  }
101
100
 
102
101
  export class TerminalMailbox {
103
- constructor(root, recipient, log, load) {
104
- this.dir = join(root, 'terminal-delivery', hash(recipient));
105
- mkdirSync(this.dir, { recursive: true, mode: 0o700 });
106
- this.file = join(this.dir, 'queue.json'); this.log = log; this.load = load;
107
- try { this.state = JSON.parse(readFileSync(this.file, 'utf8')); }
108
- catch (error) { if (error.code !== 'ENOENT') throw error; this.state = { recipient, pending: [], active: null, receipts: [], accepted: {} }; }
102
+ constructor(store, recipient, log, load) {
103
+ this.store = store; this.recipient = recipient; this.log = log; this.load = load;
104
+ this.state = store.terminalState(recipient) ?? { recipient, pending: [], active: null, receipts: [], accepted: {} };
109
105
  this.work = null;
110
106
  }
111
107
  save() {
112
108
  const encoded = JSON.stringify(this.state) + '\n';
113
109
  if (encoded === this.saved) return;
114
- const temporary = `${this.file}.${process.pid}.tmp`;
115
- writeFileSync(temporary, encoded, { mode: 0o600 });
116
- renameSync(temporary, this.file); this.saved = encoded;
110
+ this.store.saveTerminalState(this.recipient, this.state); this.saved = encoded;
117
111
  }
118
112
  receipt(ids) {
119
113
  const delivered = ids.every(id => this.state.receipts.some(r => r.message_ids.includes(id)));
@@ -233,7 +227,7 @@ export class TerminalMailbox {
233
227
  }
234
228
  if (seen) {
235
229
  const receipt = { batch: batch.id, message_ids: batch.message_ids, delivered: true, receipt: seen.fragments > 1 ? 'native-user-messages' : 'native-user-message', fragments: seen.fragments ?? 1, at: Date.now() };
236
- appendFileSync(join(this.dir, 'receipts.jsonl'), JSON.stringify(receipt) + '\n', { mode: 0o600 });
230
+ this.store.terminalEvent(this.recipient, 'receipt', receipt);
237
231
  if (batch.inputOwner) await this.releaseOwner(batch);
238
232
  this.state.receipts.push(receipt);
239
233
  this.state.active = null; this.save(); this.log(receipt); return;
@@ -340,7 +334,7 @@ export class TerminalMailbox {
340
334
  batch.enters += 1; batch.last_enter = Date.now(); this.save();
341
335
  await this.host.sendInput(this.session.id, '\r', { submitMode: 'single', ...(batch.kind === 'claude-code' ? { inputOwner: batch.inputOwner } : {}) });
342
336
  const attempt = { delivered: false, queued: true, batch: batch.id, message_ids: batch.message_ids, attempt: batch.enters, at: Date.now() };
343
- appendFileSync(join(this.dir, 'attempts.jsonl'), JSON.stringify(attempt) + '\n', { mode: 0o600 });
337
+ this.store.terminalEvent(this.recipient, 'attempt', attempt);
344
338
  this.log(attempt);
345
339
  }
346
340
  batch.error = batch.kind !== 'claude-code' && batch.enters >= 3 ? 'native transcript unconfirmed after Enter retries' : 'native transcript confirmation pending';
@@ -348,9 +342,4 @@ export class TerminalMailbox {
348
342
  }
349
343
  }
350
344
 
351
- export function retainedTerminalMail(root) {
352
- const directory = join(root, 'terminal-delivery');
353
- let names; try { names = readdirSync(directory); } catch (error) { if (error.code === 'ENOENT') return []; throw error; }
354
- return names.map(name => JSON.parse(readFileSync(join(directory, name, 'queue.json'), 'utf8')))
355
- .filter(state => state.active || state.pending.length);
356
- }
345
+ export function retainedTerminalMail(store) { return store.retainedTerminalStates(); }