@volter/supercode-teams 0.2.61 → 0.2.62
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.62",
|
|
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",
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { MESSAGE_AT, OPEN_REVISION } from '@volter/teams/server/migrations';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A new version of `sessionId` at `revision` holding exactly the messages of `from`'s version (a correlation's primary):
|
|
5
|
+
* the session's open rows are closed at it and `from`'s rows are copied as rows of this version alone (schema 9).
|
|
6
|
+
*/
|
|
7
|
+
function copyVersion(all, run, teamId, from, sessionId, revision) {
|
|
8
|
+
run('UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND until_revision=?', [revision, teamId, sessionId, OPEN_REVISION]);
|
|
9
|
+
for (const m of all(`SELECT * FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT()} ORDER BY ordinal`, [teamId, from.id, from.revision, from.revision])) run('INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text) VALUES (?,?,?,?,?,?,?,?,?,?)', [teamId, sessionId, revision, m.ordinal, m.id, m.source_id, m.native_key, m.recorded_at, m.value_json, m.search_text]);
|
|
10
|
+
}
|
|
1
11
|
function normalizeSearch(value) { return String(value ?? "").normalize("NFC").toLowerCase(); }
|
|
2
12
|
function contentText(content) { return typeof content === "string" ? content : Array.isArray(content) ? content.filter((x) => x?.type === "text").map((x) => x.text).join("\n") : ""; }
|
|
3
13
|
function fail(message, code, status) { throw Object.assign(new Error(message), { code, status }); }
|
|
@@ -33,12 +43,12 @@ export function correlateSessions(db,args){
|
|
|
33
43
|
const primary=views.reduce((best,row)=>rank(row)>rank(best)||rank(row)===rank(best)&&String(row.source_id).localeCompare(String(best.source_id))<0?row:best,views[0]);
|
|
34
44
|
const team=get('SELECT journal_revision FROM teams WHERE id=?',[args.teamId]),revision=team.journal_revision+1;
|
|
35
45
|
const originSources=all(`SELECT DISTINCT source_id FROM origins WHERE team_id=? AND session_id IN (${members.map(()=>'?').join(',')})`,[args.teamId,...members.map(row=>row.id)]).map(row=>row.source_id);const descriptor={...JSON.parse(primary.descriptor_json),id:canonical.id,revision,source_ids:[...new Set(originSources)].sort()};
|
|
36
|
-
const comparable=new Map();let sourceConflict=false;for(const view of views)for(const message of all(
|
|
46
|
+
const comparable=new Map();let sourceConflict=false;for(const view of views)for(const message of all(`SELECT native_key,value_json FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT()}`,[args.teamId,view.id,view.revision,view.revision])){if(!message.native_key)continue;const value=JSON.parse(message.value_json),semantic=JSON.stringify({role:value.role,content:value.content});const previous=comparable.get(message.native_key);if(previous!==undefined&&previous!==semantic)sourceConflict=true;else comparable.set(message.native_key,semantic);}if(sourceConflict)descriptor.coverage={...descriptor.coverage,reasons:[...new Set([...(descriptor.coverage?.reasons??[]),'source_conflict'])]};
|
|
37
47
|
for(const member of members)run('UPDATE sessions SET canonical_id=? WHERE team_id=? AND id=?',[member.id===canonical.id?null:canonical.id,args.teamId,member.id]);
|
|
38
48
|
run('UPDATE sessions SET revision=? WHERE team_id=? AND id=?',[revision,args.teamId,canonical.id]);
|
|
39
49
|
run('INSERT INTO correlations(team_id,id,left_session_id,right_session_id,canonical_id,policy_json,evidence_json,revision) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,args.correlationId,leftInput.id,rightInput.id,canonical.id,JSON.stringify({reader:'all_constituents'}),JSON.stringify(args.evidence),revision]);
|
|
40
50
|
run('INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,canonical.id,revision,JSON.stringify(descriptor),primary.source_projection_json,primary.search_title,primary.sort_at,now]);
|
|
41
|
-
|
|
51
|
+
copyVersion(all,run,args.teamId,primary,canonical.id,revision);
|
|
42
52
|
run('UPDATE teams SET journal_revision=?,auth_epoch=auth_epoch+1 WHERE id=?',[revision,args.teamId]);
|
|
43
53
|
run('INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',[args.teamId,revision,canonical.id,'metadata',now]);
|
|
44
54
|
const response={version:1,context:args.context,data:descriptor};
|
|
@@ -87,16 +97,17 @@ export function ingestOtlpBatch(db, args) {
|
|
|
87
97
|
run('INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,sessionId,journalRevision,JSON.stringify(descriptor),JSON.stringify({source_id:args.sourceId,kind:'telemetry',native_session_id:items[0].decoded.native_session_id,decoder:items[0].decoded.decoder}),normalizeSearch(descriptor.title),descriptor.sort_at,now]);
|
|
88
98
|
let ordinal = 0;
|
|
89
99
|
if (prior) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
// The current version's messages stay open into this one (schema 9); new events follow them.
|
|
101
|
+
run(`UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND ${MESSAGE_AT('messages')}`,[OPEN_REVISION,args.teamId,sessionId,session.revision,session.revision]);
|
|
102
|
+
ordinal = (get(`SELECT MAX(ordinal) last FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT()}`,[args.teamId,sessionId,session.revision,session.revision])?.last ?? -1) + 1;
|
|
103
|
+
} else run('UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND until_revision=?',[journalRevision,args.teamId,sessionId,OPEN_REVISION]);
|
|
93
104
|
for (const item of messages) {
|
|
94
105
|
const decoded=item.decoded, nativeKey=decoded.identity?.native_event_id ?? item.evidenceId;
|
|
95
106
|
let messageId = decoded.identity?.native_event_id ? get('SELECT id FROM message_ids WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND native_key=?',[args.teamId,args.sourceId,decoded.harness,decoded.native_session_id,nativeKey])?.id : null;
|
|
96
|
-
if (messageId && get(
|
|
107
|
+
if (messageId && get(`SELECT 1 ok FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT()} AND m.native_key=?`,[args.teamId,sessionId,journalRevision,journalRevision,nativeKey])) continue;
|
|
97
108
|
messageId ??= item.proposedMessageId;
|
|
98
109
|
if (decoded.identity?.native_event_id) run('INSERT OR IGNORE INTO message_ids(team_id,source_id,harness,native_id,native_key,id) VALUES (?,?,?,?,?,?)',[args.teamId,args.sourceId,decoded.harness,decoded.native_session_id,nativeKey,messageId]);
|
|
99
|
-
run('INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text) VALUES (
|
|
110
|
+
run('INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text,until_revision) VALUES (?,?,?,?,?,?,?,?,?,?,?)',[args.teamId,sessionId,journalRevision,ordinal++,messageId,args.sourceId,nativeKey,decoded.recorded_at,JSON.stringify(decoded.message),normalizeSearch(contentText(decoded.message.content)),OPEN_REVISION]);
|
|
100
111
|
}
|
|
101
112
|
run('INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',[args.teamId,journalRevision,sessionId,'capture',now]); changedSessions.push({sessionId,revision:journalRevision});
|
|
102
113
|
}
|
|
@@ -112,7 +123,7 @@ export function ingestOtlpBatch(db, args) {
|
|
|
112
123
|
journalRevision++;const originSources=all(`SELECT DISTINCT source_id FROM origins WHERE team_id=? AND session_id IN (${family.map(()=>'?').join(',')})`,[args.teamId,...family.map(row=>row.id)]).map(row=>row.source_id);const descriptor={...JSON.parse(primary.descriptor_json),id:canonicalId,revision:journalRevision,source_ids:[...new Set(originSources)].sort()};
|
|
113
124
|
run('UPDATE sessions SET revision=? WHERE team_id=? AND id=?',[journalRevision,args.teamId,canonicalId]);
|
|
114
125
|
run('INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',[args.teamId,canonicalId,journalRevision,JSON.stringify(descriptor),primary.source_projection_json,primary.search_title,primary.sort_at,now]);
|
|
115
|
-
|
|
126
|
+
copyVersion(all,run,args.teamId,primary,canonicalId,journalRevision);
|
|
116
127
|
run('INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',[args.teamId,journalRevision,canonicalId,'capture',now]);changedSessions.push({sessionId:canonicalId,revision:journalRevision});
|
|
117
128
|
}
|
|
118
129
|
const currentCounters = JSON.parse(authorized.counters_json ?? '{}');
|
|
@@ -12,6 +12,7 @@ import { normalizeSearch, textFromContent, cursorHash, cursorToken } from './cat
|
|
|
12
12
|
import { captureDigest, captureMessages, handleOtlpRoute, sha256, validateArtifactName } from './ingest.mjs';
|
|
13
13
|
import { handleCatalogEvents, handleSessionEvents, sessionNames } from './event-routes.mjs';
|
|
14
14
|
import { exact } from '@volter/teams/server/request-json';
|
|
15
|
+
import { MESSAGE_AT, OPEN_REVISION } from '@volter/teams/server/migrations';
|
|
15
16
|
import { delegatedSessionOf, looksDelegated } from '@volter/teams/server/delegated';
|
|
16
17
|
import { renderSourceCollectorConfig } from './otlp/collector-config.mjs';
|
|
17
18
|
import { attachmentActionRoute, createAttachment, liveGrantRoute, machineEffectRoute } from './effects.mjs';
|
|
@@ -146,8 +147,8 @@ async function listSessions(api,res,auth,url,requestId,search){
|
|
|
146
147
|
);
|
|
147
148
|
if(cursorKey)statements.push({kind:'query',sql:`SELECT s.*,v.revision version_revision,v.descriptor_json,v.sort_at FROM ${cursorVersions} JOIN session_versions v ON v.team_id=? AND v.session_id=json_extract(j.value,'$[0]') AND v.revision=json_extract(j.value,'$[1]') JOIN sessions s ON s.team_id=v.team_id AND s.id=v.session_id WHERE s.deleted_at IS NULL AND v.revision<>s.revision`,params:[...cursorKey,auth.team_id]});
|
|
148
149
|
if(search){
|
|
149
|
-
statements.push({kind:'query',sql:'SELECT m.id,m.session_id,
|
|
150
|
-
if(cursorKey)statements.push({kind:'query',sql:`SELECT m.id,m.session_id,
|
|
150
|
+
statements.push({kind:'query',sql:'SELECT m.id,m.session_id,s.revision revision,m.recorded_at,m.search_text FROM sessions s JOIN messages m ON m.team_id=s.team_id AND m.session_id=s.id AND m.revision<=s.revision AND s.revision<COALESCE(m.until_revision,m.revision+1) WHERE s.team_id=? AND s.deleted_at IS NULL',params:[auth.team_id]});
|
|
151
|
+
if(cursorKey)statements.push({kind:'query',sql:`SELECT m.id,m.session_id,json_extract(j.value,'$[1]') revision,m.recorded_at,m.search_text FROM ${cursorVersions} JOIN messages m ON m.team_id=? AND m.session_id=json_extract(j.value,'$[0]') AND m.revision<=json_extract(j.value,'$[1]') AND json_extract(j.value,'$[1]')<COALESCE(m.until_revision,m.revision+1) JOIN sessions s ON s.team_id=m.team_id AND s.id=m.session_id WHERE s.deleted_at IS NULL AND json_extract(j.value,'$[1]')<>s.revision`,params:[...cursorKey,auth.team_id]});
|
|
151
152
|
}
|
|
152
153
|
const values=await api.store.snapshot(statements),offset=cursorKey?1:0,cursor=cursorKey?values[0]:null,team=values[offset],credential=values[offset+1],membership=values[offset+2],currentRows=values[offset+3],allGrants=values[offset+4];
|
|
153
154
|
let at=offset+5;const olderRows=cursorKey?values[at++]:[],allMessages=search?[...values[at++],...(cursorKey?values[at++]:[])]:[];
|
|
@@ -208,7 +209,8 @@ async function correlateRoute(api,req,res,auth,path,requestId){
|
|
|
208
209
|
async function mutateSession(api,req,res,auth,id,requestId){const seed=await api.store.one('SELECT * FROM sessions WHERE team_id=? AND id=?',[auth.team_id,id]),root=await canonicalSession(api,auth.team_id,seed),canonicalId=root?.id??id;const row=await api.store.one('SELECT s.*,v.descriptor_json,v.source_projection_json,v.search_title,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,canonicalId]);if(!row)fail(404,'not_found','not found');const members=await constituents(api,auth.team_id,row);for(const member of members)if(!isAdmin(auth)&&!canManage(auth,member,await grants(api,auth.team_id,member.id)))fail(404,'not_found','not found');const expected=Number(String(req.headers['if-match']??'').replaceAll('"',''));if(!Number.isSafeInteger(expected))fail(400,'invalid_request','If-Match is required');if(expected!==row.revision)fail(409,'revision_mismatch','session revision changed');const boundaryNow=api.clock.nowMs(),revisionParam={$journalRevision:true},guards=[...authGuards(auth,boundaryNow),{sql:'SELECT 1 ok FROM sessions WHERE team_id=? AND id=? AND revision=? AND deleted_at IS NULL',params:[auth.team_id,row.id,expected],code:'revision_mismatch',status:409,message:'session revision changed'}];
|
|
209
210
|
for(const member of members)guards.push(managerGuard(auth,member.id,boundaryNow,{governance:true}));
|
|
210
211
|
if(req.method==='DELETE'){const deletion=[];for(const member of members)deletion.push({sql:'UPDATE sessions SET deleted_at=?,revision=? WHERE team_id=? AND id=?',params:[boundaryNow,revisionParam,auth.team_id,member.id]},{sql:'UPDATE origins SET deleted_at=? WHERE team_id=? AND session_id=?',params:[boundaryNow,auth.team_id,member.id]},{sql:'DELETE FROM session_versions WHERE team_id=? AND session_id=?',params:[auth.team_id,member.id]});deletion.push({sql:'UPDATE teams SET journal_revision=?,auth_epoch=auth_epoch+1 WHERE id=?',params:[revisionParam,auth.team_id]},{sql:'INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',params:[auth.team_id,revisionParam,row.id,'delete',boundaryNow]});const {revision}=await guardedMutation(api,res,auth,guards,deletion,{status:204,body:null,targetId:row.id,journal:true});api.events.publish(auth.team_id,{type:'session_deleted',cursor:String(revision),revision,session_id:row.id});return send(res,204,null,requestId);}
|
|
211
|
-
const body=await parseJson(req);exact(body,['title']);if(body.title!==null&&typeof body.title!=='string')fail(400,'invalid_request','title must be string or null');const descriptor={...projection(row),title:body.title};delete descriptor.revision;const
|
|
212
|
+
const body=await parseJson(req);exact(body,['title']);if(body.title!==null&&typeof body.title!=='string')fail(400,'invalid_request','title must be string or null');const descriptor={...projection(row),title:body.title};delete descriptor.revision;const statements=[{sql:'UPDATE sessions SET revision=? WHERE team_id=? AND id=? AND revision=?',params:[revisionParam,auth.team_id,row.id,expected]},{sql:'INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',params:[auth.team_id,row.id,revisionParam,{$journalJson:descriptor},row.source_projection_json,normalizeSearch(body.title??''),row.sort_at,boundaryNow]}];// The new version holds the same messages: the current version's rows stay open into it (schema 9), none copied.
|
|
213
|
+
statements.push({sql:`UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND ${MESSAGE_AT('messages')}`,params:[OPEN_REVISION,auth.team_id,row.id,row.revision,row.revision]});statements.push({sql:'UPDATE teams SET journal_revision=? WHERE id=?',params:[revisionParam,auth.team_id]},{sql:'INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',params:[auth.team_id,revisionParam,row.id,'metadata',boundaryNow]});const storedResponse=result(api,auth,{...descriptor,revision:'__JOURNAL_REVISION__'}),{revision}=await guardedMutation(api,res,auth,guards,statements,{status:200,body:storedResponse,targetId:row.id,journal:true}),responseDescriptor={...descriptor,revision};api.events.publish(auth.team_id,{type:'session_changed',cursor:String(revision),revision,session_id:row.id,reason:'metadata',read_required:true});send(res,200,result(api,auth,responseDescriptor),requestId,{ETag:`"${revision}"`});}
|
|
212
214
|
async function readSession(api,res,auth,id,url,requestId){
|
|
213
215
|
const query=qobj(url);
|
|
214
216
|
validateReadQuery(query);
|
|
@@ -294,8 +296,20 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
|
|
|
294
296
|
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=[];
|
|
295
297
|
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]});
|
|
296
298
|
if(lineage){lineageValue.new_session_id=sid;const operationRow=await api.store.one('SELECT value_json FROM operations WHERE team_id=? AND id=?',[auth.team_id,lineageValue.operation_id]),operationValue=JSON.parse(operationRow.value_json);operationValue.result={...operationValue.result,new_session_id:sid};statements.push({sql:"UPDATE continuations SET value_json=? WHERE team_id=? AND id=? AND state='running' AND value_json=?",params:[JSON.stringify(lineageValue),auth.team_id,lineage.id,lineage.value_json]},{sql:'UPDATE operations SET value_json=? WHERE team_id=? AND id=?',params:[JSON.stringify(operationValue),auth.team_id,lineageValue.operation_id]});lineageGuards.push({sql:"SELECT 1 ok FROM continuations WHERE team_id=? AND id=? AND state='running' AND value_json=?",params:[auth.team_id,lineage.id,lineage.value_json],code:'conflict',status:409,message:'continuation lineage changed'});}
|
|
297
|
-
statements.push({sql:'INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,{$journalJsonValue:descriptor},JSON.stringify({source_id:sourceId}),normalizeSearch(commit.title??''),sortAt,boundaryNow]})
|
|
298
|
-
|
|
299
|
+
statements.push({sql:'INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,{$journalJsonValue:descriptor},JSON.stringify({source_id:sourceId}),normalizeSearch(commit.title??''),sortAt,boundaryNow]});// Only what changed is written (schema 9): a message the session's current version holds unchanged at the same place
|
|
300
|
+
// stays open into this version; one that changed or went is closed at it; a new one is written. A capture of a long
|
|
301
|
+
// session that gained one message writes one row, not the whole session again.
|
|
302
|
+
const carryGuards=[],currentRevision=origin?(await api.store.one('SELECT revision FROM sessions WHERE team_id=? AND id=?',[auth.team_id,sid]))?.revision??null:null;
|
|
303
|
+
const held=new Map();if(currentRevision!=null)for(const row of await api.store.query(`SELECT revision,ordinal,id,source_id,native_key,recorded_at,value_json,until_revision FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT()}`,[auth.team_id,sid,currentRevision,currentRevision]))held.set(row.ordinal,row);
|
|
304
|
+
// Another commit to this session between this read and the write would make the carried rows wrong: refused, retried.
|
|
305
|
+
if(currentRevision!=null)carryGuards.push({sql:'SELECT 1 ok FROM sessions WHERE team_id=? AND id=? AND revision=?',params:[auth.team_id,sid,currentRevision],code:'revision_mismatch',status:409,message:'session changed during capture; retry'});
|
|
306
|
+
const close=row=>{if(row.until_revision===OPEN_REVISION)statements.push({sql:'UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND revision=? AND ordinal=?',params:[revisionParam,auth.team_id,sid,row.revision,row.ordinal]});};
|
|
307
|
+
let ordinal=0;for(const m of messages){const value=JSON.stringify(m.value),old=held.get(ordinal);held.delete(ordinal);
|
|
308
|
+
if(old&&old.source_id===sourceId&&old.native_key===m.native_key&&old.recorded_at===m.recorded_at&&old.value_json===value){if(old.until_revision!==OPEN_REVISION)statements.push({sql:'UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND revision=? AND ordinal=?',params:[OPEN_REVISION,auth.team_id,sid,old.revision,old.ordinal]});ordinal++;continue;}
|
|
309
|
+
if(old)close(old);
|
|
310
|
+
let mid=(await api.store.one('SELECT id FROM message_ids WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND native_key=?',[auth.team_id,sourceId,cap.harness,cap.native_id,m.native_key]))?.id;if(!mid){mid=api.ids.next('message');statements.push({sql:'INSERT INTO message_ids(team_id,source_id,harness,native_id,native_key,id) VALUES (?,?,?,?,?,?)',params:[auth.team_id,sourceId,cap.harness,cap.native_id,m.native_key,mid]});}statements.push({sql:'INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text,until_revision) VALUES (?,?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,ordinal++,mid,sourceId,m.native_key,m.recorded_at,value,normalizeSearch(textFromContent(m.value.content)),OPEN_REVISION]});}
|
|
311
|
+
for(const row of held.values())close(row);
|
|
312
|
+
const bundle=commit.native_bundle??null,legacyBundle=commit.artifacts.length?captureId:null;for(let index=0;index<commit.artifacts.length;index++){const a=commit.artifacts[index],file=bundle?.files.find(x=>x.upload_id===a.upload_id);statements.push({sql:'UPDATE uploads SET manifest_json=?,committed=1 WHERE team_id=? AND id=?',params:[JSON.stringify(a),auth.team_id,a.upload_id]},{sql:'INSERT INTO session_artifacts(team_id,session_id,revision,bundle_id,upload_id,format,provenance,file_role,fidelity,residue_json) VALUES (?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,bundle?.id??legacyBundle,a.upload_id,bundle?.harness??cap.harness,'capture',file?.role??(index===0?'primary':'bundle'),bundle?.fidelity??'semantic',JSON.stringify(bundle?.residue??['native_bundle_metadata_unavailable'])]});}const storedReceipt={source_id:sourceId,capture_id:captureId,session_id:sid,generation:cap.generation,sequence:cap.sequence,digest:commit.digest,revision:'__JOURNAL_REVISION__',duplicate:false};statements.push({sql:"UPDATE captures SET state='committed',commit_json=?,receipt_json=? WHERE team_id=? AND id=?",params:[JSON.stringify(commit),{$journalJsonValue:storedReceipt},auth.team_id,captureId]},{sql:'DELETE FROM capture_parts WHERE team_id=? AND capture_id=?',params:[auth.team_id,captureId]},{sql:'UPDATE sources SET last_received_at=? WHERE team_id=? AND id=?',params:[boundaryNow,auth.team_id,sourceId]},{sql:'UPDATE teams SET journal_revision=? WHERE id=?',params:[revisionParam,auth.team_id]},{sql:'INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',params:[auth.team_id,revisionParam,sid,'capture',boundaryNow]});const guards=[{sql:'SELECT 1 ok FROM credentials WHERE id=? AND source_id=? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)',params:[auth.credential_id,sourceId,boundaryNow],code:'authentication_required',status:401,message:'source credential expired or revoked'},...lineageGuards,...carryGuards,{sql:"SELECT 1 ok FROM captures 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=s.machine_id LEFT JOIN sync_rules r ON r.team_id=c.team_id AND r.id=json_extract(c.begin_json,'$.rule_id') WHERE c.team_id=? AND c.id=? AND c.source_id=? AND c.state='staging' AND s.state='active' AND (s.kind!='connector' OR (m.revoked_at IS NULL AND r.machine_id=m.id AND r.source_id=s.id AND r.state='active' AND r.revision=json_extract(c.begin_json,'$.rule_revision') AND r.visibility=json_extract(c.begin_json,'$.visibility')))",params:[auth.team_id,captureId,sourceId],code:'conflict',status:409,message:'capture authorization changed'}],storedResponse=result(api,auth,storedReceipt),{revision}=await guardedMutation(api,res,auth,guards,statements,{status:200,body:storedResponse,targetId:captureId,journal:true}),receipt={...storedReceipt,revision};api.events.publish(auth.team_id,{type:'session_changed',cursor:String(revision),revision,session_id:sid,reason:'capture',read_required:true});send(res,200,result(api,auth,receipt),requestId);
|
|
299
313
|
}
|
|
300
314
|
|
|
301
315
|
/**
|
package/server/store-core.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// them on the object's own SQLite.
|
|
6
6
|
import { ingestOtlpBatch, correlateSessions } from './arc2-transactions.mjs';
|
|
7
7
|
import { claimEffect, getEffect, listEffects, reportEffect } from './effects-transactions.mjs';
|
|
8
|
-
import { MIGRATIONS } from '@volter/teams/server/migrations';
|
|
8
|
+
import { MIGRATIONS, MESSAGE_AT } from '@volter/teams/server/migrations';
|
|
9
9
|
|
|
10
10
|
/** Bring a database to the newest schema. `version()`/`setVersion(n)` name where the host records it. */
|
|
11
11
|
export function migrate({ exec, tx, version, setVersion }) {
|
|
@@ -85,12 +85,12 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
85
85
|
}else{
|
|
86
86
|
revision=requestedRevision??session.revision;
|
|
87
87
|
if(!get('SELECT 1 ok FROM session_versions WHERE team_id=? AND session_id=? AND revision=?',[teamId,session.id,revision]))domainError(404,'not_found','session revision not found');
|
|
88
|
-
ordinals=all(
|
|
88
|
+
ordinals=all(`SELECT ordinal FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT()} ORDER BY ordinal`,[teamId,session.id,revision,revision]).map(row=>row.ordinal);
|
|
89
89
|
}
|
|
90
90
|
const pageOrdinals=ordinals.slice(-tail),remaining=ordinals.slice(0,-tail);
|
|
91
91
|
// A page is a run of one revision's ordinals, read as a range: a hosted SQLite binds at most 100 variables, and a
|
|
92
92
|
// page of more messages than that, as an IN list, failed ("too many SQL variables").
|
|
93
|
-
const rows=pageOrdinals.length?all(
|
|
93
|
+
const rows=pageOrdinals.length?all(`SELECT * FROM messages m WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT()} AND m.ordinal>=? AND m.ordinal<=? ORDER BY m.ordinal`,[teamId,session.id,revision,revision,pageOrdinals[0],pageOrdinals[pageOrdinals.length-1]]).filter(row=>pageOrdinals.includes(row.ordinal)):[];
|
|
94
94
|
const version=get('SELECT descriptor_json FROM session_versions WHERE team_id=? AND session_id=? AND revision=?',[teamId,session.id,revision]);
|
|
95
95
|
if(!version)domainError(404,'not_found','session revision not found');
|
|
96
96
|
let descriptorJson=version.descriptor_json;
|