@volter/supercode-teams 0.2.48 → 0.2.49
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.mjs +5 -4
- package/connector/index.mjs +20 -14
- package/package.json +1 -1
package/bin/workspace.mjs
CHANGED
|
@@ -720,14 +720,15 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
720
720
|
// callers the server admits; its host key id rides the connector's heartbeat.
|
|
721
721
|
const daemonHome = teamsHome();
|
|
722
722
|
const hostKeyId = publicKeyId(loadOrCreateKeyPair(paths(daemonHome).machineKey).publicKeyPem);
|
|
723
|
-
// Large transcripts load through
|
|
724
|
-
// time, and a long load would hold discovery and every
|
|
723
|
+
// Large transcripts load through harness processes of their own (a fast lane and a slow one): one serve process
|
|
724
|
+
// answers one request at a time, and a long load would hold discovery and every other capture behind it.
|
|
725
725
|
const largeCore = new SupercodeHarnessClient({ command: binary, args: ['harness', 'serve'] });
|
|
726
|
-
const
|
|
726
|
+
const slowCore = new SupercodeHarnessClient({ command: binary, args: ['harness', 'serve'] });
|
|
727
|
+
const connector = new TeamsConnector({ server: context.origin, teamId: context.team_id, machineId: saved.machine.id, sourceId: saved.source.id, credential: saved.credential.token, stateDir: state.directory, core, largeCore, slowCore, hostKeyId });
|
|
727
728
|
// A connected machine is a person's: each pane it launches opens a window on its screen (SUPERCODE_POPUPS=0: none).
|
|
728
729
|
const machine = new TeamsMachine({ home: daemonHome, supercode: binary, popups: process.env.SUPERCODE_POPUPS !== '0', ...(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) });
|
|
729
730
|
machine.on('link', (event) => { if (event.event === 'replaced') process.stderr.write(`supercode teams: ${event.error}. Two connectors were running for this machine; stop one of them (\`supercode teams disconnect\` where it should not run).\n`); });
|
|
730
|
-
const stopAll = async () => { await machine.stop().catch(() => {}); await connector.stop(); await core.close(); await largeCore.close(); await lifecycle.close(); state.update(current => current ? ({ ...current, process: undefined }) : null); };
|
|
731
|
+
const stopAll = async () => { await machine.stop().catch(() => {}); await connector.stop(); await core.close(); await largeCore.close(); await slowCore.close(); await lifecycle.close(); state.update(current => current ? ({ ...current, process: undefined }) : null); };
|
|
731
732
|
try { await connector.start({ roots: () => saved.roots }); await machine.start(); } catch (error) { await stopAll(); throw error; }
|
|
732
733
|
output({ version: 1, context: scope(context), data: { machine: saved.machine, source: saved.source, roots: saved.roots.map(({ path: _path, ...item }) => item), socket: machine.socketPath, foreground: true } });
|
|
733
734
|
await new Promise(resolveStop => { let stopped = false; stopConnector = async () => { if (stopped) return; stopped = true; await stopAll(); resolveStop(); }; for (const signal of ['SIGINT', 'SIGTERM', ...(process.platform === 'win32' ? ['SIGHUP', 'SIGBREAK'] : [])]) process.once(signal, stopConnector); });
|
package/connector/index.mjs
CHANGED
|
@@ -65,7 +65,7 @@ const NATIVE_IDLE_MS=60000,NATIVE_IDLE_BYTES=8*1024*1024;
|
|
|
65
65
|
// other request queued behind. A file of LARGE_CAPTURE_BYTES or more is captured at most once per
|
|
66
66
|
// LARGE_CAPTURE_EVERY_MS (tried or not), its load given a second per megabyte, and one past NATIVE_MAX_BYTES is
|
|
67
67
|
// published without a native bundle, which could not be built in time.
|
|
68
|
-
const LARGE_CAPTURE_BYTES=8*1024*1024,LARGE_CAPTURE_EVERY_MS=5*60000,NATIVE_MAX_BYTES=64*1024*1024;
|
|
68
|
+
const LARGE_CAPTURE_BYTES=8*1024*1024,LARGE_CAPTURE_EVERY_MS=5*60000,NATIVE_MAX_BYTES=64*1024*1024,FAST_LOAD_MS=5000;
|
|
69
69
|
// A turn a Room member typed here waits this long to be found in its session's transcript.
|
|
70
70
|
const TURN_NOTE_MS=24*3600000;
|
|
71
71
|
const messageText=value=>typeof value?.content==='string'?value.content.trim():Array.isArray(value?.content)?value.content.map(part=>typeof part==='string'?part:part?.type==='text'?part.text??'':'').join('').trim():'';
|
|
@@ -88,7 +88,7 @@ function atomic(path, value) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
export class TeamsConnector {
|
|
91
|
-
constructor({server,teamId,machineId,sourceId,credential,stateDir,core,largeCore=core,hostKeyId=null,effectExecutor=null,clock={nowMs:()=>Date.now()},fetch:fetchImpl=globalThis.fetch,heartbeatMs=15000,nativeIdleMs=NATIVE_IDLE_MS,nativeIdleBytes=NATIVE_IDLE_BYTES}){Object.assign(this,{server:serverOrigin(server),teamId,machineId,sourceId,credential,stateDir,core,largeCore,hostKeyId,effectExecutor,clock,fetch:fetchImpl,heartbeatMs,nativeIdleMs,nativeIdleBytes});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,origins:{},outbox:[]};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.state.native_cache??={};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();}
|
|
91
|
+
constructor({server,teamId,machineId,sourceId,credential,stateDir,core,largeCore=core,slowCore=largeCore,hostKeyId=null,effectExecutor=null,clock={nowMs:()=>Date.now()},fetch:fetchImpl=globalThis.fetch,heartbeatMs=15000,nativeIdleMs=NATIVE_IDLE_MS,nativeIdleBytes=NATIVE_IDLE_BYTES}){Object.assign(this,{server:serverOrigin(server),teamId,machineId,sourceId,credential,stateDir,core,largeCore,slowCore,hostKeyId,effectExecutor,clock,fetch:fetchImpl,heartbeatMs,nativeIdleMs,nativeIdleBytes});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,origins:{},outbox:[]};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.state.native_cache??={};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();}
|
|
92
92
|
save(){atomic(this.statePath,this.state);}
|
|
93
93
|
// What `supercode teams status` shows about this connector: its last error, and the sessions it could not capture
|
|
94
94
|
// (most recent first, bounded; an entry clears when that session next captures). They are kept in connector.json,
|
|
@@ -434,33 +434,39 @@ export class TeamsConnector {
|
|
|
434
434
|
largeSession({locator}){
|
|
435
435
|
try{return locator?.storage?.kind==='file'&&statSync(realpathSync(locator.storage.path)).size>=LARGE_CAPTURE_BYTES;}catch{return false;}
|
|
436
436
|
}
|
|
437
|
-
// A large transcript's load takes seconds to minutes. It runs
|
|
438
|
-
//
|
|
437
|
+
// A large transcript's load takes seconds to minutes. It runs outside the serial capture queue, so no small
|
|
438
|
+
// session's capture waits behind it; only the capture of what it loaded joins the queue. Two lanes, each with a
|
|
439
|
+
// harness process of its own: a session whose last load was quick (FAST_LOAD_MS) goes to the fast lane, one never
|
|
440
|
+
// loaded or slow to the slow one, so a live session never waits behind a giant old transcript.
|
|
439
441
|
captureLarge(options){
|
|
440
|
-
|
|
441
|
-
|
|
442
|
+
const key=`${options.harness}:${options.nativeSessionId}`,last=this.largeCaptures?.get(key);
|
|
443
|
+
const lane=last&&last.ms<FAST_LOAD_MS?'fast':'slow';
|
|
444
|
+
this.lanes??={fast:{queue:new Map(),running:null,core:this.largeCore},slow:{queue:new Map(),running:null,core:this.slowCore}};
|
|
445
|
+
for(const other of Object.values(this.lanes))other.queue.delete(key);
|
|
446
|
+
const target=this.lanes[lane];target.queue.set(key,options);
|
|
447
|
+
if(!target.running)target.running=this.runLargeLane(target).finally(()=>{target.running=null;});
|
|
442
448
|
return null;
|
|
443
449
|
}
|
|
444
|
-
async runLargeLane(){
|
|
445
|
-
while(
|
|
446
|
-
// The session written most recently goes first
|
|
450
|
+
async runLargeLane(lane){
|
|
451
|
+
while(lane.queue.size&&!this.abort.signal.aborted){
|
|
452
|
+
// The session written most recently goes first.
|
|
447
453
|
const written=([,item])=>{try{return statSync(realpathSync(item.locator.storage.path)).mtimeMs;}catch{return 0;}};
|
|
448
|
-
const [key,options]=[...
|
|
454
|
+
const [key,options]=[...lane.queue.entries()].reduce((best,entry)=>written(entry)>written(best)?entry:best);lane.queue.delete(key);
|
|
449
455
|
let started=null;
|
|
450
456
|
try{
|
|
451
457
|
const artifactBefore=statSync(realpathSync(options.locator.storage.path));
|
|
452
|
-
// A session is loaded again no sooner than ten times its last load took (a 0.
|
|
453
|
-
// that takes minutes: every LARGE_CAPTURE_EVERY_MS), so a live session streams while the
|
|
458
|
+
// A session is loaded again no sooner than ten times its last load took (a 0.5 s load: every 5 s; a load
|
|
459
|
+
// that takes minutes: every LARGE_CAPTURE_EVERY_MS), so a live session streams while the lanes stay light.
|
|
454
460
|
// A change that arrives sooner is captured when its wait ends.
|
|
455
461
|
this.largeCaptures??=new Map();const last=this.largeCaptures.get(key),wait=last?Math.min(LARGE_CAPTURE_EVERY_MS,last.ms*10)-(this.clock.nowMs()-last.at):0;
|
|
456
462
|
if(wait>0){this.largeRetry??=new Map();if(!this.largeRetry.has(key))this.largeRetry.set(key,setTimeout(()=>{this.largeRetry.delete(key);this.captureLarge(options);},wait).unref?.());continue;}
|
|
457
463
|
started=this.clock.nowMs();
|
|
458
|
-
const loaded=await
|
|
464
|
+
const loaded=await lane.core.session(options.locator).load({timeoutMs:Math.max(30000,Math.ceil(artifactBefore.size/(1024*1024))*1000)});
|
|
459
465
|
this.largeCaptures.set(key,{at:started,ms:this.clock.nowMs()-started});
|
|
460
466
|
await this.capture({...options,preloaded:{loaded,artifactBefore}});
|
|
461
467
|
}catch(error){
|
|
462
468
|
if(this.abort.signal.aborted)return;
|
|
463
|
-
// A load that failed or timed out waits like a slow one, so it does not hold
|
|
469
|
+
// A load that failed or timed out waits like a slow one, so it does not hold its lane on every pass.
|
|
464
470
|
if(started!==null)this.largeCaptures.set(key,{at:started,ms:this.clock.nowMs()-started});
|
|
465
471
|
this.noteSessionProblem(options,error);
|
|
466
472
|
process.stderr.write(`supercode teams: ${options.harness} session ${options.nativeSessionId} not synced: ${error.message}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.49",
|
|
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",
|