@volter/supercode-teams 0.2.117 → 0.2.119

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/teams.mjs CHANGED
@@ -19,6 +19,7 @@
19
19
  // title set <pane> <text> | title ls
20
20
  // attention ls | rank | request <pane> [--urgency now|soon|whenever] [--reason text] | clear <pane>
21
21
  // watch [--interval ms] [--for ms]
22
+ // launches get <key> | list operators: keyed node launch receipts
22
23
  // rpc <harness.v1 method> [json-params] operators: one harness.v1 request
23
24
  // log verify
24
25
  //
@@ -44,7 +45,7 @@ import { moveWorkspace } from '../workspace-move.mjs';
44
45
  import { teamsHome, paths, readJson, AccountLog, parseHostPort } from '@volter/teams/home';
45
46
  import { NODE_PROTOCOL } from '@volter/teams/channel';
46
47
 
47
- const MACHINE_VERBS = new Set(['machine', 'describe', 'panes', 'input', 'suspend', 'resume', 'files', 'ports', 'port', 'browser', 'stream', 'jobs', 'workspace', 'title', 'attention', 'watch', 'rpc', 'log', 'mail']);
48
+ const MACHINE_VERBS = new Set(['machine', 'describe', 'panes', 'input', 'suspend', 'resume', 'files', 'ports', 'port', 'browser', 'stream', 'jobs', 'workspace', 'title', 'attention', 'watch', 'rpc', 'launches', 'log', 'mail']);
48
49
  // Every flag a machine verb reads; any other is refused rather than silently ignored.
