@volter/supercode-teams 0.2.56 → 0.2.58

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.
@@ -66,6 +66,9 @@ const NATIVE_IDLE_MS=60000,NATIVE_IDLE_BYTES=8*1024*1024;
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
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
+ // Each capture writes the session's whole transcript as a new version on the server, so a session is captured at
70
+ // most this often (a later change is captured when the wait ends): a busy fleet filled the server's database.
71
+ const CAPTURE_FLOOR_MS=5000,LARGE_CAPTURE_FLOOR_MS=20000;
69
72
  // A turn a Room member typed here waits this long to be found in its session's transcript.
70
73
  const TURN_NOTE_MS=24*3600000;
71
74
  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():'';
@@ -135,7 +138,7 @@ export class TeamsConnector {
135
138
  const heartbeat = () => {
136
139
  const next = beating.then(async () => {
137
140
  this.state.sequence += 1;
138
- await this.request('POST', `/machines/${this.machineId}/heartbeat`, {
141
+ const reply = await this.request('POST', `/machines/${this.machineId}/heartbeat`, {
139
142
  incarnation_id: this.state.incarnation_id, sequence: this.state.sequence,
140
143
  bindings: [...this.nativeEffects.published(), ...await bindings()],
141
144
  roots: this.localRoots.map(({id,machine_id,name,harnesses})=>({id,machine_id,name,harnesses})),
@@ -143,6 +146,8 @@ export class TeamsConnector {
143
146
  // What each synced session's harness is doing (its turn: working, idle, …), from the last discovery.
144
147
  ...(this.reportedActivity ? { activity: this.reportedActivity } : {}),
145
148
  });
149
+ // The sessions someone is following now (a viewer or channel open on them) skip the capture floors.
150
+ if (Array.isArray(reply?.watched)) this.watched = new Set(reply.watched.map(item => `${item.harness}:${item.native_id}`));
146
151
  this.save();
147
152
  });
148
153
  beating = next.catch(() => {});
@@ -409,6 +414,11 @@ export class TeamsConnector {
409
414
  async capture(options) {
410
415
  if(!options.preloaded&&this.largeSession(options))return this.captureLarge(options);
411
416
  if(!options.preloaded){
417
+ const key=`${options.harness}:${options.nativeSessionId}`,now=this.clock.nowMs();
418
+ this.lastCaptures??=new Map();this.captureRetry??=new Map();
419
+ const wait=this.watched?.has(key)?0:CAPTURE_FLOOR_MS-(now-(this.lastCaptures.get(key)??0));
420
+ if(wait>0){if(!this.captureRetry.has(key))this.captureRetry.set(key,setTimeout(()=>{this.captureRetry.delete(key);this.capture(options).catch(()=>{});},wait).unref?.());return null;}
421
+ this.lastCaptures.set(key,now);
412
422
  const {locator}=options,path=locator?.storage?.kind==='file'&&existsSync(locator.storage.path)?realpathSync(locator.storage.path):null;
413
423
  const artifactBefore=path?statSync(path):null;
414
424
  options={...options,preloaded:{loaded:await (options.live?this.liveCore:this.core).session(locator).load(),artifactBefore}};
@@ -465,8 +475,8 @@ export class TeamsConnector {
465
475
  const artifactBefore=statSync(realpathSync(options.locator.storage.path));
466
476
  // A session is loaded again no sooner than ten times its last load took (a 0.5 s load: every 5 s; a load
467
477
  // that takes minutes: every LARGE_CAPTURE_EVERY_MS), so a live session streams while the lanes stay light.
468
- // A change that arrives sooner is captured when its wait ends.
469
- 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;
478
+ // A change that arrives sooner is captured when its wait ends. A session someone is following has no wait.
479
+ this.largeCaptures??=new Map();const last=this.largeCaptures.get(key),wait=last&&!this.watched?.has(key)?Math.min(LARGE_CAPTURE_EVERY_MS,Math.max(LARGE_CAPTURE_FLOOR_MS,last.ms*10))-(this.clock.nowMs()-last.at):0;
470
480
  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;}
471
481
  started=this.clock.nowMs();
472
482
  const loaded=await lane.core.session(options.locator).load({timeoutMs:Math.max(30000,Math.ceil(artifactBefore.size/(1024*1024))*1000)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.56",
3
+ "version": "0.2.58",
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",
@@ -132,6 +132,10 @@ export async function handleSessionEvents({ api, req, res, auth, id, url, reques
132
132
  }
133
133
  });
134
134
  stream.cleanup(api.events.subscribe(`session:${auth.team_id}:${sessionId}`, enqueue));
135
+ // Who is watching this session live: its machine captures a watched session as fast as it changes (heartbeat).
136
+ const watchers = api.watchers ??= new Map(), watchKey = `${auth.team_id}:${sessionId}`;
137
+ watchers.set(watchKey, (watchers.get(watchKey) ?? 0) + 1);
138
+ stream.cleanup(() => { const left = (watchers.get(watchKey) ?? 1) - 1; if (left > 0) watchers.set(watchKey, left); else watchers.delete(watchKey); });
135
139
  stream.cleanup(api.events.subscribe(`team:${auth.team_id}`, event => { if (event.type === 'authorization_changed') enqueue(); }));
136
140
  // The snapshot pins its journal cursor atomically. Replay closes the snapshot/subscription gap.
137
141
  await enqueue();
package/worker/index.mjs CHANGED
@@ -119,7 +119,7 @@ export class TeamsServer extends DurableObject {
119
119
  await this.ready;
120
120
  const done = this.api.serverId ? await maintain(this.api).catch(() => null) : null;
121
121
  // A backlog (versions or parts left over) is worked again in seconds, not a minute.
122
- await this.ctx.storage.setAlarm(Date.now() + (done?.backlog ? 5_000 : MAINTAIN_MS));
122
+ await this.ctx.storage.setAlarm(Date.now() + (done?.backlog ? 3_000 : MAINTAIN_MS));
123
123
  }
124
124
  }
125
125