@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 +3 -1
- package/connector/index.mjs +58 -17
- package/package.json +1 -1
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
|
-
|
|
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
|
|
package/connector/index.mjs
CHANGED
|
@@ -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,(
|
|
274
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|