49
50
  const KNOWN_FLAGS = new Set(['home', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path']);
50
51
 
@@ -64,7 +65,7 @@ for (let i = 0; i < argv.length; i += 1) {
64
65
  }
65
66
  const [verb, sub, ...rest] = positional;
66
67
 
67
- const MACHINE_HELP = 'machine verbs: machine start|status, describe, panes, input, suspend, resume, files, ports, port, stream, jobs, workspace, title, attention, watch, rpc, log, browser (add --machine NAME for an enrolled machine)';
68
+ const MACHINE_HELP = 'machine verbs: machine start|status, describe, panes, input, suspend, resume, files, ports, port, stream, jobs, workspace, title, attention, watch, rpc, launches, log, browser (add --machine NAME for an enrolled machine)';
68
69
 
69
70
  if (verb === undefined || verb === 'help') {
70
71
  process.stdout.write(`supercode teams: ${MACHINE_HELP}\n`);
@@ -312,6 +313,12 @@ async function main() {
312
313
  case 'jobs':
313
314
  // `jobs [--machine NAME]`: the execution jobs the machine's daemon owns now.
314
315
  return withNode(async (rpc) => out(await rpc.call('jobs.list', {})));
316
+ case 'launches': {
317
+ if (sub === 'get' && rest[0]) return withNode(async (rpc) => out(await rpc.call('launches.get', { key: rest[0] })));
318
+ if (sub === 'list') return withNode(async (rpc) => out(await rpc.call('launches.list', {})));
319
+ die('launches get <key> | launches list [--machine NAME]');
320
+ return;
321
+ }
315
322
  case 'rpc': {
316
323
  const channel = await machineChannel();
317
324
  const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
@@ -207,6 +207,9 @@ export class TeamsConnector {
207
207
  /** The first sync pass, for a caller that waits on it; the connector keeps running if it fails. */
208
208
  this.firstPass = syncPass();
209
209
  this.timer = setInterval(syncPass, 15000);
210
+ // Viewed file-backed sessions should not wait for catalog discovery. Poll
211
+ // only their cheap file stamps; full reads remain coalesced in capture lanes.
212
+ this.liveCaptureTimer = setInterval(() => this.captureViewedFiles(), 750);
210
213
  this.heartbeatTimer = setInterval(() => {
211
214
  if (this.abort.signal.aborted) return;
212
215
  void heartbeat().catch(error => { this.lastError = error; });
@@ -243,6 +246,7 @@ export class TeamsConnector {
243
246
  // reads any session's tail, so a beat costs one bounded scan per root, not one over every session on the
244
247
  // machine; rules over the same root and harnesses share the scan.
245
248
  const discoveries=new Map(),followCandidates=[],discovered=new Set(),activity=[];
249
+ this.liveFiles??=new Map();
246
250
  this.turnStates??=new Map();
247
251
  for(const rule of activeRules.values()){
248
252
  const root=roots.find(item=>item.id===rule.selector.root_id);
@@ -268,6 +272,7 @@ export class TeamsConnector {
268
272
  const turn=descriptor.activity?.turn;
269
273
  if(typeof turn==='string'&&turn&&activity.length<4096){const key=`${harness}:${nativeSessionId}`,held=this.turnStates.get(key),since=held?.turn===turn?held.since:this.clock.nowMs();this.turnStates.set(key,{turn,since});activity.push({harness,native_id:nativeSessionId,turn,since});}
270
274
  const options={locator,descriptor,harness,nativeSessionId,ruleId:rule.id,ruleRevision:rule.revision,backfill:rule.backfill,rootPath:workspace,configuration};
275
+ if(locator.storage?.kind==='file')this.liveFiles.set(`${harness}:${nativeSessionId}`,options);
271
276
  // A session whose native store has not changed since this rule last captured it is not reloaded.
272
277
  // One session that cannot be captured (a transcript that rewrote its history needs an explicit resync)
273
278
  // is recorded, logged and skipped; it must not stop every other session, or the connector itself, from
@@ -289,12 +294,29 @@ export class TeamsConnector {
289
294
  followCandidates.push({key:`${rule.id}:${harness}:${nativeSessionId}`,activity:this.sessionActivity(options),options});
290
295
  }
291
296
  }
297
+ for(const [key,options] of this.liveFiles)if(!discovered.has(key)||!activeRules.has(options.ruleId))this.liveFiles.delete(key);
292
298
  this.nativeEffects.endDiscovery();
293
299
  for(const key of this.turnStates.keys())if(!discovered.has(key))this.turnStates.delete(key);
294
300
  this.reportedActivity=activity;
295
301
  this.pruneNativeCache(discovered);
296
302
  this.rotateFollowers(followCandidates);
297
303
  }
304
+ captureViewedFiles(){
305
+ if(this.abort.signal.aborted)return;
306
+ this.liveFileCaptures??=new Set();this.liveFileStamps??=new Map();
307
+ for(const key of [...(this.watched??[])].slice(0,FOLLOW_LIMIT)){
308
+ const known=this.liveFiles?.get(key);
309
+ if(!known||this.liveFileCaptures.has(key))continue;
310
+ const options={...known};
311
+ if(!this.sessionChanged(options)||this.liveFileStamps.get(key)===options.stamp)continue;
312
+ this.liveFileStamps.set(key,options.stamp);this.liveFileCaptures.add(key);
313
+ // Re-read authorization when a live file changes; never keep a stale rule
314
+ // alive merely because somebody still has the transcript open.
315
+ void this.capture({...options,configuration:null,live:true}).catch(error=>{
316
+ this.liveFileStamps.delete(key);if(!this.abort.signal.aborted)this.noteSessionProblem(options,error);
317
+ }).finally(()=>this.liveFileCaptures.delete(key));
318
+ }
319
+ }
298
320
  // A session never published under a rule that keeps back history recorded before a time (a `since` window, or a
299
321
  // `none` rule's capture_after), whose native file was last written before that time, holds only history the rule
300
322
  // keeps back: loading it (a 228 MB transcript is gigabytes in the harness process and here) would publish nothing.
@@ -538,6 +560,8 @@ export class TeamsConnector {
538
560
  const [key,options]=[...lane.queue.entries()].reduce((best,entry)=>written(entry)>written(best)?entry:best);lane.queue.delete(key);
539
561
  let started=null;
540
562
  try{
563
+ const seen=this.state.origins[key]?.capture_stamps?.[options.ruleId];
564
+ if(options.live&&seen?.rule_revision===options.ruleRevision&&seen.stamp===this.sessionStamp(options))continue;
541
565
  const artifactBefore=statSync(realpathSync(options.locator.storage.path));
542
566
  // A session is loaded again no sooner than ten times its last load took (a 0.5 s load: every 5 s; a load
543
567
  // that takes minutes: every LARGE_CAPTURE_EVERY_MS), so a live session streams while the lanes stay light.
@@ -740,5 +764,5 @@ export class TeamsConnector {
740
764
  }
741
765
  async flushOutbox(){let latest=null;while(this.state.outbox.length){const item=this.state.outbox[0];let receipt;this.dropLostArtifacts(item);this.refitParts(item);try{receipt=await this.sendCapture(item);}catch(error){if(error.status===409&&error.code==='carry_mismatch'){const key=`${item.begin.harness}:${item.begin.native_session_id}`;this.lastError=error;process.stderr.write(`supercode teams: the server does not hold what a capture of ${item.begin.harness} session ${item.begin.native_session_id} carried; it goes whole next time\n`);this.state.outbox.shift();if(this.state.origins[key])this.state.origins[key]={...this.state.origins[key],observed_keys:[],observed_digests:[],carry_refused:true};this.save();continue;}if(error.status===400&&error.code==='invalid_request'||error.status===413){this.lastError=error;this.noteSessionProblem({harness:item.begin.harness,nativeSessionId:item.begin.native_session_id,ruleId:item.begin.rule_id??null},error);(this.droppedCaptures??=new Set()).add(`${item.begin.harness}:${item.begin.native_session_id}`);process.stderr.write(`supercode teams: dropped a queued capture of ${item.begin.harness} session ${item.begin.native_session_id} the server refused${error.status===413?' (one message is larger than a request may be)':' as malformed'}: ${error.message}\n`);this.state.outbox.shift();this.save();continue;}if(!await this.ruleRejected(item,error))throw error;this.lastError=error;process.stderr.write(`supercode teams: dropped a queued capture of ${item.begin.harness} session ${item.begin.native_session_id}; its sync rule changed or was removed: ${error.message}\n`);this.state.outbox.shift();this.save();continue;}const key=`${item.begin.harness}:${item.begin.native_session_id}`,prior=this.state.origins[key]??{},next={...prior,generation:item.begin.generation,sequence:item.begin.sequence,digest:item.commit.digest,observed_keys:item.observed?.keys??prior.observed_keys,observed_digests:item.observed?.digests??prior.observed_digests,last_rule_revision:item.begin.rule_revision,backfill_mode:item.backfillMode};if(item.clearBaseline){delete next.baseline;delete next.baseline_digests;delete next.baseline_tool_tail;}if(item.nativeDeferred)next.native_deferred=true;else delete next.native_deferred;if(!item.begin.carry)delete next.carry_refused;if(item.stamp&&item.begin.rule_id)next.capture_stamps={...prior.capture_stamps,[item.begin.rule_id]:{stamp:item.stamp,rule_revision:item.begin.rule_revision,backfill_mode:item.backfillMode}};this.state.origins[key]=next;this.state.outbox.shift();this.save();// A queued capture is often flushed by another session's capture: its own session's problem ends with it.
742
766
  this.clearSessionProblem(item.begin.harness,item.begin.native_session_id);if(item.artifacts?.length)this.pruneSpill();latest=receipt;}return latest;}
743
- stop(){try{this.flushState();}catch{}if(this.timer)clearInterval(this.timer);if(this.heartbeatTimer)clearInterval(this.heartbeatTimer);this.abort.abort();for(const follower of this.followers?.values()??[])follower.controller.abort();return this.nativeEffects.close();}
767
+ stop(){try{this.flushState();}catch{}if(this.timer)clearInterval(this.timer);if(this.liveCaptureTimer)clearInterval(this.liveCaptureTimer);if(this.heartbeatTimer)clearInterval(this.heartbeatTimer);this.abort.abort();for(const follower of this.followers?.values()??[])follower.controller.abort();return this.nativeEffects.close();}
744
768
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.117",
3
+ "version": "0.2.119",
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",