@volter/supercode-teams 0.3.87 → 0.3.88
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/live-sessions.mjs +17 -5
- package/machine.mjs +3 -1
- package/mail/agents.mjs +30 -15
- package/mail/store.mjs +3 -1
- package/mail/verbs.mjs +16 -9
- package/package.json +1 -1
package/live-sessions.mjs
CHANGED
|
@@ -453,7 +453,13 @@ export class LiveSessions extends EventEmitter {
|
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
/** The session a client runs in, from its claim: its parent's current session, else its environment's id through the map. */
|
|
456
|
-
#sessionOfClaim({ pid, ppid, session }) {
|
|
456
|
+
#sessionOfClaim({ pid, ppid, session, runtime }) {
|
|
457
|
+
// a runtime supercode hosts (the native agent) says so in its claim: it is named by its own process first, never by
|
|
458
|
+
// the session whose shell started it; one with no receipt is named by the rest of its claim, as any client is
|
|
459
|
+
if (runtime === true) {
|
|
460
|
+
const own = this.#runtimeOfPid(Number(pid));
|
|
461
|
+
if (own) return own;
|
|
462
|
+
}
|
|
457
463
|
const parent = this.claude.get(Number(ppid));
|
|
458
464
|
if (parent && alive(parent.pid)) return this.#address('claude-code', parent.sessionId);
|
|
459
465
|
if (typeof session === 'string' && session) {
|
|
@@ -465,14 +471,20 @@ export class LiveSessions extends EventEmitter {
|
|
|
465
471
|
// only when exactly one live runtime's receipt names that process. A process several receipts name (harness serve
|
|
466
472
|
// records its own pid on each runtime it hosts with no child of its own) is no one runtime, and is refused.
|
|
467
473
|
for (const candidate of [Number(pid), Number(ppid)]) {
|
|
468
|
-
|
|
469
|
-
|
|
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`);
|
|
474
|
+
const found = this.#runtimeOfPid(candidate);
|
|
475
|
+
if (found) return found;
|
|
472
476
|
}
|
|
473
477
|
return null;
|
|
474
478
|
}
|
|
475
479
|
|
|
480
|
+
/** The runtime whose live receipt alone names process `pid`, or null; several receipts naming it are no one runtime, refused. */
|
|
481
|
+
#runtimeOfPid(pid) {
|
|
482
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
483
|
+
const held = [...this.runtimes.values()].filter((runtime) => runtime.pid === pid && alive(runtime.pid));
|
|
484
|
+
if (held.length > 1) throw new CallerRefused(`process ${pid} hosts ${held.length} runtimes, so which one is calling is not known; nothing was done`);
|
|
485
|
+
return held.length === 1 ? this.#address(held[0].harness, held[0].sessionId) : null;
|
|
486
|
+
}
|
|
487
|
+
|
|
476
488
|
/**
|
|
477
489
|
* Who a local client is, from its claim `{pid, ppid, session, named}` (docs/architecture/overview.md, "Who is
|
|
478
490
|
* calling"): a session's address, a board's address, or null for a person. Throws CallerRefused when the claim names
|
package/machine.mjs
CHANGED
|
@@ -649,7 +649,9 @@ export class TeamsMachine extends MachineCore {
|
|
|
649
649
|
deliverRuntime: deliverToRuntime, codexAnswer: (request) => codexQuestions(request) });
|
|
650
650
|
// an agent's verbs (3b-2): its threads, a delegation, its record
|
|
651
651
|
this.agentVerbs = new AgentVerbs({ machine, store: this.mailStore, agents: this.agents, live, supercodeBin: this.supercodeBin, env: this.env,
|
|
652
|
-
paneOf: (session) => this.mail.paneOf(session), mailRoot
|
|
652
|
+
paneOf: (session) => this.mail.paneOf(session), mailRoot,
|
|
653
|
+
// a delegation's pane that never became its holder is closed, as `panes.close` closes one
|
|
654
|
+
closePane: async (pane) => { const session = await this.#paneByContext(pane); if (session) { await this.host.closeSession(session.id); this.#panesChanged(); } } });
|
|
653
655
|
// the names sessions ran under (`<mail>/names.json`): a name an agent was given reaches the same session after it
|
|
654
656
|
// runs under another, kept from the map's changes (at most once a second)
|
|
655
657
|
let naming = null;
|
package/mail/agents.mjs
CHANGED
|
@@ -12,6 +12,8 @@ import { cameThroughLink, parseAddress, threadOf } from './compose.mjs';
|
|
|
12
12
|
|
|
13
13
|
/** The pseudo-harness of an agent address. */
|
|
14
14
|
export const AGENT_HARNESS = 'agent';
|
|
15
|
+
/** How recent a folder time is not trusted to say its folder is unchanged (a coarse clock's tick, a 1 s filesystem). */
|
|
16
|
+
const RACY_MS = 2000;
|
|
15
17
|
/** How long a send waits for a resumed session to be reachable again. */
|
|
16
18
|
export const RESUME_WAIT_MS = 45_000;
|
|
17
19
|
|
|
@@ -63,34 +65,47 @@ export class Agents {
|
|
|
63
65
|
thread(id) { return validName(id) ? readJson(join(this.threadsDir, `${id}.json`)) : null; }
|
|
64
66
|
|
|
65
67
|
/** Every thread, oldest first. */
|
|
66
|
-
threads() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
threads() { return this.#records().list.map((thread) => structuredClone(thread)); }
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The thread records, read once and kept while their folder is unchanged, with an index of them by holder and by
|
|
72
|
+
* channel marker. Every writer (this daemon, and mail_agent.rs's in the native watch until step 4) renames a whole
|
|
73
|
+
* record into the folder, which moves its time: one stat a call, never a read of every record, though a send's plan
|
|
74
|
+
* asks on every send and an agent's threads grow with its history. A folder time within RACY_MS of now is not trusted
|
|
75
|
+
* (a write in the same tick of a coarse clock leaves it unmoved): read again, and not kept, until it is older.
|
|
76
|
+
*/
|
|
77
|
+
#records() {
|
|
70
78
|
let changed;
|
|
71
|
-
try { changed = statSync(this.threadsDir).mtimeMs; } catch { return []; }
|
|
72
|
-
if (this.threadsCache?.changed === changed
|
|
79
|
+
try { changed = statSync(this.threadsDir).mtimeMs; } catch { return { list: [], byHolder: new Map(), byMarker: new Map() }; }
|
|
80
|
+
if (this.threadsCache?.changed === changed && Date.now() - changed > RACY_MS) return this.threadsCache;
|
|
73
81
|
let names = [];
|
|
74
|
-
try { names = readdirSync(this.threadsDir); } catch { return []; }
|
|
82
|
+
try { names = readdirSync(this.threadsDir); } catch { return { list: [], byHolder: new Map(), byMarker: new Map() }; }
|
|
75
83
|
const list = names.filter((name) => name.endsWith('.json')).map((name) => readJson(join(this.threadsDir, name))).filter((thread) => thread?.id)
|
|
76
84
|
.sort((a, b) => (a.created_at_ms - b.created_at_ms) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
const byHolder = new Map(), byMarker = new Map();
|
|
86
|
+
for (const thread of list) {
|
|
87
|
+
byHolder.set(thread.holder, [...(byHolder.get(thread.holder) ?? []), thread]);
|
|
88
|
+
for (const [marker, id] of Object.entries(thread.markers ?? {})) if (!byMarker.has(marker)) byMarker.set(marker, { thread, id });
|
|
89
|
+
}
|
|
90
|
+
const records = { changed, list, byHolder, byMarker };
|
|
91
|
+
if (Date.now() - changed > RACY_MS) this.threadsCache = records;
|
|
92
|
+
return records;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
/** The thread `address` holds for an agent other than as its main session (mail_agent.rs `thread_held_by`). */
|
|
82
96
|
heldBy(address) {
|
|
83
|
-
|
|
84
|
-
if (thread.
|
|
97
|
+
const held = [...(this.#records().byHolder.get(address) ?? [])].reverse().find((thread) => {
|
|
98
|
+
if (thread.parent != null) return false;
|
|
85
99
|
const agent = this.load(thread.agent);
|
|
86
100
|
return agent != null && agent.main_session !== address;
|
|
87
|
-
})
|
|
101
|
+
});
|
|
102
|
+
return held ? structuredClone(held) : null;
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
/** The thread whose channel message has the platform id `marker`, with that message's id. */
|
|
91
106
|
ofMarker(marker) {
|
|
92
|
-
|
|
93
|
-
return null;
|
|
107
|
+
const found = this.#records().byMarker.get(marker);
|
|
108
|
+
return found ? { thread: structuredClone(found.thread), id: found.id } : null;
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
/** The agent `address` is a session of: its main, or a thread's holder. */
|
|
@@ -163,7 +178,7 @@ export class Agents {
|
|
|
163
178
|
}
|
|
164
179
|
|
|
165
180
|
/** The threads of the agent `name`, oldest first. */
|
|
166
|
-
threadsOf(name) { return this
|
|
181
|
+
threadsOf(name) { return this.#records().list.filter((thread) => thread.agent === name).map((thread) => structuredClone(thread)); }
|
|
167
182
|
|
|
168
183
|
/**
|
|
169
184
|
* A line that reached an agent's main session through its own input (its terminal, its DM) is a root of the agent:
|
package/mail/store.mjs
CHANGED
|
@@ -124,7 +124,9 @@ export class MailStore extends EventEmitter {
|
|
|
124
124
|
if (!envelope || typeof envelope.id !== 'string') { box.skipped.add(`${part}/${file}`); return; }
|
|
125
125
|
// its body's first line is kept beside the header (never in it): a listing of threads names each root by it with no
|
|
126
126
|
// body read
|
|
127
|
-
|
|
127
|
+
// the line whole, then capped by characters (a surrogate pair is never split), never a slice of the body first
|
|
128
|
+
const end = typeof envelope.body === 'string' ? envelope.body.indexOf('\n') : -1;
|
|
129
|
+
const firstLine = typeof envelope.body === 'string' ? Array.from(end < 0 ? envelope.body : envelope.body.slice(0, end)).slice(0, 400).join('') : '';
|
|
128
130
|
box.files.set(`${part}/${file}`, { header: headerOf(envelope), state: STATES[part], path, part, firstLine });
|
|
129
131
|
}
|
|
130
132
|
|
package/mail/verbs.mjs
CHANGED
|
@@ -19,8 +19,8 @@ export class AgentVerbs {
|
|
|
19
19
|
/** `machine` this machine's name; `store` the mail store; `agents` an agent's records; `live` the live-session map;
|
|
20
20
|
* `supercodeBin` the native binary (`supercode open` starts a session in a pane); `paneOf(session)` the daemon pane a
|
|
21
21
|
* live session runs in; `mailRoot` the mail folder. */
|
|
22
|
-
constructor({ machine, store, agents, live, supercodeBin, env = process.env, paneOf, mailRoot }) {
|
|
23
|
-
Object.assign(this, { machine, store, agents, live, supercodeBin, env, paneOf, mailRoot });
|
|
22
|
+
constructor({ machine, store, agents, live, supercodeBin, env = process.env, paneOf, mailRoot, closePane = async () => {} }) {
|
|
23
|
+
Object.assign(this, { machine, store, agents, live, supercodeBin, env, paneOf, mailRoot, closePane });
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
#whole(stored) { const envelope = this.store.body(stored); return envelope ? { ...stored, envelope } : null; }
|
|
@@ -126,17 +126,24 @@ export class AgentVerbs {
|
|
|
126
126
|
child.on('close', (code) => resolve({ code, out, err }));
|
|
127
127
|
});
|
|
128
128
|
if (run.code !== 0) throw new Error(`the session did not start: ${run.err.trim()}`);
|
|
129
|
-
if (known) return known;
|
|
130
129
|
let pane = null;
|
|
131
130
|
try { pane = JSON.parse(run.out).pane; } catch { /* not reported */ }
|
|
131
|
+
if (known) return known;
|
|
132
132
|
if (typeof pane !== 'string') throw new Error('the new pane was not reported');
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
// a launch that fails once its pane is open closes that pane: nothing it started is left running, holding what was
|
|
134
|
+
// copied to it, after its thread goes back
|
|
135
|
+
try {
|
|
136
|
+
const started = Date.now();
|
|
137
|
+
while (Date.now() - started < 60_000) {
|
|
138
|
+
const session = this.live.sessions().find((row) => this.paneOf(row) === pane);
|
|
139
|
+
if (session) { await ready(session.address); return session.address; }
|
|
140
|
+
await sleep(500);
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`${program} started in pane ${pane}, but no session appeared there within a minute; its pane is closed`);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
try { await this.closePane(pane); } catch (closing) { error.message += ` (its pane ${pane} could not be closed: ${closing.message})`; }
|
|
145
|
+
throw error;
|
|
138
146
|
}
|
|
139
|
-
throw new Error(`${program} started in pane ${pane}, but no session appeared there within a minute`);
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.88",
|
|
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",
|