@volter/supercode-teams 0.3.85 → 0.3.87
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/bin/workspace/calendar.mjs +13 -1
- package/bin/workspace.mjs +5 -0
- package/connector/index.mjs +20 -2
- package/execution.mjs +3 -1
- package/live-sessions.mjs +56 -4
- package/machine-events.mjs +7 -9
- package/machine.mjs +269 -64
- package/mail/agents.mjs +386 -0
- package/mail/compose.mjs +97 -0
- package/mail/envelope.mjs +155 -0
- package/mail/reads.mjs +389 -0
- package/mail/relay.mjs +233 -0
- package/mail/runtime.mjs +20 -0
- package/mail/send.mjs +723 -0
- package/mail/store.mjs +419 -0
- package/mail/transcript.mjs +287 -0
- package/mail/verbs.mjs +232 -0
- package/mail-history.mjs +1 -1
- package/package.json +2 -1
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
import { execFileSync } from 'node:child_process';
|
|
13
13
|
import { randomUUID } from 'node:crypto';
|
|
14
14
|
import { usage, requireArgs } from './args.mjs';
|
|
15
|
+
import { openMachine } from '../../machine-door.mjs';
|
|
16
|
+
import { nodeRpc } from '../../client.mjs';
|
|
17
|
+
import { ownClaim } from '../../live-sessions.mjs';
|
|
18
|
+
|
|
19
|
+
/** One mail sent by this machine's daemon, as this process's caller (its claim): throws when it was not sent. */
|
|
20
|
+
async function mail(to, body, subject) {
|
|
21
|
+
const channel = await openMachine({ machine: null });
|
|
22
|
+
try {
|
|
23
|
+
const answer = await (await nodeRpc(channel)).call('harness.v1.mail.send', { to, body, subject, claim: ownClaim() }, { timeoutMs: 120_000 });
|
|
24
|
+
if (answer?.code !== 0) throw new Error(answer?.text ?? 'not sent');
|
|
25
|
+
} finally { channel.close(); }
|
|
26
|
+
}
|
|
15
27
|
|
|
16
28
|
async function pack() {
|
|
17
29
|
try { return await import('@volter/supercode-calendar'); } catch { /* not installed */ }
|
|
@@ -116,7 +128,7 @@ export async function calendarCommand(sub, { flags, rest, output, client, contex
|
|
|
116
128
|
const uid = `notice-${risk.id}-${risk.due}`;
|
|
117
129
|
if (notices.has(uid)) continue;
|
|
118
130
|
const body = `${risk.title} (${risk.id}) is at risk for ${new Date(risk.due).toISOString().slice(0, 16)}Z: ${calendar.sentence(risk.chance, risk.p85, risk.due)} (${risk.reason}). The calendar's base rate; your call is yours.`;
|
|
119
|
-
|
|
131
|
+
await mail(String(flags.notify), body, `At risk: ${risk.id}`);
|
|
120
132
|
await put(client, path, 'notice', { '@type': 'Event', uid, title: `Mailed ${flags.notify} about ${risk.id}`, start: new Date().toISOString(), duration: 'PT0S', 'volter:kind': 'notice', 'volter:card': risk.id });
|
|
121
133
|
sent.push(risk.id);
|
|
122
134
|
}
|
package/bin/workspace.mjs
CHANGED
|
@@ -1132,6 +1132,11 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
1132
1132
|
if (!context.machine_only) machineEvents.published = new PublishedEvents({ supercodeHome: home, context: contextName, machineId: saved.machine.id });
|
|
1133
1133
|
const machine = new TeamsMachine({ home: daemonHome, supercode: binary, ...(flags.tmux_socket ? { tmuxSocket: String(flags.tmux_socket) } : {}), link: { server: context.origin, teamId: context.team_id, machineId: saved.machine.id, credential: saved.credential.token, incarnationId: connector.state.incarnation_id, configuration: () => connector.configuration() }, onTurn: (turn) => connector.noteTurn(turn), onTurnWithdrawn: (turn) => connector.withdrawTurn(turn) });
|
|
1134
1134
|
machine.machineEvents = machineEvents;
|
|
1135
|
+
// what ends the daemon's harness serve is said in the connector's log, as `machine start` says it
|
|
1136
|
+
machine.on('serve-exit', (exit) => process.stderr.write(`supercode teams: harness serve exited: ${JSON.stringify(exit)}\n`));
|
|
1137
|
+
machine.on('serve-stderr', (text) => process.stderr.write(`supercode teams: harness serve: ${String(text).trimEnd().slice(-2000)}\n`));
|
|
1138
|
+
// its mail goes through the daemon's own send, in this process
|
|
1139
|
+
machineEvents.send = (request) => machine.sendOwn(request);
|
|
1135
1140
|
// the Claude worker homes the roots declare: their live sessions are this machine's too (the live-session map)
|
|
1136
1141
|
connector.onClaudeHomes = (homes) => machine.live?.claudeHomes(homes);
|
|
1137
1142
|
// The boards registered with this daemon are published by this connector, under the machine credential (D138 step 4).
|
package/connector/index.mjs
CHANGED
|
@@ -176,6 +176,9 @@ function atomic(path, text) {
|
|
|
176
176
|
/** How long a held session's never-published observation is kept. */
|
|
177
177
|
const PROFILE_FORGOTTEN_MS=7*86400000;
|
|
178
178
|
|
|
179
|
+
/** How long the connector waits for the machine daemon's harness to come back before it gives up on its feeds. */
|
|
180
|
+
const REOPEN_PATIENCE_MS=3*60_000;
|
|
181
|
+
|
|
179
182
|
export class TeamsConnector {
|
|
180
183
|
constructor({server,teamId,machineId,sourceId,credential,stateDir,daemon=null,core,largeCore=core,slowCore=largeCore,harnesses=[],hostKeyId=null,effectExecutor=null,clock={nowMs:()=>Date.now()},fetch:fetchImpl=globalThis.fetch,heartbeatMs=15000,nativeIdleMs=NATIVE_IDLE_MS}){Object.assign(this,{server:serverOrigin(server),teamId,machineId,sourceId,credential,stateDir,daemon,core,largeCore,slowCore,harnesses,hostKeyId,effectExecutor,clock,fetch:fetchImpl,heartbeatMs,nativeIdleMs});mkdirSync(stateDir,{recursive:true,mode:0o700});chmodSync(stateDir,0o700);this.statePath=join(stateDir,'connector.json');this.state=existsSync(this.statePath)?JSON.parse(readFileSync(this.statePath,'utf8')):{version:1,sequence:0};this.loadEntries();this.state.effects??={};if(Array.isArray(this.state.sync_status?.problems))this.state.sync_status.problems=this.state.sync_status.problems.filter(item=>item?.code!==CAPACITY_CODE);this.forgetNativeCache();this.state.native_failed??={};this.spillHolders();this.removeUnheldSpills();this.state.incarnation_id=`inc_${randomBytes(16).toString('hex')}`;this.abort=new AbortController();this.work=Promise.resolve();this.nativeEffects=new NativeEffects(this);this.effects=new EffectConsumer(this,effectExecutor?{prepare:async effect=>({execute:()=>effectExecutor(effect,{core:this.core,signal:this.abort.signal})})}:this.nativeEffects);this.save();}
|
|
181
184
|
// A capture burst saves after every step, and each save is a synchronous stringify, write and fsync of the whole
|
|
@@ -515,8 +518,23 @@ export class TeamsConnector {
|
|
|
515
518
|
if(this.reopening||!this.feeds?.size)return;this.reopening=true;
|
|
516
519
|
process.stderr.write(`supercode teams: ${reason}; the session index feeds are opened again\n`);
|
|
517
520
|
for(const feed of this.feeds.values())feed.dead=true;
|
|
518
|
-
|
|
519
|
-
|
|
521
|
+
// The daemon starts its harness serve again after it ends (1 s doubling to 1 min): the feeds are opened again on a
|
|
522
|
+
// growing wait that outlasts it (1 s doubling to 30 s, for up to 3 minutes), and only a daemon that never brings
|
|
523
|
+
// one back ends this connector.
|
|
524
|
+
const started=Date.now();let wait=1000;
|
|
525
|
+
try{
|
|
526
|
+
for(;;){
|
|
527
|
+
try{for(const [key,feed] of [...this.feeds])if(feed.dead)await this.openFeed(key,feed,{});break;}
|
|
528
|
+
catch(error){
|
|
529
|
+
if(this.abort.signal.aborted)return;
|
|
530
|
+
if(Date.now()-started>REOPEN_PATIENCE_MS){this.feedsFailed(error);return;}
|
|
531
|
+
process.stderr.write(`supercode teams: the session index feeds are not open yet (${error?.message??error}); tried again in ${Math.round(wait/1000)} s\n`);
|
|
532
|
+
await new Promise(resolve=>{const timer=setTimeout(resolve,wait);timer.unref?.();});
|
|
533
|
+
wait=Math.min(30_000,wait*2);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
this.activityFingerprint=null;this.queueActivityFollow();
|
|
537
|
+
}
|
|
520
538
|
finally{this.reopening=false;}
|
|
521
539
|
}
|
|
522
540
|
/**
|
package/execution.mjs
CHANGED
|
@@ -109,7 +109,9 @@ export class JobOwner {
|
|
|
109
109
|
writeFileSync(join(this.dir, `${id}.json`), JSON.stringify(record), { mode: 0o600 });
|
|
110
110
|
this.live.set(id, record);
|
|
111
111
|
child.stdout.on('data', (bytes) => { if (!stream.closed) stream.write(bytes.toString('base64')); });
|
|
112
|
-
|
|
112
|
+
// a job that stopped reading its input (EPIPE) ends that job's stream, never the daemon
|
|
113
|
+
child.stdin.on('error', (error) => finish(`its input closed (${error.code ?? error.message})`));
|
|
114
|
+
stream.on('data', (chunk) => { if (!child.stdin.destroyed && child.stdin.writable) child.stdin.write(Buffer.from(chunk, 'base64')); });
|
|
113
115
|
const finish = (reason) => {
|
|
114
116
|
if (!this.live.has(id)) return;
|
|
115
117
|
this.live.delete(id);
|
package/live-sessions.mjs
CHANGED
|
@@ -219,6 +219,8 @@ export class LiveSessions extends EventEmitter {
|
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
221
|
if (typeof record.sessionId !== 'string' || !record.sessionId) return;
|
|
222
|
+
// a parked session (its process kept for a job) is no live session (claude_peer.rs `read_registry`)
|
|
223
|
+
if (record.parkedJobId != null) { if (known?.dir === dir) { this.#dropClaude(pid); this.#changed(); } return; }
|
|
222
224
|
const history = known ? known.history.filter((id) => id !== record.sessionId) : [];
|
|
223
225
|
history.push(record.sessionId);
|
|
224
226
|
while (history.length > HISTORY_KEPT) history.shift();
|
|
@@ -226,6 +228,7 @@ export class LiveSessions extends EventEmitter {
|
|
|
226
228
|
this.claude.set(pid, {
|
|
227
229
|
pid, dir, sessionId: record.sessionId, name: typeof record.name === 'string' ? record.name : '', status: typeof record.status === 'string' ? record.status : 'unknown',
|
|
228
230
|
cwd: typeof record.cwd === 'string' ? record.cwd : null, tmux: typeof record.tmux === 'string' ? record.tmux : null, procStart, history,
|
|
231
|
+
socket: typeof record.messagingSocketPath === 'string' && record.messagingSocketPath ? record.messagingSocketPath : null,
|
|
229
232
|
// a rewrite of the same process's file (its status, its name) keeps its start's check: no `ps` for each rewrite
|
|
230
233
|
...(known && known.procStart === procStart && known.startChecked ? { startChecked: known.startChecked } : {}),
|
|
231
234
|
});
|
|
@@ -371,20 +374,20 @@ export class LiveSessions extends EventEmitter {
|
|
|
371
374
|
sessions() {
|
|
372
375
|
const out = []; const seen = new Set();
|
|
373
376
|
const named = new Map([...this.claude.values()].map((entry) => [entry.sessionId, entry]));
|
|
374
|
-
for (const runtime of this.runtimes
|
|
377
|
+
for (const [id, runtime] of this.runtimes) {
|
|
375
378
|
if (!alive(runtime.pid)) continue;
|
|
376
379
|
const registered = named.get(runtime.sessionId) ?? (runtime.runtimeSessionId ? named.get(runtime.runtimeSessionId) : null);
|
|
377
380
|
if (registered?.name.startsWith(RELAY_PREFIX)) continue;
|
|
378
381
|
const address = this.#address(runtime.harness, runtime.sessionId);
|
|
379
382
|
if (seen.has(address)) continue; seen.add(address);
|
|
380
383
|
const name = registered?.name || `${runtime.harness}-${runtime.sessionId.slice(0, 8)}`;
|
|
381
|
-
out.push({ address, name: `${name}@${this.machine}`, status: 'hosted', door: 'runtime', pid: runtime.pid, cwd: runtime.cwd, pane: null, tools: false });
|
|
384
|
+
out.push({ address, name: `${name}@${this.machine}`, status: 'hosted', door: 'runtime', pid: runtime.pid, cwd: runtime.cwd, pane: null, tools: false, receipt: id });
|
|
382
385
|
}
|
|
383
386
|
for (const entry of this.claude.values()) {
|
|
384
387
|
if (entry.name.startsWith(RELAY_PREFIX) || !alive(entry.pid)) continue;
|
|
385
388
|
const address = this.#address('claude-code', entry.sessionId);
|
|
386
389
|
if (seen.has(address)) continue; seen.add(address);
|
|
387
|
-
out.push({ address, name: `${entry.name}@${this.machine}`, status: entry.status, door: 'native', pid: entry.pid, cwd: entry.cwd, pane: entry.tmux, tools: this.hasTools(entry.pid) });
|
|
390
|
+
out.push({ address, name: `${entry.name}@${this.machine}`, status: entry.status, door: 'native', pid: entry.pid, cwd: entry.cwd, pane: entry.tmux, tools: this.hasTools(entry.pid), socket: entry.socket ?? null });
|
|
388
391
|
}
|
|
389
392
|
const codex = new Set([...this.codexRunning.keys(), ...this.codexPaneOf.keys()]);
|
|
390
393
|
for (const sessionId of codex) {
|
|
@@ -399,6 +402,36 @@ export class LiveSessions extends EventEmitter {
|
|
|
399
402
|
return out;
|
|
400
403
|
}
|
|
401
404
|
|
|
405
|
+
/**
|
|
406
|
+
* The live-runtime receipt of `harness`'s runtime `id` (its source session or its runtime session), read whole from its
|
|
407
|
+
* file and checked as live_runtime.rs `resolve_live_runtime` checks it (its schema, a loopback address, its process
|
|
408
|
+
* alive, the source it names): the bearer token a delivery uses is read here, at the delivery, never kept in the map.
|
|
409
|
+
*/
|
|
410
|
+
runtimeReceipt(harness, id) {
|
|
411
|
+
for (const [receiptId, runtime] of this.runtimes) {
|
|
412
|
+
if (runtime.harness !== harness || (runtime.sessionId !== id && runtime.runtimeSessionId !== id) || !alive(runtime.pid)) continue;
|
|
413
|
+
const receipt = readReceiptFile(join(this.receiptsDir, `${receiptId}.json`));
|
|
414
|
+
if (receipt?.receipt_id === receiptId && receipt.source?.harness === harness && receipt.source?.session_id === runtime.sessionId) return receipt;
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The live-runtime receipt named `receiptId`, read from its own file (`<live-runtimes>/<receiptId>.json`) and taken
|
|
421
|
+
* into the map: a runtime whose receipt the watch has not delivered yet (its event late or lost) is read by name, never
|
|
422
|
+
* waited on or listed for.
|
|
423
|
+
*/
|
|
424
|
+
receiptNamed(receiptId) {
|
|
425
|
+
if (typeof receiptId !== 'string' || !/^[0-9a-f]+$/i.test(receiptId)) return null;
|
|
426
|
+
const receipt = readReceiptFile(join(this.receiptsDir, `${receiptId}.json`));
|
|
427
|
+
if (!receipt || receipt.receipt_id !== receiptId || typeof receipt.source?.harness !== 'string' || typeof receipt.source?.session_id !== 'string') return null;
|
|
428
|
+
if (!this.runtimes.has(receiptId)) {
|
|
429
|
+
this.runtimes.set(receiptId, { pid: receipt.pid, harness: receipt.source.harness, sessionId: receipt.source.session_id, runtimeSessionId: typeof receipt.runtime_session_id === 'string' ? receipt.runtime_session_id : null, cwd: typeof receipt.source.workspace === 'string' ? receipt.source.workspace : null });
|
|
430
|
+
this.#changed();
|
|
431
|
+
}
|
|
432
|
+
return receipt;
|
|
433
|
+
}
|
|
434
|
+
|
|
402
435
|
/** A session the index holds that is not live: mail to it is stored until it is next active, never refused. */
|
|
403
436
|
isIndexedNotLive(address) {
|
|
404
437
|
const [, machine, harness, sessionId] = String(address).split(':');
|
|
@@ -420,7 +453,7 @@ export class LiveSessions extends EventEmitter {
|
|
|
420
453
|
}
|
|
421
454
|
|
|
422
455
|
/** The session a client runs in, from its claim: its parent's current session, else its environment's id through the map. */
|
|
423
|
-
#sessionOfClaim({ ppid, session }) {
|
|
456
|
+
#sessionOfClaim({ pid, ppid, session }) {
|
|
424
457
|
const parent = this.claude.get(Number(ppid));
|
|
425
458
|
if (parent && alive(parent.pid)) return this.#address('claude-code', parent.sessionId);
|
|
426
459
|
if (typeof session === 'string' && session) {
|
|
@@ -428,6 +461,15 @@ export class LiveSessions extends EventEmitter {
|
|
|
428
461
|
if (!found) throw new CallerRefused(`the session this command runs in (${session}) is not one this machine's live sessions have held, so it cannot act for it; nothing was done`);
|
|
429
462
|
return found;
|
|
430
463
|
}
|
|
464
|
+
// a runtime supercode hosts, calling from its own process or a child of it (the native agent's mail tool): taken
|
|
465
|
+
// only when exactly one live runtime's receipt names that process. A process several receipts name (harness serve
|
|
466
|
+
// records its own pid on each runtime it hosts with no child of its own) is no one runtime, and is refused.
|
|
467
|
+
for (const candidate of [Number(pid), Number(ppid)]) {
|
|
468
|
+
if (!Number.isInteger(candidate) || candidate <= 0) continue;
|
|
469
|
+
const held = [...this.runtimes.values()].filter((runtime) => runtime.pid === candidate && alive(runtime.pid));
|
|
470
|
+
if (held.length === 1) return this.#address(held[0].harness, held[0].sessionId);
|
|
471
|
+
if (held.length > 1) throw new CallerRefused(`process ${candidate} hosts ${held.length} runtimes, so which one is calling is not known; nothing was done`);
|
|
472
|
+
}
|
|
431
473
|
return null;
|
|
432
474
|
}
|
|
433
475
|
|
|
@@ -458,6 +500,16 @@ export class LiveSessions extends EventEmitter {
|
|
|
458
500
|
}
|
|
459
501
|
}
|
|
460
502
|
|
|
503
|
+
/** A live-runtime receipt file, when it is one a delivery may use: its schema, and a loopback HTTP address. */
|
|
504
|
+
function readReceiptFile(path) {
|
|
505
|
+
let receipt = null;
|
|
506
|
+
try { receipt = JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
507
|
+
if (receipt?.schema !== 'supercode.live-runtime.v1' || typeof receipt.token !== 'string' || !receipt.token) return null;
|
|
508
|
+
let url = null; try { url = new URL(receipt.base_url); } catch { return null; }
|
|
509
|
+
if (url.protocol !== 'http:' || !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) || !alive(receipt.pid)) return null;
|
|
510
|
+
return receipt;
|
|
511
|
+
}
|
|
512
|
+
|
|
461
513
|
/** The environment variables a client's claim is read from: what names the session or board a process acts for. */
|
|
462
514
|
export const ACTING_VARIABLES = ['CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID', 'SUPERCODE_CALLER', 'SUPERCODE_BOARD_OWNER_PID'];
|
|
463
515
|
|
package/machine-events.mjs
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// processes through the daemon's `events` door (PublishedEvents), never one stream per process.
|
|
5
5
|
import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { spawn } from 'node:child_process';
|
|
8
7
|
import { createHash, randomBytes } from 'node:crypto';
|
|
9
8
|
import { fromContext } from '@volter/teams/context';
|
|
10
9
|
import { retryAfterMs, spentClause } from '@volter/teams/http-client';
|
|
@@ -80,14 +79,13 @@ export class MachineEvents {
|
|
|
80
79
|
}
|
|
81
80
|
})().finally(()=>{this.flushing=null;});return this.flushing;
|
|
82
81
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});}
|
|
82
|
+
/** One event's mail, through this machine's daemon's own send (`send`, set by the connector that runs both): retained
|
|
83
|
+
* for retry when it is not delivered. */
|
|
84
|
+
async deliver(item){
|
|
85
|
+
if(typeof this.send!=='function')throw new Error('this machine\'s daemon has not started; retained for retry');
|
|
86
|
+
const receipt=await this.send({from:`sc:${this.machine}:operator:teams-events`,from_name:'Teams machine events',to:item.to,id:item.id,queue:item.to.split(':')[2]==='board',notice:true,body:item.body});
|
|
87
|
+
if(receipt?.code!==0)throw new Error(receipt?.text||'not delivered');
|
|
88
|
+
}
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
// What a local reader's cursor names: this connector's stream continuity (its epoch) and a server event sequence. A
|