@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.
- package/connector/index.mjs +14 -9
- package/package.json +1 -1
- package/server/harness-routes.mjs +4 -1
package/connector/index.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
429
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
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.
|
|
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'}]
|
|
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);
|