@volter/supercode-teams 0.3.12 → 0.3.14

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
@@ -72,7 +72,9 @@ const [verb, sub, ...rest] = positional;
72
72
  // Each command is timed (the slow log, @volter/supercode-harness-sdk/slow-log); a service, a watch or a forward runs
73
73
  // until stopped and is timed one door at a time instead.
74
74
  const LONG_RUNNING = new Set(['connect', 'watch', 'port', 'stream', 'attach', 'serve']);
75
- if (!LONG_RUNNING.has(verb) && !(verb === 'machine' && sub === 'start') && !(verb === 'message' && sub === 'watch')) timeCommand(`teams ${[verb, sub].filter(Boolean).join(' ')}`, argv);
75
+ // A service runs this entry as `teams.mjs teams connect`: the leading `teams` is the entry's own name, not a verb.
76
+ const [timedVerb, timedSub] = verb === 'teams' ? [sub, rest[0]] : [verb, sub];
77
+ if (!LONG_RUNNING.has(timedVerb) && !(timedVerb === 'machine' && timedSub === 'start') && !(timedVerb === 'message' && timedSub === 'watch')) timeCommand(`teams ${[timedVerb, timedSub].filter(Boolean).join(' ')}`, argv);
76
78
 
77
79
  const MACHINE_HELP = 'machine verbs: machine start|status|restart, slow [--since 1h], describe, panes, input, suspend, resume, files, ports, port, stream, jobs, workspace, title, attention, watch, rpc, launches, log, health, browser (add --machine NAME for an enrolled machine)';
78
80
 
@@ -6,6 +6,10 @@ import { EffectConsumer } from './effects.mjs';
6
6
  import { NativeEffects } from './native-effects.mjs';
7
7
  import { consumeAgentClosures } from './agent-closures.mjs';
8
8
 
9
+ /** How often a sync pass reads a root's whole session catalog; between, it reads only what changed. */
10
+ const FULL_DISCOVERY_MS = 120_000;
11
+ /** How far back a changed-only read reaches past the last read: clock skew and writes landing out of order. */
12
+ const CATALOG_MARGIN_MS = 30_000;
9
13
  const SYNC_PROBLEM_LIMIT=20;
10
14
  // Live follows are bounded. The harness SDK lets at most 64 subscription opens await replies at once (sdk/typescript
11
15
  // core.mjs MAX_PENDING_RUNTIME_OPENS; more reject with SUBSCRIPTION_OPEN_CAPACITY) and every live follow is a follower
@@ -194,7 +198,7 @@ export class TeamsConnector {
194
198
  this.activityBindings=new Map();
195
199
  this.activityEvent=event=>{
196
200
  if(event.subscription!==this.activitySubscription)return;
197
- for(const activity of event.activities??[])this.recordActivityBinding(activity);
201
+ for(const activity of event.activities??[]){this.recordActivityBinding(activity);this.recordTurn(activity);}
198
202
  this.publishActivityHeartbeat();
199
203
  };
200
204
  this.core.on?.('sessionActivityEvent',this.activityEvent);
@@ -270,27 +274,20 @@ export class TeamsConnector {
270
274
  const root=roots.find(item=>item.id===rule.selector.root_id);
271
275
  if(!root)continue;
272
276
  const workspace=root.path??root.workspace,subtree=!root.exact_only,discoveryKey=JSON.stringify([workspace,subtree,[...(rule.selector.harnesses??[])].sort(),root.homes??null]);
273
- if(!discoveries.has(discoveryKey))discoveries.set(discoveryKey,(async()=>{
274
- const descriptors=[];
275
- let cursor;
276
- do {
277
- // A broad root enrolled before rules covered subfolders keeps its exact-folder discovery.
278
- const page=await this.core.discover({workspace,...(subtree?{workspace_subtree:true}:{}),harnesses:rule.selector.harnesses,homes:root.homes,limit:2048,...(cursor?{cursor}:{})});
279
- descriptors.push(...(page.sessions??page.entries??[]));
280
- cursor=page.next_cursor??page.nextCursor??null;
281
- } while(cursor);
282
- return descriptors;
283
- })());
284
- const descriptors=await discoveries.get(discoveryKey);
277
+ if(!discoveries.has(discoveryKey))discoveries.set(discoveryKey,this.catalogOf(discoveryKey,{workspace,...(subtree?{workspace_subtree:true}:{}),harnesses:rule.selector.harnesses,homes:root.homes}));
278
+ const {descriptors,fresh}=await discoveries.get(discoveryKey);
285
279
  for(const descriptor of descriptors){
286
280
  const locator=descriptor.locator,nativeSessionId=locator?.session_id,harness=locator?.harness;
287
281
  if(!locator||!nativeSessionId||!harness)continue;
288
282
  discovered.add(`${harness}:${nativeSessionId}`);
289
283
  activityLocators.set(`${harness}:${nativeSessionId}`,locator);
290
- if(descriptor.activity)this.recordActivityBinding({...descriptor.activity,harness,session_id:nativeSessionId});
284
+ // A descriptor this pass did not read again carries the activity of the pass that did: the activity
285
+ // subscription keeps the bindings current, and only a fresh read may change them here.
286
+ const key=`${harness}:${nativeSessionId}`,isFresh=fresh.has(key);
287
+ if(descriptor.activity&&isFresh)this.recordActivityBinding({...descriptor.activity,harness,session_id:nativeSessionId});
291
288
  // A session's turn state as its harness reports it, and since when this connector has seen it so.
292
- const turn=descriptor.activity?.turn;
293
- 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});}
289
+ const held=this.turnStates.get(key),turn=isFresh?descriptor.activity?.turn:held?.turn;
290
+ if(typeof turn==='string'&&turn&&activity.length<4096){const since=held?.turn===turn?held.since:this.clock.nowMs();this.turnStates.set(key,{turn,since});activity.push({harness,native_id:nativeSessionId,turn,since});}
294
291
  const options={locator,descriptor,harness,nativeSessionId,ruleId:rule.id,ruleRevision:rule.revision,backfill:rule.backfill,rootPath:workspace,configuration};
