@volter/supercode-teams 0.2.66 → 0.2.68

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.
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync, renameSync, existsSync, chmodSync, realpathSync, statSync, openSync, closeSync, fsyncSync, unlinkSync, readSync, readdirSync } from 'node:fs';
2
2
  import { basename, dirname, join } from 'node:path';
3
3
  import { randomBytes, createHash } from 'node:crypto';
4
- import { canonicalJson } from '../contracts/index.mjs';
4
+ import { canonicalJson, carryDigest } from '../contracts/index.mjs';
5
5
  import { EffectConsumer } from './effects.mjs';
6
6
  import { NativeEffects } from './native-effects.mjs';
7
7
 
@@ -69,6 +69,8 @@ const LARGE_CAPTURE_BYTES=8*1024*1024,LARGE_CAPTURE_EVERY_MS=5*60000,NATIVE_MAX_
69
69
  // Each capture writes the session's whole transcript as a new version on the server, so a session is captured at
70
70
  // most this often (a later change is captured when the wait ends): a busy fleet filled the server's database.
71
71
  const CAPTURE_FLOOR_MS=5000,LARGE_CAPTURE_FLOOR_MS=20000;
72
+ // A capture carries its unchanged prefix (names it by digest instead of sending it) when it is at least this long.
73
+ const CARRY_MIN_MESSAGES=20;
72
74
  // A turn a Room member typed here waits this long to be found in its session's transcript.
73
75
  const TURN_NOTE_MS=24*3600000;
74
76
  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():'';
@@ -449,6 +451,20 @@ export class TeamsConnector {
449
451
  const entry=entries[index];if(entry.value)entry.value={...entry.value,metadata:{...entry.value.metadata,...annotations}};else entry.metadata={...entry.metadata,...annotations};
450
452
  });
451
453
  }
454
+ /**
455
+ * How many leading messages of this capture the server already holds from this source's last committed capture
456
+ * (same keys and digests at the same places), or 0: then the capture is sent whole. Short prefixes are not worth
457
+ * carrying; one message is always sent (a part holds at least one).
458
+ */
459
+ carriedPrefix(previous,messages,{ruleRevision,backfillMode,clearBaseline}){
460
+ if(!previous?.observed_keys?.length||previous.carry_refused||previous.last_rule_revision!==ruleRevision||previous.backfill_mode!==backfillMode)return 0;
461
+ // What the server holds is what was sent: the observed messages without a kept-back baseline.
462
+ const kept=previous.baseline&&!clearBaseline?new Set(previous.baseline):null;
463
+ const held=previous.observed_keys.map((key,index)=>[key,previous.observed_digests?.[index]]).filter(([key])=>!kept?.has(key));
464
+ const limit=Math.min(messages.length-1,held.length);let count=0;
465
+ while(count<limit&&messages[count].native_key===held[count][0]&&hash(canonicalJson(messages[count].value))===held[count][1])count++;
466
+ return count>=CARRY_MIN_MESSAGES?count:0;
467
+ }
452
468
  largeSession({locator}){
453
469
  try{return locator?.storage?.kind==='file'&&statSync(realpathSync(locator.storage.path)).size>=LARGE_CAPTURE_BYTES;}catch{return false;}
454
470
  }
