@volter/supercode-teams 0.2.44 → 0.2.46

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.
@@ -410,7 +410,8 @@ export class TeamsConnector {
410
410
  return work;
411
411
  }
412
412
  // Who typed a user turn that came through a door here (a Room member through the session channel): the daemon
413
- // notes it as it types the turn, and every capture carries it as `source` on the user message holding that text.
413
+ // notes it as it types the turn, and every capture carries it in the metadata of the user message holding that
414
+ // text (source_via, source_principal_id, source_name: the contract's per-message annotations are strings).
414
415
  noteTurn({harness,session,text,at,source}){
415
416
  const key=`${harness}:${session}`,now=this.clock.nowMs(),notes=this.state.turn_notes??={};
416
417
  notes[key]=[...(notes[key]??[]).filter(note=>now-note.at<TURN_NOTE_MS),{text:String(text).trim(),at,source}];
@@ -425,8 +426,9 @@ export class TeamsConnector {
425
426
  if(found){(this.state.turn_labels??={})[key]=labels;if(notes.length)this.state.turn_notes[key]=notes;else delete this.state.turn_notes[key];this.save();}
426
427
  messages.forEach((message,index)=>{
427
428
  const source=labels[message.native_key];if(!source)return;
428
- message.value={...message.value,source};
429
- const entry=entries[index];if(entry.value)entry.value={...entry.value,source};else entry.source=source;
429
+ const annotations=Object.fromEntries([['source_via',source.via],['source_principal_id',source.from?.principalId],['source_name',source.from?.name]].filter(([,item])=>typeof item==='string'&&item));
430
+ message.value={...message.value,metadata:{...message.value.metadata,...annotations}};
431
+ const entry=entries[index];if(entry.value)entry.value={...entry.value,metadata:{...entry.value.metadata,...annotations}};else entry.metadata={...entry.metadata,...annotations};
430
432
  });
431
433
  }
432
434
  largeSession({locator}){
@@ -444,10 +446,14 @@ export class TeamsConnector {
444
446
  const [key,options]=this.largeQueue.entries().next().value;this.largeQueue.delete(key);
445
447
  try{
446
448
  const artifactBefore=statSync(realpathSync(options.locator.storage.path));
447
- this.largeCaptures??=new Map();const last=this.largeCaptures.get(key);
448
- if(last!=null&&this.clock.nowMs()-last<LARGE_CAPTURE_EVERY_MS)continue;
449
- this.largeCaptures.set(key,this.clock.nowMs());
449
+ // A session is loaded again no sooner than ten times its last load took (a 0.7 s load: every 7 s; a load
450
+ // that takes minutes: every LARGE_CAPTURE_EVERY_MS), so a live session streams while the lane stays light.
451
+ // A change that arrives sooner is captured when its wait ends.
452
+ 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;
453
+ 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;}
454
+ const started=this.clock.nowMs();
450
455
  const loaded=await this.core.session(options.locator).load({timeoutMs:Math.max(30000,Math.ceil(artifactBefore.size/(1024*1024))*1000)});
456
+ this.largeCaptures.set(key,{at:started,ms:this.clock.nowMs()-started});
451
457
  await this.capture({...options,preloaded:{loaded,artifactBefore}});
452
458
  }catch(error){
453
459
  if(this.abort.signal.aborted)return;
@@ -463,8 +469,7 @@ export class TeamsConnector {
463
469
  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;
464
470
  const artifactSource=locator?.storage?.kind==='file'&&existsSync(locator.storage.path)?realpathSync(locator.storage.path):null;
465
471
  const artifactBefore=preloaded?preloaded.artifactBefore:artifactSource?statSync(artifactSource):null;
466
- const large=(artifactBefore?.size??0)>=LARGE_CAPTURE_BYTES,sessionKey=`${harness}:${nativeSessionId}`;
467
- 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());}
472
+ const large=(artifactBefore?.size??0)>=LARGE_CAPTURE_BYTES;
468
473
  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;
469
474
  const key=`${harness}:${nativeSessionId}`,previous=this.state.origins[key]??null;
470
475
  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};});
@@ -622,6 +627,6 @@ export class TeamsConnector {
622
627
  if(!Array.isArray(configuration?.rules))return false;
623
628
  return !configuration.rules.some(rule=>rule.id===item.begin.rule_id&&rule.revision===item.begin.rule_revision&&rule.state==='active');
624
629
  }