295
292
  if(locator.storage?.kind==='file')this.liveFiles.set(`${harness}:${nativeSessionId}`,options);
296
293
  // A session whose native store has not changed since this rule last captured it is not reloaded.
@@ -322,6 +319,50 @@ export class TeamsConnector {
322
319
  this.pruneNativeCache(discovered);
323
320
  this.rotateFollowers(followCandidates);
324
321
  }
322
+ /**
323
+ * A root's session catalog for this pass. Every FULL_DISCOVERY_MS the whole catalog is read again (what catches a
324
+ * session that went away); between, only sessions updated since the last read (with a margin for clock and write
325
+ * order) are read and merged into the held catalog. A full read every 15 s was two to three 2,048-row pages, each a
326
+ * full scan in the harness and 8 MB parsed on this daemon's thread, and it kept every door waiting (t_23e47b73).
327
+ * Answers { descriptors, fresh }: the catalog, and the keys this pass read.
328
+ */
329
+ async catalogOf(discoveryKey,query){
330
+ // Between full reads a held session's turn comes from the activity subscription (recordTurn), not from this read.
331
+ this.catalogs??=new Map();
332
+ const now=this.clock.nowMs(),held=this.catalogs.get(discoveryKey),full=!held||now-held.fullAt>=FULL_DISCOVERY_MS;
333
+ const read=[];
334
+ let cursor;
335
+ do {
336
+ // A broad root enrolled before rules covered subfolders keeps its exact-folder discovery.
337
+ const page=await this.core.discover({...query,limit:2048,...(full?{}:{updated_after_ms:Math.max(0,held.readAt-CATALOG_MARGIN_MS)}),...(cursor?{cursor}:{})});
338
+ read.push(...(page.sessions??page.entries??[]));
339
+ cursor=page.next_cursor??page.nextCursor??null;
340
+ } while(cursor);
341
+ const keyOf=descriptor=>descriptor.locator?`${descriptor.locator.harness}:${descriptor.locator.session_id}`:null;
342
+ const catalog=full?new Map():held.catalog;
343
+ const fresh=new Set();
344
+ for(const descriptor of read){const key=keyOf(descriptor);if(!key)continue;catalog.set(key,descriptor);fresh.add(key);}
345
+ this.catalogs.set(discoveryKey,{catalog,readAt:now,fullAt:full?now:held.fullAt});
346
+ // The held catalog keeps discovery's order: the most recently updated first.
347
+ const descriptors=[...catalog.values()].sort((a,b)=>(b.updated_at_ms??0)-(a.updated_at_ms??0));
348
+ return {descriptors,fresh};
349
+ }
350
+ /**
351
+ * A session's turn as its activity subscription reports a change: what keeps the turn current between full reads,
352
+ * for the changes a session's transcript never shows (an approval prompt, the process exiting). The report the
353
+ * heartbeat publishes follows it at once.
354
+ */
355
+ recordTurn(activity){
356
+ const turn=activity?.turn;
357
+ if(!activity?.harness||!activity.session_id||typeof turn!=='string'||!turn)return;
358
+ this.turnStates??=new Map();
359
+ const key=`${activity.harness}:${activity.session_id}`,held=this.turnStates.get(key);
360
+ if(held?.turn===turn)return;
361
+ const since=this.clock.nowMs();
362
+ this.turnStates.set(key,{turn,since});
363
+ const reported=this.reportedActivity?.find(row=>row.harness===activity.harness&&row.native_id===activity.session_id);
364
+ if(reported){reported.turn=turn;reported.since=since;}
365
+ }
325
366
  recordActivityBinding(activity){
326
367
  if(!activity?.harness||!activity.session_id)return;
327
368
  const key=`${activity.harness}:${activity.session_id}`,id=`activity_${hash(`${this.machineId}:${this.state.incarnation_id}:${key}`).slice(0,32)}`;
@@ -338,7 +379,7 @@ export class TeamsConnector {
338
379
  if(fingerprint===this.activityFingerprint)return;
339
380
  const opened=await this.core.subscribeSessionActivity(locators),previous=this.activitySubscription;
340
381
  this.activitySubscription=opened.subscription;this.activityFingerprint=fingerprint;
341
- for(const activity of opened.initial??[])this.recordActivityBinding(activity);
382
+ for(const activity of opened.initial??[]){this.recordActivityBinding(activity);this.recordTurn(activity);}
342
383
  if(previous)await this.core.unsubscribeSessionActivity(previous);
343
384
  }
344
385
  captureViewedFiles(){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
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",