@volter/supercode-teams 0.3.151 → 0.3.153
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.
- package/README.md +14 -1
- package/bin/teams.mjs +10 -2
- package/machine.mjs +128 -43
- package/mail/agents.mjs +40 -119
- package/mail/carrier.mjs +72 -111
- package/mail/envelope.mjs +8 -10
- package/mail/hooks.mjs +43 -27
- package/mail/idle.mjs +14 -5
- package/mail/import-maildir.mjs +350 -0
- package/mail/questions.mjs +38 -46
- package/mail/reads.mjs +15 -26
- package/mail/relay-endpoint.mjs +14 -22
- package/mail/relay.mjs +100 -65
- package/mail/retire-maildir.mjs +122 -0
- package/mail/send.mjs +70 -83
- package/mail/sqlite-store.mjs +476 -0
- package/mail/sqlite.mjs +353 -0
- package/mail/transcript.mjs +3 -3
- package/mail/upkeep.mjs +46 -26
- package/mail/verbs.mjs +14 -36
- package/native-codex-pane.mjs +6 -6
- package/native-initial-input.mjs +6 -6
- package/native-launch-binding.mjs +35 -0
- package/package.json +2 -2
- package/session-people.mjs +7 -8
- package/terminal-mail.mjs +8 -19
- package/mail/store.mjs +0 -531
package/native-codex-pane.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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), '
|
|
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
|
-
|
|
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);
|
package/native-initial-input.mjs
CHANGED
|
@@ -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,
|
|
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 = [
|
|
66
|
+
paths = [store.record('native_launches', basename(binding).replace(/\.json$/, '.session.json')) ?? {}];
|
|
67
67
|
} else {
|
|
68
|
-
const seed =
|
|
69
|
-
if (seed
|
|
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.
|
|
3
|
+
"version": "0.3.153",
|
|
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": ">=
|
|
65
|
+
"node": ">=22.13.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {},
|
|
68
68
|
"dependencies": {
|
package/session-people.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
144
|
+
async function launched(store, journalPath) {
|
|
146
145
|
if (journalPath) await readJournal(journalPath);
|
|
147
|
-
const
|
|
148
|
-
|
|
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, {
|
|
188
|
-
const launches = await launched(
|
|
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 {
|
|
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(
|
|
104
|
-
this.
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(); }
|