@volter/teams 0.2.26 → 0.2.28

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/teams",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "type": "module",
5
5
  "description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
6
6
  "license": "MIT",
package/server/apps.mjs CHANGED
@@ -397,12 +397,10 @@ export async function deliverAppWebhooks(api) {
397
397
  const state = delivered ? 'delivered' : row.attempts >= 8 ? 'failed' : 'pending';
398
398
  await api.store.transaction([statement("UPDATE app_webhook_deliveries SET state=?,last_status=?,lease_until=NULL,next_at=? WHERE id=? AND lease_until=? AND state='pending'", state, status, api.clock.nowMs() + Math.min(3600000, 1000 * 2 ** row.attempts), id, lease)]);
399
399
  }
400
- // Week-old settled deliveries go from the front (oldest rowid first), a bounded batch per run, stopping at the first
401
- // delivery younger than a week or still pending (it settles within its eight attempts). Expired install codes go
402
- // with maintain's other expiries (t_6c01c966).
403
- const front = await api.store.query('SELECT rowid AS id,state,created_at FROM app_webhook_deliveries ORDER BY rowid LIMIT 500', []);
404
- let through = null; for (const row of front) { if (row.state === 'pending' || row.created_at >= api.clock.nowMs() - 7 * 86400000) break; through = row.id; }
405
- if (through != null) await api.store.transaction([statement('DELETE FROM app_webhook_deliveries WHERE rowid<=? AND state<>\'pending\'', through)]);
406
- if (through != null && front.length >= 500 && front.at(-1).id === through) return true;
400
+ // Week-old settled deliveries go through their queue (migration 024: queued when they settled), oldest first, a
401
+ // bounded batch per run; a pending delivery is never in it, so none stands in front of them (t_6c01c966).
402
+ const cut = api.clock.nowMs() - 7 * 86400000;
403
+ await api.store.transaction([statement("DELETE FROM app_webhook_deliveries WHERE id IN (SELECT id FROM delivery_settled WHERE created_at<? ORDER BY created_at LIMIT 500) AND state<>'pending'", cut)]);
404
+ if (await api.store.one('SELECT 1 AS more FROM delivery_settled WHERE created_at<? LIMIT 1', [cut])) return true;
407
405
  return due.length >= 10;
408
406
  }
package/server/core.mjs CHANGED
@@ -73,7 +73,8 @@ export async function maintain(api,{budgetMs=MAINTAIN_BUDGET_MS}={}){const now=a
73
73
  for(const [table,column,cut] of [['idempotency','expires_at',now],['global_idempotency','expires_at',now],['journal','created_at',now-86400000],['app_install_codes','expires_at',now]]){
74
74
  const front=await q(`SELECT rowid AS id,${column} AS at FROM ${table} ORDER BY rowid LIMIT ${EXPIRY_BATCH}`);
75
75
  let through=null;for(const row of front){if(row.at>cut)break;through=row.id;}
76
- if(through!=null){await tx([{sql:`DELETE FROM ${table} WHERE rowid<=?`,params:[through]}]);count(`expired ${table}`,front.findIndex(row=>row.id===through)+1);}
76
+ // fenced by its own expiry too: a row written since the read that took a reused rowid is never removed
77
+ if(through!=null){await tx([{sql:`DELETE FROM ${table} WHERE rowid<=? AND ${column}<=?`,params:[through,cut]}]);count(`expired ${table}`,front.findIndex(row=>row.id===through)+1);}
77
78
  if(through!=null&&front.length>=EXPIRY_BATCH&&front.at(-1).id===through)more=true;
78
79
  }
79
80
  // cursors expire through their expiry index
@@ -105,6 +106,14 @@ export async function maintain(api,{budgetMs=MAINTAIN_BUDGET_MS}={}){const now=a
105
106
  }
106
107
  });
107
108
  // A committed upload nothing names (no artifact, continuation or in-flight capture: upload_refs) releases its bundle.