@@ -544,7 +560,12 @@ export class TeamsConnector {
544
560
  if(!messages.length){this.recordStamp(key,ruleId,ruleRevision,backfillPolicy.mode,stamp);return null;}
545
561
  const captureId=`cap_${randomBytes(16).toString('hex')}`,generation=effectivePrevious?.generation??1,sequence=(effectivePrevious?.sequence??0)+1;
546
562
  const begin={native_session_id:nativeSessionId,harness,generation,sequence,capture_id:captureId,previous_digest:effectivePrevious?.digest??null,visibility,rule_id:ruleId,rule_revision:ruleRevision};
547
- const parts=[];for(let index=0;index<messages.length;index+=500)parts.push({capture_id:captureId,index:parts.length,messages:messages.slice(index,index+500)});
563
+ // Only what the server does not hold goes over the wire: the longest prefix this source last committed unchanged
564
+ // (the outbox is empty here, so that is the session's current version) is named by its digest, not sent again.
565
+ const carried=this.carriedPrefix(effectivePrevious,messages,{ruleRevision,backfillMode:backfillPolicy.mode,clearBaseline});
566
+ if(carried)begin.carry={count:carried,digest:carryDigest(messages.slice(0,carried))};
567
+ const sent=carried?messages.slice(carried):messages;
568
+ const parts=[];for(let index=0;index<sent.length;index+=500)parts.push({capture_id:captureId,index:parts.length,messages:sent.slice(index,index+500)});
548
569
  let artifacts=[];
549
570
  // Native stores normally live in harness homes outside the repository. The locator is
550
571
  // accepted only when it came from this configured core discovery, and raw bytes are safe
@@ -656,6 +677,6 @@ export class TeamsConnector {
656
677
  if(!Array.isArray(configuration?.rules))return false;
657
678
  return !configuration.rules.some(rule=>rule.id===item.begin.rule_id&&rule.revision===item.begin.rule_revision&&rule.state==='active');
658
679
  }
659
- 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;}
680
+ 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===409&&error.code==='carry_mismatch'){const key=`${item.begin.harness}:${item.begin.native_session_id}`;this.lastError=error;process.stderr.write(`supercode teams: the server does not hold what a capture of ${item.begin.harness} session ${item.begin.native_session_id} carried; it goes whole next time\n`);this.state.outbox.shift();if(this.state.origins[key])this.state.origins[key]={...this.state.origins[key],observed_keys:[],observed_digests:[],carry_refused:true};this.save();continue;}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.begin.carry)delete next.carry_refused;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;}
660
681
  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();}
661
682
  }
@@ -1,4 +1,4 @@
1
- import { ContractError, canonicalJson, validationFailure } from '@volter/teams/server/wire';
1
+ import { ContractError, canonicalJson, sha256, validationFailure } from '@volter/teams/server/wire';
2
2
  export { ContractError, canonicalJson, validationFailure };
3
3
  const ID = /^(srv|team|usr|svc|grp|ses|msg|src|mach|bind|cap|upload|grant|map|op|cont|inv|cred|req|root|inc|rule|att|evt|audit)_[0-9a-f]{32}$/;
4
4
  export const ERROR_CODES = Object.freeze(['invalid_request','ambiguous_reference','authentication_required','not_found','forbidden','unsupported','machine_offline','controller_busy','conflict','resync_required','revision_mismatch','idempotency_conflict','source_revoked','source_paused','session_deleted','batch_gap','capture_conflict','fidelity_unavailable','rate_limited','request_too_large','internal_error']);
@@ -33,5 +33,10 @@ export function validateNormalizedMessage(value) {
33
33
  for(const [key,item] of Object.entries(metadata))if(typeof item!=='string')throw new ContractError(`message.metadata.${key} must be a string`);
34
34
  return value;
35
35
  }
36
- export function validateCaptureBegin(value) { exact(value, ['native_session_id','harness','generation','sequence','capture_id','previous_digest','visibility','rule_id','rule_revision'], 'capture'); string(value.native_session_id, 'capture.native_session_id', {max:512}); string(value.harness, 'capture.harness'); integer(value.generation,'capture.generation',1); integer(value.sequence,'capture.sequence',1); validateId(value.capture_id,'capture.capture_id'); if (value.previous_digest !== null) string(value.previous_digest,'capture.previous_digest',{min:64,max:64}); member(value.visibility,['team','restricted'],'capture.visibility'); if (value.rule_id !== null) validateId(value.rule_id,'capture.rule_id'); if ((value.rule_id === null) !== (value.rule_revision === null)) throw new ContractError('rule_id and rule_revision must both be null or present'); if (value.rule_revision !== null) integer(value.rule_revision,'capture.rule_revision',1); return value; }
36
+ /**
37
+ * The digest of a capture's carried prefix: the messages its session's current version already holds at the first
38
+ * `count` places, which the capture names by this digest instead of sending again (begin.carry).
39
+ */
40
+ export function carryDigest(messages) { return sha256(canonicalJson(messages.map((m) => ({ native_key: m.native_key, recorded_at: m.recorded_at ?? null, value: m.value })))); }
41
+ export function validateCaptureBegin(value) { exact(value, ['native_session_id','harness','generation','sequence','capture_id','previous_digest','visibility','rule_id','rule_revision','carry'], 'capture'); if (value.carry !== undefined) { exact(value.carry, ['count','digest'], 'capture.carry'); integer(value.carry.count, 'capture.carry.count', 1); string(value.carry.digest, 'capture.carry.digest', {min:64,max:64}); } string(value.native_session_id, 'capture.native_session_id', {max:512}); string(value.harness, 'capture.harness'); integer(value.generation,'capture.generation',1); integer(value.sequence,'capture.sequence',1); validateId(value.capture_id,'capture.capture_id'); if (value.previous_digest !== null) string(value.previous_digest,'capture.previous_digest',{min:64,max:64}); member(value.visibility,['team','restricted'],'capture.visibility'); if (value.rule_id !== null) validateId(value.rule_id,'capture.rule_id'); if ((value.rule_id === null) !== (value.rule_revision === null)) throw new ContractError('rule_id and rule_revision must both be null or present'); if (value.rule_revision !== null) integer(value.rule_revision,'capture.rule_revision',1); return value; }
37
42
  export function validateCapturePart(value) { exact(value,['capture_id','index','messages'],'part'); validateId(value.capture_id,'part.capture_id'); integer(value.index,'part.index'); if (!Array.isArray(value.messages) || value.messages.length < 1 || value.messages.length > 500) throw new ContractError('part.messages must contain 1..500 messages'); for (const [i,m] of value.messages.entries()) { exact(m,['native_key','recorded_at','value'],`part.messages[${i}]`); string(m.native_key,`part.messages[${i}].native_key`,{max:512}); if (m.recorded_at !== null) integer(m.recorded_at,`part.messages[${i}].recorded_at`); validateNormalizedMessage(m.value); } return value; }
