@volter/supercode-teams 0.2.41 → 0.2.43

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.
@@ -401,20 +401,48 @@ export class TeamsConnector {
401
401
  this.save();
402
402
  }
403
403
  capture(options) {
404
+ if(!options.preloaded&&this.largeSession(options))return Promise.resolve(this.captureLarge(options));
404
405
  const work=this.work.then(()=>this.captureSerial(options)).then(receipt=>{this.clearSessionProblem(options.harness,options.nativeSessionId);return receipt;});
405
406
  this.work=work.catch(()=>{});
406
407
  return work;
407
408
  }
408
- async captureSerial({locator,descriptor=null,harness,nativeSessionId,visibility='restricted',ruleId=null,ruleRevision=null,coverage={completeness:'complete',reasons:[],missing:[]},backfill='all',rootPath=null,configuration=null,stamp}) {
409
+ largeSession({locator}){
410
+ try{return locator?.storage?.kind==='file'&&statSync(realpathSync(locator.storage.path)).size>=LARGE_CAPTURE_BYTES;}catch{return false;}
411
+ }
412
+ // A large transcript's load takes seconds to minutes. It runs in a lane of its own, outside the serial capture
413
+ // queue, so no small session's capture waits behind it; only the capture of what it loaded joins the queue.
414
+ captureLarge(options){
415
+ (this.largeQueue??=new Map()).set(`${options.harness}:${options.nativeSessionId}`,options);
416
+ if(!this.largeLane)this.largeLane=this.runLargeLane().finally(()=>{this.largeLane=null;});
417
+ return null;
418
+ }
419
+ async runLargeLane(){
420
+ while(this.largeQueue.size&&!this.abort.signal.aborted){
421
+ const [key,options]=this.largeQueue.entries().next().value;this.largeQueue.delete(key);
422
+ try{
423
+ const artifactBefore=statSync(realpathSync(options.locator.storage.path));
424
+ this.largeCaptures??=new Map();const last=this.largeCaptures.get(key);
425
+ if(last!=null&&this.clock.nowMs()-last<LARGE_CAPTURE_EVERY_MS)continue;
426
+ this.largeCaptures.set(key,this.clock.nowMs());
427
+ const loaded=await this.core.session(options.locator).load({timeoutMs:Math.max(30000,Math.ceil(artifactBefore.size/(1024*1024))*1000)});
428
+ await this.capture({...options,preloaded:{loaded,artifactBefore}});
429
+ }catch(error){
430
+ if(this.abort.signal.aborted)return;
431
+ this.noteSessionProblem(options,error);
432
+ process.stderr.write(`supercode teams: ${options.harness} session ${options.nativeSessionId} not synced: ${error.message}\n`);
433
+ }
434
+ }
435
+ }
436
+ async captureSerial({locator,descriptor=null,harness,nativeSessionId,visibility='restricted',ruleId=null,ruleRevision=null,coverage={completeness:'complete',reasons:[],missing:[]},backfill='all',rootPath=null,configuration=null,stamp,preloaded=null}) {
409
437
  if(this.state.outbox.length)await this.flushSerial();
410
438
  // Discovery hands each session the configuration its beat fetched; a follow event fetches its own.
411
439
  if(stamp===undefined)stamp=this.sessionStamp({locator,descriptor});
412
440
  if(!configuration)configuration=await this.configuration();const rule=configuration.rules.find(item=>item.id===ruleId&&item.revision===ruleRevision&&item.state==='active');if(!rule)throw Object.assign(new Error('sync rule is absent, inactive, or stale'),{code:'forbidden'});visibility=rule.visibility;
413
441
  const artifactSource=locator?.storage?.kind==='file'&&existsSync(locator.storage.path)?realpathSync(locator.storage.path):null;
414
- const artifactBefore=artifactSource?statSync(artifactSource):null;
442
+ const artifactBefore=preloaded?preloaded.artifactBefore:artifactSource?statSync(artifactSource):null;
415
443
  const large=(artifactBefore?.size??0)>=LARGE_CAPTURE_BYTES,sessionKey=`${harness}:${nativeSessionId}`;
416
- if(large){this.largeCaptures??=new Map();const last=this.largeCaptures.get(sessionKey);if(last!=null&&this.clock.nowMs()-last<LARGE_CAPTURE_EVERY_MS)return null;this.largeCaptures.set(sessionKey,this.clock.nowMs());}
417
- const loaded=await this.core.session(locator).load(large?{timeoutMs:Math.max(30000,Math.ceil(artifactBefore.size/(1024*1024))*1000)}:undefined),configuredBackfill=rule.backfill??backfill??'all',backfillPolicy=typeof configuredBackfill==='string'?{mode:configuredBackfill,since:null}:configuredBackfill;
444
+ if(large&&!preloaded){this.largeCaptures??=new Map();const last=this.largeCaptures.get(sessionKey);if(last!=null&&this.clock.nowMs()-last<LARGE_CAPTURE_EVERY_MS)return null;this.largeCaptures.set(sessionKey,this.clock.nowMs());}
445
+ const loaded=preloaded?preloaded.loaded:await this.core.session(locator).load(large?{timeoutMs:Math.max(30000,Math.ceil(artifactBefore.size/(1024*1024))*1000)}:undefined),configuredBackfill=rule.backfill??backfill??'all',backfillPolicy=typeof configuredBackfill==='string'?{mode:configuredBackfill,since:null}:configuredBackfill;
418
446
  const key=`${harness}:${nativeSessionId}`,previous=this.state.origins[key]??null;
419
447
  let messages=(loaded.messages??loaded.turns??[]).map((entry,index)=>{const value=entry.value??Object.fromEntries(Object.entries(entry).filter(([key])=>!['id','native_key','recorded_at','timestamp'].includes(key)));return{native_key:entry.id??entry.native_key??String(index),recorded_at:recordedAt(entry),value};});
420
448
  // A session published without its native bundle is captured again, with the same messages, once the bundle can be built.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.41",
3
+ "version": "0.2.43",
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",
@@ -100,7 +100,9 @@ const managerGuard=(auth,sessionId,now,{governance=false}={})=>({
100
100
  params:[auth.principal_id,auth.team_id,sessionId,governance?1:0,auth.principal_id,now,auth.principal_id,auth.principal_id],
101
101
  code:'not_found',status:404,message:'not found'
102
102
  });
103
- async function resolve(api,auth,ref){let rows;if(ref.length>=8)rows=await api.store.query('SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? AND s.id LIKE ?',[auth.team_id,`${ref}%`]);else rows=[];const visible=[];for(let r of rows){r=await canonicalSession(api,auth.team_id,r);r=await api.store.one('SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? AND s.id=?',[auth.team_id,r.id]);if(r&&await canReadMerged(api,auth,r)&&!visible.some(x=>x.id===r.id))visible.push(r);}if(visible.length>1)fail(400,'ambiguous_reference','ambiguous session reference');if(!visible.length)fail(404,'not_found','not found');return visible[0];}
103
+ // A session is named by its id, a prefix of it (eight characters or more), or `harness:native id` (its origin on a
104
+ // machine). A whole id is matched exactly: the platform's LIKE refuses a pattern much longer than an id.
105
+ async function resolve(api,auth,ref){const head='SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? AND ',native=/^([a-z][a-z0-9-]*):(.+)$/.exec(ref);let rows;if(native)rows=await api.store.query(head+'s.id IN (SELECT session_id FROM origins WHERE team_id=? AND harness=? AND native_id=? AND deleted_at IS NULL)',[auth.team_id,auth.team_id,native[1],native[2]]);else if(ref.length>=36)rows=await api.store.query(head+'s.id=?',[auth.team_id,ref]);else if(ref.length>=8)rows=await api.store.query(head+'s.id LIKE ?',[auth.team_id,`${ref}%`]);else rows=[];const visible=[];for(let r of rows){r=await canonicalSession(api,auth.team_id,r);r=await api.store.one('SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? AND s.id=?',[auth.team_id,r.id]);if(r&&await canReadMerged(api,auth,r)&&!visible.some(x=>x.id===r.id))visible.push(r);}if(visible.length>1)fail(400,'ambiguous_reference','ambiguous session reference');if(!visible.length)fail(404,'not_found','not found');return visible[0];}
104
106
  function qobj(url){return Object.fromEntries([...url.searchParams].map(([k,v])=>[k,['limit','since','until','tail','revision'].includes(k)?Number(v):k==='include_children'?v==='true':v]));}
105
107
  function projection(row){return JSON.parse(row.descriptor_json);}
106
108
  async function readerProjection(api,auth,row){
@@ -36,9 +36,13 @@ export function createStoreHandlers({ all, get, run, tx }) {
36
36
  if(typeof ref!=='string'||ref.length<8)domainError(404,'not_found','not found');
37
37
  const groups=all('SELECT group_id FROM group_members WHERE team_id=? AND principal_id=?',[teamId,principalId]).map(row=>row.group_id);
38
38
  // A session is named by its id (or a prefix of it) or by the harness's own id for it (a native
39
- // session id such as a Claude Code session's uuid), which its origin records.
40
- let candidates=all('SELECT * FROM sessions WHERE team_id=? AND id LIKE ? AND deleted_at IS NULL',[teamId,`${ref}%`]);
41
- if(!candidates.length)candidates=all('SELECT DISTINCT s.* FROM sessions s JOIN origins o ON o.team_id=s.team_id AND o.session_id=s.id WHERE s.team_id=? AND o.native_id=? AND o.deleted_at IS NULL AND s.deleted_at IS NULL',[teamId,ref]);
39
+ // session id such as a Claude Code session's uuid, bare or as `harness:id`), which its origin records. Only a
40
+ // ref no longer than an id is a prefix: the platform's LIKE refuses a pattern much longer than one.
41
+ let candidates=ref.length<=36?all('SELECT * FROM sessions WHERE team_id=? AND id LIKE ? AND deleted_at IS NULL',[teamId,`${ref}%`]):[];
42
+ const native=/^([a-z][a-z0-9-]*):(.+)$/.exec(ref);
43
+ if(!candidates.length)candidates=native
44
+ ?all('SELECT DISTINCT s.* FROM sessions s JOIN origins o ON o.team_id=s.team_id AND o.session_id=s.id WHERE s.team_id=? AND o.harness=? AND o.native_id=? AND o.deleted_at IS NULL AND s.deleted_at IS NULL',[teamId,native[1],native[2]])
45
+ :all('SELECT DISTINCT s.* FROM sessions s JOIN origins o ON o.team_id=s.team_id AND o.session_id=s.id WHERE s.team_id=? AND o.native_id=? AND o.deleted_at IS NULL AND s.deleted_at IS NULL',[teamId,ref]);
42
46
  const visible=new Map();
43
47
  for(const candidate of candidates){
44
48
  let root=candidate;const seen=new Set();