@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 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 a harness process of their own: one serve process answers one request at a
724
- // time, and a long load would hold discovery and every small session's capture behind it.
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 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, hostKeyId });
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); });
@@ -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 in a lane of its own, outside the serial capture
438
- // queue, so no small session's capture waits behind it; only the capture of what it loaded joins the queue.
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
- (this.largeQueue??=new Map()).set(`${options.harness}:${options.nativeSessionId}`,options);
441
- if(!this.largeLane)this.largeLane=this.runLargeLane().finally(()=>{this.largeLane=null;});
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(this.largeQueue.size&&!this.abort.signal.aborted){
446
- // The session written most recently goes first: a watched live session never waits behind a slow old one.
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]=[...this.largeQueue.entries()].reduce((best,entry)=>written(entry)>written(best)?entry:best);this.largeQueue.delete(key);
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.7 s load: every 7 s; a load
453
- // that takes minutes: every LARGE_CAPTURE_EVERY_MS), so a live session streams while the lane stays light.
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 this.largeCore.session(options.locator).load({timeoutMs:Math.max(30000,Math.ceil(artifactBefore.size/(1024*1024))*1000)});
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 the lane on every pass.
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.48",
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",