package/machine.mjs CHANGED
@@ -118,7 +118,7 @@ function launchCommand(program, args) {
118
118
  */
119
119
  /** How a pane the host keeps after its program ended (`dead`) ended, as `paneExit` says it. */
120
120
  function deadPaneExit(session) {
121
- return Number.isInteger(session.exitCode) ? { failed: session.exitCode !== 0, how: `status ${session.exitCode}` } : { failed: true, how: 'failed' };
121
+ return Number.isInteger(session.exitCode) ? { failed: session.exitCode !== 0, how: `status ${session.exitCode}` } : { failed: true, how: 'its exit status unknown' };
122
122
  }
123
123
 
124
124
  /** How often a pane whose launcher asked to be told of its end is looked for. */
@@ -155,7 +155,7 @@ export class TeamsMachine extends MachineCore {
155
155
  // Windows has no tmux: its panes live in the machine's pane server, a process of their own that outlives this
156
156
  // daemon (a restart or an upgrade never ends a pane), as tmux panes live in tmux's server.
157
157
  if (process.platform === 'win32' && !terminal.PaneServerHost) throw new Error('panes on Windows need a newer @volter/supercode-terminal (one with PaneServerHost)');
158
- this.host = process.platform === 'win32' ? new terminal.PaneServerHost({ pipe: `\\\\.\\pipe\\supercode-panes-${fingerprint(this.identity.publicKeyPem)}` }) : new TmuxTerminalHost({
158
+ this.host = process.platform === 'win32' ? new terminal.PaneServerHost({ pipe: `\\\\.\\pipe\\supercode-panes-${fingerprint(this.identity.publicKeyPem)}`, runtimeDir: join(this.home, 'pane-server') }) : new TmuxTerminalHost({
159
159
  // Stable owner id so panes this daemon launched stay "owned" across restarts.
160
160
  ownerId: `supercode-teams-${fingerprint(this.identity.publicKeyPem)}`,
161
161
  ...(tmuxSocket ? { socket: tmuxSocket } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.66",
3
+ "version": "0.2.68",
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",
@@ -7,7 +7,7 @@
7
7
  // idempotency check, and last among a team's routes. Each hook returns true when it
8
8
  // answered the request.
9
9
  import { bearer, hashToken, canRead, canManage, isAdmin } from '@volter/teams/server/auth';
10
- import { validateListQuery, validateSearchQuery, validateReadQuery, validateCaptureBegin, validateCapturePart, canonicalJson } from '../contracts/index.mjs';
10
+ import { validateListQuery, validateSearchQuery, validateReadQuery, validateCaptureBegin, validateCapturePart, canonicalJson, carryDigest } from '../contracts/index.mjs';
11
11
  import { normalizeSearch, textFromContent, cursorHash, cursorToken } from './catalog.mjs';
12
12
  import { captureDigest, captureMessages, handleOtlpRoute, sha256, validateArtifactName } from './ingest.mjs';
13
13
  import { handleCatalogEvents, handleSessionEvents, sessionNames } from './event-routes.mjs';
@@ -288,10 +288,22 @@ async function artifactChunk(api,req,res,auth,sourceId,uploadId,index,requestId)
288
288
  ],{status:200,body:response,targetId:uploadId});
289
289
  send(res,200,response,requestId);
290
290
  }
291
- async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){const source=await sourceAuth(api,auth,sourceId);const commit=await parseJson(req);exact(commit,['capture_id','parts','digest','title','repository','reported_user','recorded_activity_at','parent_native_id','coverage','artifacts','native_bundle']);if(commit.capture_id!==captureId||!Number.isSafeInteger(commit.parts)||commit.parts<1)fail(400,'invalid_request','invalid commit');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)fail(404,'not_found','capture not found');if(cap.state==='committed'){const receipt=JSON.parse(cap.receipt_json);if(receipt.digest!==commit.digest)fail(409,'capture_conflict','commit replay differs');receipt.duplicate=true;return send(res,200,result(api,auth,receipt),requestId);}const rows=await api.store.query('SELECT * FROM capture_parts WHERE team_id=? AND capture_id=? ORDER BY part_index',[auth.team_id,captureId]);if(rows.length!==commit.parts)fail(409,'batch_gap','capture parts incomplete');const begin=JSON.parse(cap.begin_json);if(source.kind==='connector'){const rule=await api.store.one('SELECT * FROM sync_rules WHERE team_id=? AND id=? AND source_id=? AND machine_id=?',[auth.team_id,begin.rule_id,sourceId,auth.machine_id]);if(!rule||rule.state!=='active'||rule.revision!==begin.rule_revision||rule.visibility!==begin.visibility)fail(rule?.state==='paused'?409:403,rule?.state==='paused'?'source_paused':'forbidden','sync rule changed before commit');}const parts=rows.map(x=>JSON.parse(x.payload_json));if(captureDigest(begin,parts,commit)!==commit.digest)fail(409,'capture_conflict','capture digest differs');const messages=captureMessages(parts);if(messages.length>100000)fail(413,'request_too_large','capture contains too many messages');
291
+ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){const source=await sourceAuth(api,auth,sourceId);const commit=await parseJson(req);exact(commit,['capture_id','parts','digest','title','repository','reported_user','recorded_activity_at','parent_native_id','coverage','artifacts','native_bundle']);if(commit.capture_id!==captureId||!Number.isSafeInteger(commit.parts)||commit.parts<1)fail(400,'invalid_request','invalid commit');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)fail(404,'not_found','capture not found');if(cap.state==='committed'){const receipt=JSON.parse(cap.receipt_json);if(receipt.digest!==commit.digest)fail(409,'capture_conflict','commit replay differs');receipt.duplicate=true;return send(res,200,result(api,auth,receipt),requestId);}const rows=await api.store.query('SELECT * FROM capture_parts WHERE team_id=? AND capture_id=? ORDER BY part_index',[auth.team_id,captureId]);if(rows.length!==commit.parts)fail(409,'batch_gap','capture parts incomplete');const begin=JSON.parse(cap.begin_json);if(source.kind==='connector'){const rule=await api.store.one('SELECT * FROM sync_rules WHERE team_id=? AND id=? AND source_id=? AND machine_id=?',[auth.team_id,begin.rule_id,sourceId,auth.machine_id]);if(!rule||rule.state!=='active'||rule.revision!==begin.rule_revision||rule.visibility!==begin.visibility)fail(rule?.state==='paused'?409:403,rule?.state==='paused'?'source_paused':'forbidden','sync rule changed before commit');}const parts=rows.map(x=>JSON.parse(x.payload_json));if(captureDigest(begin,parts,commit)!==commit.digest)fail(409,'capture_conflict','capture digest differs');let messages=captureMessages(parts);if(messages.length>100000)fail(413,'request_too_large','capture contains too many messages');
292
292
  for(const manifest of commit.artifacts){validateArtifactName(manifest.relative_name);const upload=await api.store.one('SELECT source_id FROM uploads WHERE team_id=? AND id=?',[auth.team_id,manifest.upload_id]);if(!upload||upload.source_id!==sourceId)fail(409,'capture_conflict','artifact upload belongs to another source');const chunks=await api.store.query('SELECT c.* FROM artifact_chunks c JOIN uploads u ON u.team_id=c.team_id AND u.id=c.upload_id WHERE c.team_id=? AND c.upload_id=? AND u.source_id=? ORDER BY c.chunk_index',[auth.team_id,manifest.upload_id,sourceId]);const bytes=Buffer.concat(chunks.map(x=>x.bytes));if(chunks.length!==manifest.chunks||bytes.length!==manifest.byte_length||sha256(bytes)!==manifest.sha256)fail(409,'capture_conflict','artifact manifest does not match staged chunks');}
293
293
  if(commit.native_bundle!=null){const bundle=commit.native_bundle;exact(bundle,['id','harness','fidelity','residue','files'],'native_bundle');if(typeof bundle.id!=='string'||bundle.harness!==cap.harness||!['byte_lossless','value_lossless','semantic'].includes(bundle.fidelity)||!Array.isArray(bundle.residue)||!bundle.residue.every(x=>typeof x==='string')||!Array.isArray(bundle.files))fail(400,'invalid_request','invalid native bundle');const artifactIds=new Set(commit.artifacts.map(x=>x.upload_id)),seen=new Set();for(const file of bundle.files){exact(file,['upload_id','role'],'native_bundle file');if(!artifactIds.has(file.upload_id)||seen.has(file.upload_id)||!['primary','subagent','bundle','source_recovery'].includes(file.role))fail(400,'invalid_request','native bundle files must uniquely reference captured artifacts');seen.add(file.upload_id);}if(seen.size!==artifactIds.size||bundle.files.filter(x=>x.role==='primary').length!==1||new Set(commit.artifacts.map(x=>x.relative_name)).size!==commit.artifacts.length)fail(400,'invalid_request','native bundle must describe every unique artifact and one primary');}
294
294
  let origin=await api.store.one('SELECT * FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=?',[auth.team_id,sourceId,cap.harness,cap.native_id]);
295
+ // A capture that carries (schema 9's wire half) names its first carry.count messages by digest instead of sending
296
+ // them again: they are the ones this session's current version holds at those places, written by this source. The
297
+ // server rebuilds them from its rows and checks the digest; anything else is refused, and the source sends it whole.
298
+ if(begin.carry){
299
+ const {count,digest}=begin.carry,session=origin?await api.store.one('SELECT revision FROM sessions WHERE team_id=? AND id=?',[auth.team_id,origin.session_id]):null;
300
+ const rows=session?await api.store.query(`SELECT ordinal,source_id,native_key,recorded_at,value_json FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT()} AND m.ordinal<? ORDER BY m.ordinal`,[auth.team_id,origin.session_id,session.revision,session.revision,count]):[];
301
+ const carried=rows.map(row=>({native_key:row.native_key,recorded_at:row.recorded_at,value:JSON.parse(row.value_json)}));
302
+ if(rows.length!==count||rows.some((row,index)=>row.ordinal!==index||row.source_id!==sourceId)||carryDigest(carried)!==digest)fail(409,'carry_mismatch','the carried messages are not what this session holds; send the capture whole');
303
+ const sent=new Set(messages.map(m=>m.native_key));
304
+ if(carried.some(m=>sent.has(m.native_key)))fail(409,'capture_conflict','duplicate native message key');
305
+ messages=[...carried,...messages];
306
+ }
295
307
  let lineage=null,lineageValue=null,lineageMembers=[],lineageGuards=[];if(!origin&&source.kind==='connector'&&source.machine_id){const candidates=await api.store.query("SELECT * FROM continuations WHERE team_id=? AND state='running' AND json_extract(value_json,'$.new_session_id') IS NULL AND json_extract(value_json,'$.harness')=? AND json_extract(value_json,'$.destination.machine_id')=? AND json_extract(value_json,'$.remote_result.state')='succeeded' AND json_extract(value_json,'$.remote_result.receipt.native_session_id')=?",[auth.team_id,cap.harness,source.machine_id,cap.native_id]);if(candidates.length===1){const candidate=candidates[0],membership=await api.store.one('SELECT role,active FROM memberships WHERE team_id=? AND principal_id=?',[auth.team_id,candidate.requester_id]),reader={kind:'user',team_id:auth.team_id,principal_id:candidate.requester_id,role:membership?.role,active:membership?.active===1,now:api.clock.nowMs(),group_ids:(await api.store.query('SELECT group_id FROM group_members WHERE team_id=? AND principal_id=?',[auth.team_id,candidate.requester_id])).map(x=>x.group_id)};if(await canReadMerged(api,reader,await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=?',[auth.team_id,candidate.source_session_id]))){lineage=candidate;lineageValue=JSON.parse(candidate.value_json);lineageMembers=await constituents(api,auth.team_id,await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=?',[auth.team_id,candidate.source_session_id]));}}if(lineageValue){const lineageNow=api.clock.nowMs();lineageGuards=[{sql:'SELECT 1 ok FROM memberships WHERE team_id=? AND principal_id=? AND active=1',params:[auth.team_id,lineage.requester_id],code:'conflict',status:409,message:'continuation requester membership changed'},{sql:'SELECT 1 ok FROM credentials WHERE id=? AND principal_id=? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)',params:[lineageValue.issuer_credential_id,lineage.requester_id,lineageNow],code:'conflict',status:409,message:'continuation issuer credential changed'},...lineageMembers.map(member=>({sql:`SELECT 1 ok FROM sessions s WHERE s.team_id=? AND s.id=? AND s.deleted_at IS NULL AND (?=0 OR s.access_revision=?) AND (s.visibility='team' OR s.custodian_id=? OR EXISTS (SELECT 1 FROM session_grants g WHERE g.team_id=s.team_id AND g.session_id=s.id AND (g.expires_at IS NULL OR g.expires_at>?) AND (g.subject_kind='principal' AND g.subject_id=? OR g.subject_kind='group' AND EXISTS (SELECT 1 FROM group_members gm WHERE gm.team_id=s.team_id AND gm.group_id=g.subject_id AND gm.principal_id=?))))`,params:[auth.team_id,member.id,member.id===lineage.source_session_id?1:0,lineageValue.access_revision,lineage.requester_id,lineageNow,lineage.requester_id,lineage.requester_id],code:'conflict',status:409,message:'continuation source authorization changed'}))];}}
296
308
  const sid=origin?.session_id??api.ids.next('session'),parentOrigin=commit.parent_native_id?await api.store.one('SELECT session_id FROM origins WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND deleted_at IS NULL',[auth.team_id,sourceId,cap.harness,commit.parent_native_id]):null,revisionParam={$journalRevision:true},recorded=commit.recorded_activity_at,boundaryNow=api.clock.nowMs(),sortAt=recorded??boundaryNow,descriptor={id:sid,revision:'__JOURNAL_REVISION__',title:commit.title,harness:cap.harness,user:{principal_id:source.kind==='connector'?source.custodian_id:null,reported_user:commit.reported_user,status:source.kind==='connector'?'verified':commit.reported_user?'reported':'unknown',mapping_id:null},repository:commit.repository,parent_id:parentOrigin?.session_id??null,continued_from:lineage?.source_session_id??null,recorded_activity_at:recorded,received_at:boundaryNow,sort_at:sortAt,activity_basis:recorded==null?'received':'recorded',source_ids:[sourceId],machine_ids:source.machine_id?[source.machine_id]:[],coverage:commit.coverage,bindings:[],actions:{}},statements=[];
297
309
  if(!origin)statements.push({sql:'INSERT INTO sessions(team_id,id,harness,custodian_id,visibility,revision,created_at) VALUES (?,?,?,?,?,?,?)',params:[auth.team_id,sid,cap.harness,source.custodian_id,lineage?'restricted':begin.visibility,revisionParam,boundaryNow]},{sql:'INSERT INTO origins(team_id,source_id,harness,native_id,session_id,generation,sequence,digest) VALUES (?,?,?,?,?,?,?,?)',params:[auth.team_id,sourceId,cap.harness,cap.native_id,sid,cap.generation,cap.sequence,commit.digest]});else statements.push({sql:'UPDATE sessions SET revision=? WHERE team_id=? AND id=?',params:[revisionParam,auth.team_id,sid]},{sql:'UPDATE origins SET generation=?,sequence=?,digest=? WHERE team_id=? AND source_id=? AND harness=? AND native_id=?',params:[cap.generation,cap.sequence,commit.digest,auth.team_id,sourceId,cap.harness,cap.native_id]});