109
+ // A team with native capture off holds no native transcripts: its artifact rows go a batch at a time, and the
110
+ // uploads and blobs they named follow through the steps below once nothing else names them.
111
+ await step('native off',async()=>{
112
+ const rows=await q(`SELECT a.rowid id FROM session_artifacts a JOIN teams t ON t.id=a.team_id WHERE t.native_capture=0 LIMIT 200`);
113
+ if(!rows.length)return;
114
+ await tx(rows.map(row=>({sql:'DELETE FROM session_artifacts WHERE rowid=?',params:[row.id]})));
115
+ count('native artifacts',rows.length);if(rows.length>=200)more=true;
116
+ });
108
117
  await step('uploads',async()=>{
109
118
  if(!await walked(api,['artifact_refs','continuation_refs','captures','uploads'])){more=true;return;}
110
119
  const queued=await q(`SELECT team_id,upload_id,generation FROM upload_prune_queue LIMIT ${UPLOAD_PRUNE_BATCH}`);
@@ -135,8 +144,11 @@ export async function maintain(api,{budgetMs=MAINTAIN_BUDGET_MS}={}){const now=a
135
144
  // session's current version, the newest version from each of its sources, the newest one from each source that a
136
145
  // capture's artifact names (the next capture and a correlation read those), and every version a live cursor pins.
137
146
  await step('versions',async()=>{
138
- if(!await walked(api,['version_sources','cursor_pins'])){more=true;return;}
147
+ // every cursor made since migration 023 pins through its trigger; one made before it is waited out (cursors live
148
+ // 15 minutes), never unpacked
149
+ if(!await walked(api,['version_sources'])||!await cursorsBeforePinsGone(api,now)){more=true;return;}
139
150
  const due=await q(`SELECT team_id,session_id,generation FROM version_prune_queue WHERE due_at<=? ORDER BY due_at LIMIT ${VERSION_SESSION_BATCH}`,[now]);
151
+ let messageBudget=MESSAGE_BUDGET;
140
152
  for(const {team_id,session_id,generation} of due){
141
153
  if(!left()){more=true;break;}
142
154
  const session=await one('SELECT revision FROM sessions WHERE team_id=? AND id=?',[team_id,session_id]);
@@ -162,11 +174,27 @@ export async function maintain(api,{budgetMs=MAINTAIN_BUDGET_MS}={}){const now=a
162
174
  ORDER BY vs.revision LIMIT ${budget}`,[team_id,session_id,source,newest,session.revision,now,capture]);
163
175
  // A message row lives from the version that wrote it until one that changed it (schema 9), so a row a later
164
176
  // version still holds moves to the next version that holds it before its own goes; the rest go with the version.
165
- for(const {revision} of free)await tx([
166
- {sql:'UPDATE messages SET revision=(SELECT MIN(k.revision) FROM session_versions k WHERE k.team_id=messages.team_id AND k.session_id=messages.session_id AND k.revision>messages.revision AND k.revision<COALESCE(messages.until_revision,messages.revision+1)) WHERE team_id=? AND session_id=? AND revision=? AND EXISTS (SELECT 1 FROM session_versions k WHERE k.team_id=messages.team_id AND k.session_id=messages.session_id AND k.revision>messages.revision AND k.revision<COALESCE(messages.until_revision,messages.revision+1))',params:[team_id,session_id,revision]},
167
- ...['messages','session_artifacts','session_versions'].map(table=>({sql:`DELETE FROM ${table} WHERE team_id=? AND session_id=? AND revision=?`,params:[team_id,session_id,revision]})),
168
- ]);
169
- count('versions',free.length);budget-=free.length;if(budget<=0)full=true;
177
+ // A long session's version holds many rows: they are worked a bounded chunk at a time (by ordinal, on the key),
178
+ // and the version goes once none is left; one not finished keeps its session due.
179
+ let removed=0;
180
+ for(const {revision} of free){
181
+ for(;;){
182
+ if(messageBudget<=0||!left()){full=true;break;}
183
+ const edge=await one(`SELECT ordinal FROM messages WHERE team_id=? AND session_id=? AND revision=? ORDER BY ordinal LIMIT 1 OFFSET ${MESSAGE_CHUNK-1}`,[team_id,session_id,revision]);
184
+ const upTo=edge?.ordinal??Number.MAX_SAFE_INTEGER;
185
+ await tx([
186
+ {sql:'UPDATE messages SET revision=(SELECT MIN(k.revision) FROM session_versions k WHERE k.team_id=messages.team_id AND k.session_id=messages.session_id AND k.revision>messages.revision AND k.revision<COALESCE(messages.until_revision,messages.revision+1)) WHERE team_id=? AND session_id=? AND revision=? AND ordinal<=? AND EXISTS (SELECT 1 FROM session_versions k WHERE k.team_id=messages.team_id AND k.session_id=messages.session_id AND k.revision>messages.revision AND k.revision<COALESCE(messages.until_revision,messages.revision+1))',params:[team_id,session_id,revision,upTo]},
187
+ {sql:'DELETE FROM messages WHERE team_id=? AND session_id=? AND revision=? AND ordinal<=?',params:[team_id,session_id,revision,upTo]},
188
+ ]);
189
+ messageBudget-=MESSAGE_CHUNK;
190
+ if(!edge)break;
191
+ }
192
+ if(await one('SELECT 1 AS left FROM messages WHERE team_id=? AND session_id=? AND revision=? LIMIT 1',[team_id,session_id,revision]))break;
193
+ await tx(['session_artifacts','session_versions'].map(table=>({sql:`DELETE FROM ${table} WHERE team_id=? AND session_id=? AND revision=?`,params:[team_id,session_id,revision]})));
194
+ removed++;
195
+ }
196
+ count('versions',removed);budget-=free.length;if(budget<=0)full=true;
197
+ if(full)break;
170
198
  }
171
199
  // more of this session: due now; else due when its first live pin expires (what it pins may then go); else done
172
200
  const pin=full?null:await one('SELECT MIN(expires_at) AS at FROM cursor_pins WHERE team_id=? AND session_id=? AND expires_at>?',[team_id,session_id,now]);
@@ -209,6 +237,17 @@ export async function maintain(api,{budgetMs=MAINTAIN_BUDGET_MS}={}){const now=a
209
237
  if(Object.keys(counts).length)console.log(`maintain: removed ${Object.entries(counts).map(([name,n])=>`${n} ${name}`).join(', ')}${more?' (backlog)':''}`);
210
238
  return result;}
211
239
 
240
+ /** Whether every cursor made before migration 023 (whose versions it pins are not pin rows) has expired: the latest
241
+ * expiry among the cursors there when this was first asked, kept, then waited out (cursors live 15 minutes). */
242
+ async function cursorsBeforePinsGone(api,now){
243
+ let row=await api.store.one("SELECT after_rowid AS until FROM maintain_backfill WHERE name='cursors_before_pins'");
244
+ if(!row){
245
+ const latest=await api.store.one('SELECT expires_at FROM cursors ORDER BY expires_at DESC LIMIT 1');
246
+ row={until:latest?.expires_at??0};
247
+ await api.store.transaction([{sql:"INSERT OR IGNORE INTO maintain_backfill(name,after_rowid,done) VALUES ('cursors_before_pins',?,1)",params:[row.until]}]);
248
+ }
249
+ return now>row.until;
250
+ }
212
251
  /** Whether each named walk of migration 023's backfill is done. */
213
252
  async function walked(api,names){
214
253
  for(const name of names){const row=await api.store.one('SELECT done FROM maintain_backfill WHERE name=?',[name]);if(!row?.done)return false;}
@@ -220,9 +259,6 @@ const BACKFILLS=[
220
259
  ['version_sources','session_versions',[
221
260
  "INSERT OR IGNORE INTO version_sources SELECT team_id,session_id,COALESCE(json_extract(source_projection_json,'$.source_id'),''),revision FROM session_versions WHERE rowid>? AND rowid<=?",
222
261
  'INSERT INTO version_prune_queue(team_id,session_id,due_at,generation) SELECT DISTINCT team_id,session_id,0,1 FROM session_versions WHERE rowid>? AND rowid<=? ON CONFLICT(team_id,session_id) DO UPDATE SET due_at=0,generation=generation+1']],
223
- ['cursor_pins','cursors',[
224
- "INSERT INTO cursor_pins SELECT c.id,c.team_id,json_extract(j.value,'$[0]'),json_extract(j.value,'$[1]'),c.expires_at FROM cursors c, json_each(c.snapshot_json) j WHERE c.rowid>? AND c.rowid<=? AND c.expires_at>NOWMS AND c.kind IN ('list','search') AND c.snapshot_json IS NOT NULL AND json_extract(j.value,'$[0]') IS NOT NULL AND json_extract(j.value,'$[1]') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM cursor_pins p WHERE p.cursor_id=c.id)",
225
- "INSERT INTO cursor_pins SELECT c.id,c.team_id,json_extract(c.snapshot_json,'$.session_id'),json_extract(c.snapshot_json,'$.revision'),c.expires_at FROM cursors c WHERE c.rowid>? AND c.rowid<=? AND c.expires_at>NOWMS AND c.kind='history' AND json_extract(c.snapshot_json,'$.session_id') IS NOT NULL AND json_extract(c.snapshot_json,'$.revision') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM cursor_pins p WHERE p.cursor_id=c.id)"]],
226
262
  ['artifact_refs','session_artifacts',[
227
263
  "INSERT OR IGNORE INTO upload_refs SELECT team_id,upload_id,'a:'||session_id||':'||revision||':'||bundle_id FROM session_artifacts WHERE rowid>? AND rowid<=?"]],
228
264
  ['continuation_refs','continuations',[
@@ -234,6 +270,8 @@ const BACKFILLS=[
234
270
  ['uploads','uploads',[
235
271
  'INSERT INTO upload_prune_queue SELECT team_id,id,1 FROM uploads WHERE rowid>? AND rowid<=? AND committed=1 ON CONFLICT(team_id,upload_id) DO UPDATE SET generation=generation+1',
236
272
  'INSERT OR IGNORE INTO pending_uploads SELECT team_id,id,source_id,created_at FROM uploads WHERE rowid>? AND rowid<=? AND committed=0']],
273
+ ['deliveries','app_webhook_deliveries',[
274
+ "INSERT OR IGNORE INTO delivery_settled SELECT id,created_at FROM app_webhook_deliveries WHERE rowid>? AND rowid<=? AND state<>'pending'"]],
237
275
  ['blobs','artifact_blobs',[
238
276
  'INSERT INTO blob_prune_queue SELECT team_id,sha256,1 FROM artifact_blobs WHERE rowid>? AND rowid<=? ON CONFLICT(team_id,sha256) DO UPDATE SET generation=generation+1']],
239
277
  ];
@@ -270,6 +308,8 @@ const MAINTAIN_MS=60_000,MAINTAIN_BUDGET_MS=250;
270
308
  /** Sessions whose versions a run reads, and versions it removes from each; how far it walks down a source to find the
271
309
  * newest version a capture names. */
272
310
  const VERSION_SESSION_BATCH=10,VERSION_PRUNE_BATCH=20,CAPTURE_WALK=500;
311
+ /** Message rows a run moves or removes in all (a long session's version is worked across runs), and per statement. */
312
+ const MESSAGE_BUDGET=2000,MESSAGE_CHUNK=500;
273
313
  /** Committed captures whose parts a run removes, and parts per capture per run. */
274
314
  const PARTS_CAPTURE_BATCH=20,PART_PRUNE_BATCH=50;
275
315
  /** Rows of one expiry a run removes; captures and uploads in flight a run expires. */
package/server/http.mjs CHANGED
@@ -86,6 +86,7 @@ export function createHttpHandler(api){return async(req,res)=>{const requestId=a
86
86
  if(req.method==='POST'&&sub==='/invitations')return await createInvitation(api,req,res,auth,requestId);
87
87
  if(req.method==='POST'&&sub==='/credentials')return await createCredential(api,req,res,auth,requestId);
88
88
  if(req.method==='DELETE'&&(m=/^\/credentials\/([^/]+)$/.exec(sub)))return await revokeCredential(api,req,res,auth,m[1],requestId);
89
+ if((req.method==='GET'||req.method==='PATCH')&&sub==='/settings')return await teamSettings(api,req,res,auth,requestId);
89
90
  if(req.method==='GET'&&sub==='/members')return await listMembers(api,res,auth,url,requestId);
90
91
  if(req.method==='PATCH'&&(m=/^\/members\/([^/]+)$/.exec(sub)))return await updateMember(api,req,res,auth,m[1],requestId);
91
92
  if(req.method==='DELETE'&&(m=/^\/members\/([^/]+)$/.exec(sub)))return await removeMember(api,req,res,auth,m[1],requestId);
@@ -317,6 +318,16 @@ async function collectionPage(api,auth,url,name,items){
317
318
  const page=remaining.slice(0,limit),rest=remaining.slice(limit);let next=null;if(rest.length){next=cursorToken();const team=await api.store.one('SELECT auth_epoch,journal_revision FROM teams WHERE id=?',[auth.team_id]);await api.store.exec('INSERT INTO cursors(id,team_id,principal_id,kind,query_json,auth_epoch,revision,snapshot_json,expires_at) VALUES (?,?,?,?,?,?,?,?,?)',[cursorHash(next),auth.team_id,auth.principal_id,'list',JSON.stringify(normalized),team.auth_epoch,team.journal_revision,JSON.stringify(rest),api.clock.nowMs()+900000]);}return{version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},items:page,next_cursor:next};
318
319
  }
319
320
  async function listMembers(api,res,auth,url,requestId){const rows=auth.role==='viewer'?await api.store.query('SELECT m.*,p.kind,p.display_name FROM memberships m JOIN principals p ON p.id=m.principal_id WHERE m.team_id=? AND m.principal_id=? AND m.active=1',[auth.team_id,auth.principal_id]):await api.store.query('SELECT m.*,p.kind,p.display_name FROM memberships m JOIN principals p ON p.id=m.principal_id WHERE m.team_id=? AND m.active=1 ORDER BY p.display_name,m.principal_id',[auth.team_id]);send(res,200,await collectionPage(api,auth,url,'members',rows.map(membershipJson)),requestId);}
321
+ // The team's own settings. native_capture: whether machines upload each session's native transcript (see migration 025).
322
+ async function teamSettings(api,req,res,auth,requestId){
323
+ const read=async()=>{const t=await api.store.one('SELECT native_capture FROM teams WHERE id=?',[auth.team_id]);return{native_capture:t.native_capture===1};};
324
+ if(req.method==='GET')return send(res,200,result(api,auth,await read()),requestId);
325
+ if(!isAdmin(auth))fail(403,'forbidden','forbidden');
326
+ const body=await parseJson(req);exact(body,['native_capture']);if(typeof body.native_capture!=='boolean')fail(400,'invalid_request','native_capture must be a boolean');
327
+ const now=api.clock.nowMs(),response=result(api,auth,{native_capture:body.native_capture});
328
+ await guardedMutation(api,res,auth,authGuards(auth,now),[{sql:'UPDATE teams SET native_capture=? WHERE id=?',params:[body.native_capture?1:0,auth.team_id]}],{status:200,body:response,targetId:auth.team_id});
329
+ send(res,200,response,requestId);
330
+ }
320
331
  async function updateMember(api,req,res,auth,principalId,requestId){await requireUnlinked(api,auth.team_id);
321
332
  if(!isAdmin(auth))fail(403,'forbidden','forbidden');
322
333
  const body=await parseJson(req);exact(body,['role']);if(!['owner','admin','member','viewer'].includes(body.role))fail(400,'invalid_request','invalid team role');
@@ -385,12 +396,12 @@ async function heartbeat(api,req,res,auth,machineId,requestId){if(auth.kind!=='m
385
396
  async function machineConfiguration(api,res,auth,machineId,requestId){
386
397
  if(auth.kind!=='machine'||auth.machine_id!==machineId)fail(auth.kind==='machine'?404:403,auth.kind==='machine'?'not_found':'forbidden','not found');
387
398
  const [machine,rules]=await api.store.snapshot([
388
- {kind:'one',sql:'SELECT m.*,(SELECT linked FROM machine_presence p WHERE p.team_id=m.team_id AND p.machine_id=m.id) presence_linked,s.id source_id,s.name source_name,s.kind source_kind,s.state source_state,s.visibility,s.revision source_revision,s.last_received_at,s.decoder,s.counters_json,s.last_error,t.auth_epoch FROM machines m JOIN sources s ON s.team_id=m.team_id AND s.machine_id=m.id JOIN teams t ON t.id=m.team_id JOIN memberships mem ON mem.team_id=m.team_id AND mem.principal_id=m.custodian_id AND mem.active=1 WHERE m.team_id=? AND m.id=? AND m.revoked_at IS NULL',params:[auth.team_id,machineId]},
399
+ {kind:'one',sql:'SELECT m.*,(SELECT linked FROM machine_presence p WHERE p.team_id=m.team_id AND p.machine_id=m.id) presence_linked,s.id source_id,s.name source_name,s.kind source_kind,s.state source_state,s.visibility,s.revision source_revision,s.last_received_at,s.decoder,s.counters_json,s.last_error,t.auth_epoch,t.native_capture FROM machines m JOIN sources s ON s.team_id=m.team_id AND s.machine_id=m.id JOIN teams t ON t.id=m.team_id JOIN memberships mem ON mem.team_id=m.team_id AND mem.principal_id=m.custodian_id AND mem.active=1 WHERE m.team_id=? AND m.id=? AND m.revoked_at IS NULL',params:[auth.team_id,machineId]},
389
400
  {kind:'query',sql:'SELECT r.* FROM sync_rules r JOIN machines m ON m.team_id=r.team_id AND m.id=r.machine_id JOIN sources s ON s.team_id=r.team_id AND s.id=r.source_id WHERE r.team_id=? AND r.machine_id=? AND m.revoked_at IS NULL AND s.machine_id=m.id ORDER BY r.id',params:[auth.team_id,machineId]},
390
401
  ]);
391
402
  if(!machine)fail(404,'not_found','not found');
392
403
  if(machine.source_state==='revoked')fail(403,'source_revoked','source revoked');
393
- send(res,200,result(api,auth,{machine:machineJson(machine,api.clock.nowMs(),api),source:sourceJson({id:machine.source_id,name:machine.source_name,kind:machine.source_kind,state:machine.source_state,machine_id:machineId,custodian_id:machine.custodian_id,visibility:machine.visibility,revision:machine.source_revision,last_received_at:machine.last_received_at,decoder:machine.decoder,counters_json:machine.counters_json,last_error:machine.last_error}),rules:rules.map(syncRuleJson),custodian_id:machine.custodian_id,machine_grants:await machineGrantRows(api,auth.team_id,machineId),authorization_revision:machine.auth_epoch}),requestId);
404
+ send(res,200,result(api,auth,{machine:machineJson(machine,api.clock.nowMs(),api),source:sourceJson({id:machine.source_id,name:machine.source_name,kind:machine.source_kind,state:machine.source_state,machine_id:machineId,custodian_id:machine.custodian_id,visibility:machine.visibility,revision:machine.source_revision,last_received_at:machine.last_received_at,decoder:machine.decoder,counters_json:machine.counters_json,last_error:machine.last_error}),rules:rules.map(syncRuleJson),custodian_id:machine.custodian_id,machine_grants:await machineGrantRows(api,auth.team_id,machineId),authorization_revision:machine.auth_epoch,native_capture:machine.native_capture===1}),requestId);
394
405
  }
395
406
  const syncRuleJson=r=>({id:r.id,machine_id:r.machine_id,source_id:r.source_id,selector:JSON.parse(r.selector_json),visibility:r.visibility,backfill:JSON.parse(r.backfill_json),state:r.state,revision:r.revision,capture_after:r.capture_after});
396
407
  async function createSource(api,req,res,auth,requestId){if(!isAdmin(auth))fail(403,'forbidden','forbidden');const b=await parseJson(req);exact(b,['name','kind','custodian_id','visibility']);if(b.kind!=='otlp'||!['team','restricted'].includes(b.visibility))fail(400,'invalid_request','only otlp sources may be created');const id=api.ids.next('source'),now=api.clock.nowMs(),response=result(api,auth,{id,name:b.name,kind:'otlp',state:'active',machine_id:null,custodian_id:b.custodian_id,default_visibility:b.visibility,revision:1,last_received_at:null,decoder:null}),guards=[...authGuards(auth,now),{sql:'SELECT 1 ok FROM memberships WHERE team_id=? AND principal_id=? AND active=1',params:[auth.team_id,b.custodian_id],code:'invalid_request',status:400,message:'source custodian must be active'}];await guardedMutation(api,res,auth,guards,[{sql:"INSERT INTO sources(team_id,id,name,kind,custodian_id,visibility,state) VALUES (?,?,?,?,?,?,'active')",params:[auth.team_id,id,b.name,'otlp',b.custodian_id,b.visibility]}],{status:201,body:response,targetId:id});send(res,201,response,requestId);}
@@ -0,0 +1,14 @@
1
+ // A delivery that settled (delivered, failed, cancelled) is queued with its age, so retention removes week-old
2
+ // settled deliveries through that queue: a pending delivery never stands in front of them (t_6c01c966, #1120 review
3
+ // DO7). Constant cost at activation: a table, its index and two triggers; rows from before it are walked into the
4
+ // queue by `maintain` (its `deliveries` walk).
5
+ export default `
6
+ CREATE TABLE delivery_settled (id TEXT PRIMARY KEY, created_at INTEGER NOT NULL) STRICT;
7
+ CREATE INDEX delivery_settled_created ON delivery_settled(created_at);
8
+ CREATE TRIGGER delivery_settled_on_state AFTER UPDATE OF state ON app_webhook_deliveries WHEN NEW.state<>'pending' BEGIN
9
+ INSERT OR IGNORE INTO delivery_settled VALUES (NEW.id, NEW.created_at);
10
+ END;
11
+ CREATE TRIGGER delivery_settled_on_delete AFTER DELETE ON app_webhook_deliveries BEGIN
12
+ DELETE FROM delivery_settled WHERE id=OLD.id;
13
+ END;
14
+ `;
@@ -0,0 +1,6 @@
1
+ // A team chooses whether its machines upload each session's native transcript (the byte-exact file a harness resumes
2
+ // from) beside its messages. Off, the server refuses those uploads and retention releases the copies it holds; native
3
+ // continuation and export of a session then answer fidelity_unavailable. Constant cost at activation: one column.
4
+ export default `
5
+ ALTER TABLE teams ADD COLUMN native_capture INTEGER NOT NULL DEFAULT 1 CHECK(native_capture IN (0,1));
6
+ `;
@@ -22,7 +22,9 @@ import m020 from './020.mjs';
22
22
  import m021 from './021.mjs';
23
23
  import m022 from './022.mjs';
24
24
  import m023 from './023.mjs';
25
- export const MIGRATIONS = [m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011, m012, m013, m014, m015, m016, m017, m018, m019, m020, m021, m022, m023];
25
+ import m024 from './024.mjs';
26
+ import m025 from './025.mjs';
27
+ export const MIGRATIONS = [m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011, m012, m013, m014, m015, m016, m017, m018, m019, m020, m021, m022, m023, m024, m025];
26
28
  /** A message row's `until_revision` while its session's current version still holds it (schema 9). */
27
29
  export const OPEN_REVISION = Number.MAX_SAFE_INTEGER;
28
30
  /** SQL: message row `m` belongs to the version at revision ?1 (bind the revision twice). */