625
- async flushOutbox(){let latest=null;while(this.state.outbox.length){const item=this.state.outbox[0];let receipt;this.dropLostArtifacts(item);try{receipt=await this.sendCapture(item);}catch(error){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.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();if(item.artifacts?.length)this.pruneSpill();latest=receipt;}return latest;}
630
+ async flushOutbox(){let latest=null;while(this.state.outbox.length){const item=this.state.outbox[0];let receipt;this.dropLostArtifacts(item);try{receipt=await this.sendCapture(item);}catch(error){if(error.status===400&&error.code==='invalid_request'){this.lastError=error;process.stderr.write(`supercode teams: dropped a queued capture of ${item.begin.harness} session ${item.begin.native_session_id} the server refused 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.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();if(item.artifacts?.length)this.pruneSpill();latest=receipt;}return latest;}
626
631
  stop(){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();}
627
632
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.44",
3
+ "version": "0.2.46",
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",
@@ -243,7 +243,10 @@ async function sourceConfig(api,res,auth,id,url,requestId){if(url.searchParams.g
243
243
  async function sourceAuth(api,auth,id){if(!auth||!['machine','ingest'].includes(auth.kind)||auth.source_id!==id)fail(403,'forbidden','source credential required');const s=await api.store.one('SELECT * FROM sources WHERE team_id=? AND id=?',[auth.team_id,id]);if(!s)fail(404,'not_found','not found');if(s.state==='revoked')fail(403,'source_revoked','source revoked');if(s.state==='paused')fail(409,'source_paused','source paused');if(auth.kind==='machine'){const machine=await api.store.one('SELECT 1 ok FROM machines WHERE team_id=? AND id=? AND revoked_at IS NULL',[auth.team_id,auth.machine_id]);if(!machine||s.machine_id!==auth.machine_id)fail(403,'forbidden','machine enrollment is not active');}return s;}
244
244
  async function expireStaging(api,teamId,sourceId,id,table,row){if(!row||row.state&&row.state!=='staging'||row.committed===1||row.created_at+86400000>api.clock.nowMs())return false;await api.store.transaction([{sql:'INSERT OR REPLACE INTO capture_expiries(team_id,source_id,id,expired_at) VALUES (?,?,?,?)',params:[teamId,sourceId,id,api.clock.nowMs()]},{sql:`DELETE FROM ${table} WHERE team_id=? AND id=? AND source_id=?`,params:[teamId,id,sourceId]}]);return true;}
245
245
  async function captureBegin(api,req,res,auth,id,requestId){const source=await sourceAuth(api,auth,id);const b=await parseJson(req);validateCaptureBegin(b);if(source.kind==='connector'){if(b.rule_id===null)fail(403,'forbidden','connector capture requires an active sync rule');const rule=await api.store.one('SELECT * FROM sync_rules WHERE team_id=? AND id=? AND source_id=? AND machine_id=?',[auth.team_id,b.rule_id,id,auth.machine_id]);if(!rule||rule.state!=='active'||rule.revision!==b.rule_revision)fail(403,'forbidden','sync rule is absent, inactive, or stale');if(b.visibility!==rule.visibility)fail(400,'invalid_request','capture visibility differs from sync rule');}else if(b.visibility!==source.visibility)fail(400,'invalid_request','capture visibility differs from source policy');const expired=await api.store.one('SELECT id FROM capture_expiries WHERE team_id=? AND source_id=? AND id=?',[auth.team_id,id,b.capture_id]);if(expired)fail(409,'resync_required','capture reservation expired');const existing=await api.store.one('SELECT * FROM captures WHERE team_id=? AND id=?',[auth.team_id,b.capture_id]);if(await expireStaging(api,auth.team_id,id,b.capture_id,'captures',existing))fail(409,'resync_required','capture reservation expired');if(existing){if(existing.begin_json!==canonicalJson(b))fail(409,'capture_conflict','capture id reused with different request');if(existing.state==='committed')return send(res,200,result(api,auth,{state:'committed',receipt:JSON.parse(existing.receipt_json)}),requestId);const next=(await api.store.one('SELECT count(*) n FROM capture_parts WHERE team_id=? AND capture_id=?',[auth.team_id,b.capture_id])).n;return send(res,200,result(api,auth,{state:'staging',capture_id:b.capture_id,next_part:next}),requestId);}
246
- const origin=await api.store.one('SELECT * FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=?',[auth.team_id,id,b.harness,b.native_session_id]);if(origin?.deleted_at!=null)fail(410,'session_deleted','session deleted');const expected=origin?{generation:origin.generation,sequence:origin.sequence+1,digest:origin.digest}:{generation:1,sequence:1,digest:null};if(b.generation!==expected.generation||b.sequence!==expected.sequence||b.previous_digest!==expected.digest)fail(409,'batch_gap','capture is not the next source revision',expected);const now=api.clock.nowMs(),response=result(api,auth,{state:'staging',capture_id:b.capture_id,next_part:0}),guards=[{sql:"SELECT 1 ok FROM credentials c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id LEFT JOIN machines m ON m.team_id=s.team_id AND m.id=c.machine_id LEFT JOIN sync_rules r ON r.team_id=s.team_id AND r.id=? WHERE c.id=? AND c.team_id=? AND c.source_id=? AND c.revoked_at IS NULL AND (c.expires_at IS NULL OR c.expires_at>?) AND s.state='active' AND ((s.kind!='connector' AND s.visibility=?) OR (s.kind='connector' AND c.kind='machine' AND m.id=c.machine_id AND m.revoked_at IS NULL AND r.source_id=s.id AND r.machine_id=m.id AND r.state='active' AND r.revision=? AND r.visibility=?))",params:[b.rule_id,auth.credential_id,auth.team_id,id,now,b.visibility,b.rule_revision,b.visibility],code:'forbidden',status:403,message:'source authorization changed'},{sql:"SELECT 1 ok WHERE NOT EXISTS (SELECT 1 FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND deleted_at IS NOT NULL) AND ((?=1 AND ?=1 AND ? IS NULL AND NOT EXISTS (SELECT 1 FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=?)) OR EXISTS (SELECT 1 FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND generation=? AND sequence=?-1 AND digest IS ?))",params:[auth.team_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.previous_digest,auth.team_id,id,b.harness,b.native_session_id,auth.team_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.previous_digest],code:'batch_gap',status:409,message:'capture is not the next source revision'}];await guardedMutation(api,res,auth,guards,[{sql:"INSERT INTO captures(team_id,id,source_id,harness,native_id,generation,sequence,previous_digest,state,begin_json,created_at) VALUES (?,?,?,?,?,?,?,?, 'staging',?,?)",params:[auth.team_id,b.capture_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.previous_digest,canonicalJson(b),now]}],{status:201,body:response,targetId:b.capture_id});send(res,201,response,requestId);}
246
+ const origin=await api.store.one('SELECT * FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=?',[auth.team_id,id,b.harness,b.native_session_id]);if(origin?.deleted_at!=null)fail(410,'session_deleted','session deleted');const expected=origin?{generation:origin.generation,sequence:origin.sequence+1,digest:origin.digest}:{generation:1,sequence:1,digest:null};if(b.generation!==expected.generation||b.sequence!==expected.sequence||b.previous_digest!==expected.digest)fail(409,'batch_gap','capture is not the next source revision',expected);const now=api.clock.nowMs(),response=result(api,auth,{state:'staging',capture_id:b.capture_id,next_part:0}),guards=[{sql:"SELECT 1 ok FROM credentials c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id LEFT JOIN machines m ON m.team_id=s.team_id AND m.id=c.machine_id LEFT JOIN sync_rules r ON r.team_id=s.team_id AND r.id=? WHERE c.id=? AND c.team_id=? AND c.source_id=? AND c.revoked_at IS NULL AND (c.expires_at IS NULL OR c.expires_at>?) AND s.state='active' AND ((s.kind!='connector' AND s.visibility=?) OR (s.kind='connector' AND c.kind='machine' AND m.id=c.machine_id AND m.revoked_at IS NULL AND r.source_id=s.id AND r.machine_id=m.id AND r.state='active' AND r.revision=? AND r.visibility=?))",params:[b.rule_id,auth.credential_id,auth.team_id,id,now,b.visibility,b.rule_revision,b.visibility],code:'forbidden',status:403,message:'source authorization changed'},{sql:"SELECT 1 ok WHERE NOT EXISTS (SELECT 1 FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND deleted_at IS NOT NULL) AND ((?=1 AND ?=1 AND ? IS NULL AND NOT EXISTS (SELECT 1 FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=?)) OR EXISTS (SELECT 1 FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND generation=? AND sequence=?-1 AND digest IS ?))",params:[auth.team_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.previous_digest,auth.team_id,id,b.harness,b.native_session_id,auth.team_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.previous_digest],code:'batch_gap',status:409,message:'capture is not the next source revision'}];// A reservation this source abandoned for the same revision (its connector restarted, or dropped the capture,
247
+ // before committing) is superseded by the new begin, never left to refuse it until the reservation expires.
248
+ const abandoned="FROM captures WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND generation=? AND sequence=? AND state='staging' AND id<>?",abandonedParams=[auth.team_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.capture_id];
249
+ await guardedMutation(api,res,auth,guards,[{sql:`DELETE FROM capture_parts WHERE team_id=? AND capture_id IN (SELECT id ${abandoned})`,params:[auth.team_id,...abandonedParams]},{sql:`DELETE ${abandoned}`,params:abandonedParams},{sql:"INSERT INTO captures(team_id,id,source_id,harness,native_id,generation,sequence,previous_digest,state,begin_json,created_at) VALUES (?,?,?,?,?,?,?,?, 'staging',?,?)",params:[auth.team_id,b.capture_id,id,b.harness,b.native_session_id,b.generation,b.sequence,b.previous_digest,canonicalJson(b),now]}],{status:201,body:response,targetId:b.capture_id});send(res,201,response,requestId);}
247
250
  async function capturePart(api,req,res,auth,sourceId,captureId,index,requestId){await sourceAuth(api,auth,sourceId);const b=await parseJson(req);validateCapturePart(b);if(b.capture_id!==captureId||b.index!==index)fail(400,'invalid_request','capture path/body mismatch');const cap=await api.store.one('SELECT * FROM captures WHERE team_id=? AND id=? AND source_id=?',[auth.team_id,captureId,sourceId]);if(await expireStaging(api,auth.team_id,sourceId,captureId,'captures',cap))fail(409,'resync_required','capture reservation expired');if(!cap||cap.state!=='staging')fail(404,'not_found','capture not staging');const payload=canonicalJson(b),digest=sha256(payload),old=await api.store.one('SELECT * FROM capture_parts WHERE team_id=? AND capture_id=? AND part_index=?',[auth.team_id,captureId,index]);if(old){if(old.digest!==digest)fail(409,'capture_conflict','part replay differs');return send(res,200,result(api,auth,{next_part:index+1}),requestId);}const count=(await api.store.one('SELECT count(*) n FROM capture_parts WHERE team_id=? AND capture_id=?',[auth.team_id,captureId])).n;if(index!==count)fail(409,'batch_gap','capture parts must be contiguous');const now=api.clock.nowMs(),response=result(api,auth,{next_part:index+1}),guards=[{sql:"SELECT 1 ok FROM credentials c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id JOIN captures cap ON cap.team_id=s.team_id AND cap.source_id=s.id WHERE c.id=? AND c.source_id=? AND c.revoked_at IS NULL AND (c.expires_at IS NULL OR c.expires_at>?) AND s.state='active' AND cap.id=? AND cap.state='staging' AND cap.created_at+86400000>? AND (SELECT count(*) FROM capture_parts WHERE team_id=cap.team_id AND capture_id=cap.id)=?",params:[auth.credential_id,sourceId,now,captureId,now,index],code:'batch_gap',status:409,message:'capture authorization or checkpoint changed'}];await guardedMutation(api,res,auth,guards,[{sql:'INSERT INTO capture_parts(team_id,capture_id,part_index,digest,payload_json) VALUES (?,?,?,?,?)',params:[auth.team_id,captureId,index,digest,payload]}],{status:200,body:response,targetId:captureId});send(res,200,response,requestId);}
248
251
  async function artifactChunk(api,req,res,auth,sourceId,uploadId,index,requestId){
249
252
  await sourceAuth(api,auth,sourceId);