@volter/supercode-teams 0.3.90 → 0.3.91
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/connector/index.mjs +8 -2
- package/live-sessions.mjs +14 -6
- package/machine.mjs +1 -0
- package/mail/relay.mjs +32 -4
- package/package.json +1 -1
package/connector/index.mjs
CHANGED
|
@@ -19,6 +19,8 @@ import { NativeEffects } from './native-effects.mjs';
|
|
|
19
19
|
import { consumeAgentClosures } from './agent-closures.mjs';
|
|
20
20
|
|
|
21
21
|
const SYNC_PROBLEM_LIMIT=20;
|
|
22
|
+
// How many times a session's launch agent is asked of harness serve: when first seen, and after a change at most twice more.
|
|
23
|
+
const LAUNCH_AGENT_ASKS=3;
|
|
22
24
|
const CAPACITY_CODE='SUBSCRIPTION_OPEN_CAPACITY';
|
|
23
25
|
// Whether a root covers a session's folder: the root's own, or (unless the root is exact-only) one under it, by real path.
|
|
24
26
|
const realOr=path=>{try{return realpathSync(path);}catch{return path;}};
|
|
@@ -638,8 +640,12 @@ export class TeamsConnector {
|
|
|
638
640
|
// the binding is written only while this work is still the session's (checked again after its awaits)
|
|
639
641
|
const bindingRead=this.clock.nowMs();
|
|
640
642
|
try{await this.nativeEffects.discover(descriptor,ruleId,()=>this.sessionLive(key,ruleId,epoch)&&!((this.runtimeEnded?.get(key)??-Infinity)>=bindingRead));}catch(error){this.lastError=error;}
|
|
641
|
-
// The agent this session's launch named, read when first seen here and
|
|
642
|
-
|
|
643
|
+
// The agent this session's launch named, read when first seen here, and after a change at most twice more (a launch
|
|
644
|
+
// record written just after its session appeared), until queued for binding. Never on every change: a launch's agent
|
|
645
|
+
// is fixed at the launch, and asking on every transcript write of every live session kept harness serve's loop
|
|
646
|
+
// busy with more than one call a second (yuerans: 1357 calls over a second each in ten minutes).
|
|
647
|
+
const asked=this.launchChecked?.get(key)??0;
|
|
648
|
+
if(captured&&!this.state.agent_sessions?.[key]&&asked<LAUNCH_AGENT_ASKS&&(asked===0||changed)){(this.launchChecked??=new Map()).set(key,asked+1);await this.noteLaunchAgent(locator,()=>this.sessionLive(key,ruleId,epoch));}
|
|
643
649
|
// A session seen with its native bundle still owed (from before this start) waits for its file to go quiet; one
|
|
644
650
|
// captured now has its wait armed by the commit that records it (flushOutbox).
|
|
645
651
|
if(!changed)this.armNativeIdle(key);
|
package/live-sessions.mjs
CHANGED
|
@@ -62,11 +62,11 @@ function startedMs(pid) {
|
|
|
62
62
|
|
|
63
63
|
/** Whether the runtime a receipt names answers at its address as that runtime (its own identity, with the receipt's
|
|
64
64
|
* token): the proof a start read off the wall clock cannot give, as the clock may have stepped since the receipt.
|
|
65
|
-
* true or false, or null when it did not answer within
|
|
66
|
-
async function answersAs(receipt) {
|
|
65
|
+
* true or false, or null when it did not answer within `boundMs` (the request is aborted then): not known either way. */
|
|
66
|
+
async function answersAs(receipt, boundMs = 2000) {
|
|
67
67
|
try {
|
|
68
68
|
const client = new FrontendClient({ baseUrl: receipt.base_url, token: receipt.token, clientId: `live-${receipt.receipt_id}`, permissions: ['observe'] });
|
|
69
|
-
return (await client.describe({ signal: AbortSignal.timeout(
|
|
69
|
+
return (await client.describe({ signal: AbortSignal.timeout(boundMs) }))?.session_id === receipt.runtime_session_id;
|
|
70
70
|
} catch (error) {
|
|
71
71
|
if (error?.name === 'TimeoutError' || error?.name === 'AbortError') return null;
|
|
72
72
|
return false;
|
|
@@ -467,6 +467,12 @@ export class LiveSessions extends EventEmitter {
|
|
|
467
467
|
* file and checked as live_runtime.rs `resolve_live_runtime` checks it (its schema, a loopback address, its process
|
|
468
468
|
* alive, the source it names): the bearer token a delivery uses is read here, at the delivery, never kept in the map.
|
|
469
469
|
*/
|
|
470
|
+
/** The live runtimes of `harness` whose workspace is `cwd`, each by the ids it may be closed by (in-memory, no read). */
|
|
471
|
+
runtimesIn(harness, cwd) {
|
|
472
|
+
return [...this.runtimes.values()].filter((runtime) => runtime.harness === harness && runtime.cwd === cwd && alive(runtime.pid))
|
|
473
|
+
.map((runtime) => [...new Set([runtime.sessionId, runtime.runtimeSessionId].filter((id) => typeof id === 'string' && id))]);
|
|
474
|
+
}
|
|
475
|
+
|
|
470
476
|
runtimeReceipt(harness, id) {
|
|
471
477
|
for (const [receiptId, runtime] of this.runtimes) {
|
|
472
478
|
if (runtime.harness !== harness || (runtime.sessionId !== id && runtime.runtimeSessionId !== id) || !alive(runtime.pid)) continue;
|
|
@@ -544,10 +550,12 @@ export class LiveSessions extends EventEmitter {
|
|
|
544
550
|
if (held.length !== 1) return null;
|
|
545
551
|
// a caller's identity: checked again now, with no process (the runtime asked at its own address), as a runtime that
|
|
546
552
|
// ended may have left its pid to another process before the sweep lets its receipt go
|
|
547
|
-
|
|
548
|
-
|
|
553
|
+
// a host stalled by load answers late, not never: one that did not answer in 2 s is asked once more, for 8 s
|
|
554
|
+
let answered = await answersAs(held[0].probe);
|
|
555
|
+
if (answered === null) answered = await answersAs(held[0].probe, 8000);
|
|
556
|
+
// one that did not answer then is not known either way: refused, never taken for its parent's session or its
|
|
549
557
|
// environment's (which would name another caller)
|
|
550
|
-
if (answered === null) throw Object.assign(new CallerRefused(`process ${pid} is a runtime that did not answer within
|
|
558
|
+
if (answered === null) throw Object.assign(new CallerRefused(`process ${pid} is a runtime that did not answer within 10 s, so which session is calling was not confirmed; nothing was done`), { unconfirmed: true });
|
|
551
559
|
if (!answered) return null;
|
|
552
560
|
return this.#address(held[0].harness, held[0].sessionId);
|
|
553
561
|
}
|
package/machine.mjs
CHANGED
|
@@ -645,6 +645,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
645
645
|
recordedConfig: (locator) => this.#serveRequest('harness.v1.sessions.recorded_config', { locator }, 15_000) });
|
|
646
646
|
this.relays = new Relays({ mailRoot, machine, supercodeBin: this.supercodeBin, serve: (method, params, timeoutMs) => this.#serveRequest(method, params, timeoutMs),
|
|
647
647
|
runtimeOf: (harness, id) => live.runtimeReceipt(harness, id), runtimeNamed: (receiptId) => live.receiptNamed(receiptId),
|
|
648
|
+
runtimesIn: (harness, cwd) => live.runtimesIn(harness, cwd),
|
|
648
649
|
log: (line) => this.emit('mail-log', line) });
|
|
649
650
|
this.mailSend = new MailSend({ machine, mailRoot, live, store: this.mailStore, agents: this.agents, relays: this.relays,
|
|
650
651
|
// a send waits on its receiver's door there (a relay's receipt takes up to 90 s), never past MAIL_SEND_MS
|
package/mail/relay.mjs
CHANGED
|
@@ -25,6 +25,8 @@ export const MAX_RELAYED_BYTES = 100_000;
|
|
|
25
25
|
/** How many relays run at once, the most recently used (a relay runtime is some hundreds of MB; 10 held 2.1 GB on one
|
|
26
26
|
* machine): beyond them a relay's runtime is closed (its folder kept: it starts again when next needed). */
|
|
27
27
|
export const RELAYS_RUNNING = 4;
|
|
28
|
+
/** How long a relay whose runtime start lost its answer is looked at for a runtime that start left running. */
|
|
29
|
+
const LOST_START_KEPT_MS = 5 * 60_000;
|
|
28
30
|
|
|
29
31
|
const alive = (pid) => { if (!Number.isInteger(pid) || pid <= 0) return false; try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; } };
|
|
30
32
|
const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
|
|
@@ -61,8 +63,9 @@ export class Relays {
|
|
|
61
63
|
* id)` the live-runtime receipt the live-session map holds for a runtime id (its file read whole), or null;
|
|
62
64
|
* `runtimeNamed(receiptId)` one receipt read by its own name.
|
|
63
65
|
*/
|
|
64
|
-
constructor({ mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed = () => null, log = () => {} }) {
|
|
65
|
-
Object.assign(this, { mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed, log });
|
|
66
|
+
constructor({ mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed = () => null, runtimesIn = () => [], log = () => {} }) {
|
|
67
|
+
Object.assign(this, { mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed, runtimesIn, log });
|
|
68
|
+
this.lost = new Map(); // relay folder → when a start of its runtime lost its answer (the runtime may run, unnamed)
|
|
66
69
|
this.sending = new Map(); // relay folder → the send in progress through it (one at a time per relay)
|
|
67
70
|
this.running = new Map(); // relay folder → { connection, runtime_id, usedAt }: the relays this daemon runs
|
|
68
71
|
}
|
|
@@ -78,7 +81,25 @@ export class Relays {
|
|
|
78
81
|
// closed under the relay's own lock: a send to it that arrives meanwhile waits for the close, then starts it
|
|
79
82
|
// again, never handing its turn to a runtime that is closing
|
|
80
83
|
void this.#locked(directory, () => this.serve('harness.v1.runtimes.close', { connection: relay.connection, runtime_id: relay.runtime_id }, 15_000))
|
|
81
|
-
.catch((error) =>
|
|
84
|
+
.catch((error) => {
|
|
85
|
+
// still running, so still counted: the next cap closes it again (unless a send started it anew meanwhile)
|
|
86
|
+
if (!this.running.has(directory)) this.running.set(directory, relay);
|
|
87
|
+
this.log(`relay ${directory.split(/[\\/]/).pop()} beyond the ${RELAYS_RUNNING} most recent was not closed: ${error.message}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// a start whose answer was lost may have left a runtime running in its relay's folder that no record names: closed by
|
|
91
|
+
// its id once the live-session map holds it (looked for over five minutes: the start may finish after its answer)
|
|
92
|
+
for (const [directory, lostAt] of this.lost) {
|
|
93
|
+
if (Date.now() - lostAt > LOST_START_KEPT_MS) { this.lost.delete(directory); continue; }
|
|
94
|
+
if (this.sending.has(directory)) continue;
|
|
95
|
+
const named = readJson(join(directory, 'relay.json'))?.runtime_id;
|
|
96
|
+
for (const ids of this.runtimesIn('claude-code', directory)) {
|
|
97
|
+
if (ids.includes(named)) continue;
|
|
98
|
+
void this.#locked(directory, async () => {
|
|
99
|
+
for (const id of ids) { try { await this.serve('harness.v1.runtimes.close', { runtime_id: id }, 15_000); return; } catch { /* the other id */ } }
|
|
100
|
+
this.log(`relay ${directory.split(/[\\/]/).pop()}: a runtime its lost start left running was not closed`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
82
103
|
}
|
|
83
104
|
}
|
|
84
105
|
|
|
@@ -165,7 +186,7 @@ export class Relays {
|
|
|
165
186
|
catch (error) { if (this.runtimeOf('claude-code', record.runtime_id)) throw new Error(`could not retire the old relay: ${error.message}`); }
|
|
166
187
|
}
|
|
167
188
|
const key = spec.directory.split(/[\\/]/).pop();
|
|
168
|
-
const started = await this
|
|
189
|
+
const started = await this.#start(spec, {
|
|
169
190
|
harness: 'claude-code',
|
|
170
191
|
launch: {
|
|
171
192
|
program: 'claude',
|
|
@@ -270,6 +291,13 @@ export class Relays {
|
|
|
270
291
|
}
|
|
271
292
|
}
|
|
272
293
|
|
|
294
|
+
/** `runtimes.start` for a relay: a start that fails (its answer lost, or late past its bound) may still have left a
|
|
295
|
+
* runtime running, so its folder is looked at by the next caps (#cap). */
|
|
296
|
+
async #start(spec, params, timeoutMs) {
|
|
297
|
+
try { return await this.serve('harness.v1.runtimes.start', params, timeoutMs); }
|
|
298
|
+
catch (error) { this.lost.set(spec.directory, Date.now()); throw error; }
|
|
299
|
+
}
|
|
300
|
+
|
|
273
301
|
/** A relay this daemon runs, used now. */
|
|
274
302
|
#used(spec, connection) {
|
|
275
303
|
const runtimeId = readJson(spec.record)?.runtime_id;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.91",
|
|
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",
|