@volter/supercode-teams 0.2.143 → 0.2.145
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 +15 -1
- package/machine.mjs +48 -2
- package/native-codex-pane.mjs +27 -5
- package/package.json +1 -1
- package/server/continuations.mjs +1 -1
- package/server/harness-routes.mjs +25 -12
- package/server/storage-report.mjs +7 -2
- package/server/store-core.mjs +1 -1
package/connector/index.mjs
CHANGED
|
@@ -751,7 +751,21 @@ export class TeamsConnector {
|
|
|
751
751
|
}
|
|
752
752
|
async sendCapture(item){const begun=await this.request('POST',`/sources/${this.sourceId}/captures`,item.begin);if(begun.state==='committed')return begun.receipt;for(const part of item.parts)await this.request('PUT',`/sources/${this.sourceId}/captures/${item.begin.capture_id}/parts/${part.index}`,part);
|
|
753
753
|
// A capture queued by an older connector holds its chunks inline; a current one reads them from its spill file.
|
|
754
|
-
for(const artifact of item.artifacts??[])for(const chunk of artifact.spill?this.spilledChunks(artifact):artifact.chunks){
|
|
754
|
+
for(const artifact of item.artifacts??[])for(const chunk of artifact.spill?this.spilledChunks(artifact):artifact.chunks){
|
|
755
|
+
if(!chunk)throw Object.assign(new Error('a spilled native artifact changed on disk while it was sent'),{code:'resync_required'});
|
|
756
|
+
const path=`/sources/${this.sourceId}/uploads/${chunk.upload_id}/chunks/${chunk.index}`;
|
|
757
|
+
let reused=false;
|
|
758
|
+
if(this.chunkReuse!==false)try{
|
|
759
|
+
await this.request('PUT',path,{upload_id:chunk.upload_id,index:chunk.index,sha256:chunk.sha256,reuse:true});reused=true;this.chunkReuse=true;
|
|
760
|
+
}catch(error){
|
|
761
|
+
if(error.code==='invalid_request'&&error.status===400)this.chunkReuse=false; // older server
|
|
762
|
+
else if(error.code!=='chunk_missing')throw error;
|
|
763
|
+
}
|
|
764
|
+
if(!reused)await this.request('PUT',path,chunk);
|
|
765
|
+
this.state.native_transfer??={sent_bytes:0,reused_bytes:0,chunks:0};
|
|
766
|
+
this.state.native_transfer[reused?'reused_bytes':'sent_bytes']+=Buffer.byteLength(chunk.bytes_base64,'base64');
|
|
767
|
+
this.state.native_transfer.chunks++;
|
|
768
|
+
}
|
|
755
769
|
return this.request('POST',`/sources/${this.sourceId}/captures/${item.begin.capture_id}/commit`,item.commit);}
|
|
756
770
|
// True when the server refused a queued capture because its sync rule was removed, paused or revised since it
|
|
757
771
|
// was queued: the capture can never be accepted, so it must not hold every later capture behind it. The
|
package/machine.mjs
CHANGED
|
@@ -198,6 +198,8 @@ export class TeamsMachine extends MachineCore {
|
|
|
198
198
|
// `stop()` can abandon it rather than leave a timer running past the daemon.
|
|
199
199
|
this.voiceProcesses = new Map(); // session address -> optional voice pack child
|
|
200
200
|
this.inputInFlight = new Set(); // one input transaction per pane
|
|
201
|
+
this.inputRepairsPath = join(this.home, 'input-repairs.json');
|
|
202
|
+
this.inputRepairs = readJson(this.inputRepairsPath, {});
|
|
201
203
|
this.pendingInitialInputs = new Map(); // launch key → { abort, task }
|
|
202
204
|
this.pendingStartups = new Map();
|
|
203
205
|
this.registerDoor('harness', (entry, stream, accept, refuse, index) => this.#serveHarness(index, entry, stream, accept, refuse));
|
|
@@ -223,9 +225,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
223
225
|
|
|
224
226
|
async afterListen() {
|
|
225
227
|
this.#startMailWatch();
|
|
228
|
+
if(Object.keys(this.inputRepairs).length)this.#watchInputRepairs();
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
async beforeClose() {
|
|
232
|
+
clearInterval(this.inputRepairTimer);this.inputRepairTimer=null;
|
|
233
|
+
await this.inputRepairWork;
|
|
229
234
|
clearInterval(this.endWatch); this.endWatch = null;
|
|
230
235
|
// A launch still waiting for its pane to be ready is abandoned, not hurried:
|
|
231
236
|
// this daemon is going away, and shoving a message at a half-booted TUI on the
|
|
@@ -237,6 +242,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
237
242
|
this.pendingStartups.clear();
|
|
238
243
|
await Promise.allSettled([...this.pendingInitialInputs.values()].map(({ task }) => task));
|
|
239
244
|
this.pendingInitialInputs.clear();
|
|
245
|
+
clearInterval(this.inputRepairTimer);this.inputRepairTimer=null;
|
|
240
246
|
}
|
|
241
247
|
|
|
242
248
|
async afterStop() {
|
|
@@ -886,13 +892,21 @@ export class TeamsMachine extends MachineCore {
|
|
|
886
892
|
}
|
|
887
893
|
|
|
888
894
|
async #deliverPaneText(session, contextKey, text, options = {}) {
|
|
895
|
+
if(this.inputRepairs[contextKey])return {delivered:false,reason:'input-cleanup-pending',cleanup_required:true};
|
|
889
896
|
if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
|
|
890
897
|
this.inputInFlight.add(session.id);
|
|
891
898
|
try {
|
|
892
899
|
const kind = this.#paneKind(contextKey, session);
|
|
893
900
|
if (kind === 'claude-code' || kind === 'codex') {
|
|
894
901
|
if (typeof terminal.deliverComposerInput !== 'function') return { delivered: false, reason: 'terminal-package-too-old' };
|
|
895
|
-
|
|
902
|
+
const result=await terminal.deliverComposerInput(this.host, session.id, kind, submission(text, false), options);
|
|
903
|
+
if(result.cleanup_required){
|
|
904
|
+
this.inputRepairs[contextKey]={session:session.id,kind,text:result.cleanup_remaining??submission(text,false),at:Date.now(),reason:result.cleanup_reason};
|
|
905
|
+
writeJson(this.inputRepairsPath,this.inputRepairs);this.#watchInputRepairs();
|
|
906
|
+
this.log.append({principal:null,cap:'control',target:`pane:${contextKey}`,outcome:'input-cleanup-pending',detail:result.cleanup_reason});
|
|
907
|
+
}
|
|
908
|
+
const {cleanup_remaining,...receipt}=result;
|
|
909
|
+
return receipt;
|
|
896
910
|
}
|
|
897
911
|
const delivery = await this.host.sendInput(session.id, submission(text), { ...options, submitMode: 'single' });
|
|
898
912
|
return { delivered: true, delivery };
|
|
@@ -903,6 +917,38 @@ export class TeamsMachine extends MachineCore {
|
|
|
903
917
|
}
|
|
904
918
|
}
|
|
905
919
|
|
|
920
|
+
#watchInputRepairs() {
|
|
921
|
+
if(this.inputRepairTimer)return;
|
|
922
|
+
this.inputRepairTimer=setInterval(()=>{
|
|
923
|
+
if(this.inputRepairWork)return;
|
|
924
|
+
this.inputRepairWork=this.#repairInputs().catch(error=>this.emit('mail-watch-stderr',`input cleanup: ${error.message}`)).finally(()=>{this.inputRepairWork=null;});
|
|
925
|
+
},2000);
|
|
926
|
+
this.inputRepairTimer.unref?.();
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async #repairInputs() {
|
|
930
|
+
for(const [pane,repair] of Object.entries(this.inputRepairs)) {
|
|
931
|
+
if(this.inputInFlight.has(repair.session)||this.suspended.has(pane))continue;
|
|
932
|
+
const session=await this.#paneByContext(pane);
|
|
933
|
+
if(!session||session.id!==repair.session||session.dead){
|
|
934
|
+
delete this.inputRepairs[pane];writeJson(this.inputRepairsPath,this.inputRepairs);
|
|
935
|
+
this.log.append({principal:null,cap:'control',target:`pane:${pane}`,outcome:'input-cleanup-pane-gone'});continue;
|
|
936
|
+
}
|
|
937
|
+
if(!terminal.restoreComposerInput)continue;
|
|
938
|
+
this.inputInFlight.add(session.id);
|
|
939
|
+
try {
|
|
940
|
+
const result=await terminal.restoreComposerInput(this.host,session.id,repair.kind,repair.text);
|
|
941
|
+
if(result.restored){
|
|
942
|
+
delete this.inputRepairs[pane];writeJson(this.inputRepairsPath,this.inputRepairs);
|
|
943
|
+
this.log.append({principal:null,cap:'control',target:`pane:${pane}`,outcome:'input-cleanup-restored'});
|
|
944
|
+
}else if(result.remaining&&result.remaining!==repair.text){
|
|
945
|
+
repair.text=result.remaining;repair.reason=result.reason;writeJson(this.inputRepairsPath,this.inputRepairs);
|
|
946
|
+
}
|
|
947
|
+
}finally{this.inputInFlight.delete(session.id);}
|
|
948
|
+
}
|
|
949
|
+
if(!Object.keys(this.inputRepairs).length){clearInterval(this.inputRepairTimer);this.inputRepairTimer=null;}
|
|
950
|
+
}
|
|
951
|
+
|
|
906
952
|
/** The program a pane runs, as the terminal package names kinds: what it was
|
|
907
953
|
* launched as, else what its foreground command is. */
|
|
908
954
|
#paneKind(contextKey, session) {
|
|
@@ -1338,7 +1384,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1338
1384
|
// `title` is what a viewer should show: the override if one is set, else the tmux label.
|
|
1339
1385
|
return rows.filter((row) => row.session.id && shown(row.session.contextKey)).map(({ session: s, attention, rank, notify, evidence }) => ({
|
|
1340
1386
|
pane: s.contextKey, dead: Boolean(s.dead), label: s.label, title: titles[`pane:${s.contextKey}`] ?? s.label, cwd: s.cwd, owned: s.owned, activeCommand: s.activeCommand, pid: s.rootPid ?? null, size: s.size, suspended: this.suspended.has(s.contextKey),
|
|
1341
|
-
attention, rank, notify, evidence,
|
|
1387
|
+
attention, rank, notify, evidence, input_cleanup: this.inputRepairs[s.contextKey] ? {pending:true,at:this.inputRepairs[s.contextKey].at,reason:this.inputRepairs[s.contextKey].reason} : null,
|
|
1342
1388
|
}));
|
|
1343
1389
|
}
|
|
1344
1390
|
case 'launch': {
|
package/native-codex-pane.mjs
CHANGED
|
@@ -11,14 +11,36 @@ const seed = JSON.parse(readFileSync(path, 'utf8'));
|
|
|
11
11
|
const directory = mkdtempSync(join(tmpdir(), 'sc-codex-'));
|
|
12
12
|
const socketPath = join(directory, 'native.sock');
|
|
13
13
|
const endpoint = `unix://${socketPath}`;
|
|
14
|
-
// Configuration is evaluated by the native server.
|
|
15
|
-
//
|
|
14
|
+
// Configuration is evaluated by the native server. Remote resume/fork refuses
|
|
15
|
+
// permission overrides on the TUI command: translate those existing choices
|
|
16
|
+
// into server configuration without changing the requested policy.
|
|
16
17
|
const config = [];
|
|
18
|
+
const tuiArguments = [];
|
|
19
|
+
const continuing = seed.arguments.includes('resume') || seed.arguments.includes('fork');
|
|
20
|
+
const writableRoots = [];
|
|
17
21
|
for (let index = 0; index < seed.arguments.length; index += 1) {
|
|
18
22
|
const arg = seed.arguments[index];
|
|
19
|
-
if (
|
|
20
|
-
|
|
23
|
+
if (continuing && ['--dangerously-bypass-approvals-and-sandbox', '--yolo'].includes(arg)) {
|
|
24
|
+
config.push('-c', 'approval_policy="never"', '-c', 'sandbox_mode="danger-full-access"');
|
|
25
|
+
} else if (continuing && arg === '--full-auto') {
|
|
26
|
+
config.push('-c', 'approval_policy="on-request"', '-c', 'sandbox_mode="workspace-write"');
|
|
27
|
+
} else if (continuing && ['-a', '--ask-for-approval', '-s', '--sandbox', '--add-dir'].includes(arg)) {
|
|
28
|
+
const value = seed.arguments[++index];
|
|
29
|
+
if (value === undefined) throw new Error(`${arg} requires a value`);
|
|
30
|
+
if (arg === '--add-dir') writableRoots.push(value);
|
|
31
|
+
else config.push('-c', `${['-a','--ask-for-approval'].includes(arg)?'approval_policy':'sandbox_mode'}=${JSON.stringify(value)}`);
|
|
32
|
+
} else if (continuing && /^--(?:ask-for-approval|sandbox|add-dir)=/.test(arg)) {
|
|
33
|
+
const at=arg.indexOf('='), name=arg.slice(0,at), value=arg.slice(at+1);
|
|
34
|
+
if(name==='--add-dir')writableRoots.push(value);
|
|
35
|
+
else config.push('-c', `${name==='--sandbox'?'sandbox_mode':'approval_policy'}=${JSON.stringify(value)}`);
|
|
36
|
+
} else {
|
|
37
|
+
tuiArguments.push(arg);
|
|
38
|
+
if (['-c', '--config', '--enable', '--disable'].includes(arg)) {
|
|
39
|
+
const value=seed.arguments[++index];config.push(arg,value);tuiArguments.push(value);
|
|
40
|
+
} else if (/^--(?:config|enable|disable)=/.test(arg)) config.push(arg);
|
|
41
|
+
}
|
|
21
42
|
}
|
|
43
|
+
if(writableRoots.length)config.push('-c',`sandbox_workspace_write.writable_roots=${JSON.stringify(writableRoots)}`);
|
|
22
44
|
const server = spawn(seed.program, ['app-server', '--listen', endpoint, ...config], { stdio: ['ignore', 'ignore', 'inherit'] });
|
|
23
45
|
let tui;
|
|
24
46
|
let ended = false;
|
|
@@ -38,7 +60,7 @@ try {
|
|
|
38
60
|
const staged = `${path}.${process.pid}`;
|
|
39
61
|
writeFileSync(staged, JSON.stringify({ ...seed, socketPath, ownerPid: process.pid }), { mode: 0o600 });
|
|
40
62
|
renameSync(staged, path);
|
|
41
|
-
const args = [...
|
|
63
|
+
const args = [...tuiArguments, '--remote', endpoint];
|
|
42
64
|
if (!args.some((arg) => arg === '-C' || arg === '--cd' || arg.startsWith('--cd='))) args.push('--cd', process.cwd());
|
|
43
65
|
if (seed.initialInput) args.push('--', seed.initialInput);
|
|
44
66
|
tui = spawn(seed.program, args, { stdio: 'inherit' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.145",
|
|
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",
|
package/server/continuations.mjs
CHANGED
|
@@ -53,4 +53,4 @@ async function authorizedRemote(api,auth,machineId,effectId,incarnation,routeFai
|
|
|
53
53
|
|
|
54
54
|
export async function remoteContinuationRoute({api,req,res,auth,machineId,effectId,requestId,fail,send,result}){const {value}=await authorizedRemote(api,auth,machineId,effectId,req.headers['x-teams-incarnation'],fail);const ticket={kind:'remote_launch',continuation_id:value.id,operation_id:value.operation_id,harness:value.harness,fidelity:value.fidelity,artifact:value.artifact,destination:value.destination,expires_at:Math.min(value.expires_at,value.destination_authorization.expires_at)};return send(res,200,result(api,auth,ticket),requestId);}
|
|
55
55
|
|
|
56
|
-
export async function remoteContinuationArtifactRoute({api,req,res,auth,machineId,effectId,uploadId,requestId,fail}){const {value}=await authorizedRemote(api,auth,machineId,effectId,req.headers['x-teams-incarnation'],fail);const file=value.artifact.files.find(item=>item.upload_id===uploadId);if(!file)fail(404,'not_found','artifact is not in the continuation bundle');const rows=await api.store.query('SELECT bytes FROM
|
|
56
|
+
export async function remoteContinuationArtifactRoute({api,req,res,auth,machineId,effectId,uploadId,requestId,fail}){const {value}=await authorizedRemote(api,auth,machineId,effectId,req.headers['x-teams-incarnation'],fail);const file=value.artifact.files.find(item=>item.upload_id===uploadId);if(!file)fail(404,'not_found','artifact is not in the continuation bundle');const rows=await api.store.query('SELECT bytes FROM artifact_chunk_data WHERE team_id=? AND upload_id=? ORDER BY chunk_index',[auth.team_id,uploadId]),bytes=Buffer.concat(rows.map(row=>row.bytes));if(bytes.length!==file.byte_length||createHash('sha256').update(bytes).digest('hex')!==file.sha256)fail(409,'conflict','artifact bytes changed');let start=0,end=bytes.length-1,status=200;const range=req.headers.range;if(range!==undefined){const match=/^bytes=(\d*)-(\d*)$/.exec(range);if(!match||(!match[1]&&!match[2]))fail(416,'invalid_request','invalid artifact range');if(match[1]){start=Number(match[1]);end=match[2]?Number(match[2]):end;}else{const suffix=Number(match[2]);start=Math.max(0,bytes.length-suffix);}if(!Number.isSafeInteger(start)||!Number.isSafeInteger(end)||start<0||end<start||start>=bytes.length)fail(416,'invalid_request','artifact range is unsatisfiable');end=Math.min(end,bytes.length-1);status=206;}const body=bytes.subarray(start,end+1);res.writeHead(status,{'Content-Type':file.media_type,'Content-Length':String(body.length),'Accept-Ranges':'bytes','Cache-Control':'no-store','X-Request-ID':requestId,...(status===206?{'Content-Range':`bytes ${start}-${end}/${bytes.length}`}:{})});res.end(body);}
|
|
@@ -277,39 +277,52 @@ async function capturePart(api,req,res,auth,sourceId,captureId,index,requestId){
|
|
|
277
277
|
async function artifactChunk(api,req,res,auth,sourceId,uploadId,index,requestId){
|
|
278
278
|
await sourceAuth(api,auth,sourceId);
|
|
279
279
|
const b=await parseJson(req);
|
|
280
|
-
exact(b,['upload_id','index','sha256','bytes_base64']);
|
|
280
|
+
exact(b,['upload_id','index','sha256','bytes_base64','reuse']);
|
|
281
|
+
if (!/^[a-f0-9]{64}$/.test(b.sha256 ?? '') || (b.reuse !== undefined && b.reuse !== true) || (b.reuse === true && b.bytes_base64 !== undefined)) fail(400,'invalid_request','invalid chunk digest or reuse request');
|
|
281
282
|
if(b.upload_id!==uploadId||b.index!==index)fail(400,'invalid_request','upload path/body mismatch');
|
|
282
|
-
let bytes;
|
|
283
|
-
try{bytes=Buffer.from(b.bytes_base64,'base64');}catch{fail(400,'invalid_request','invalid base64');}
|
|
284
|
-
if(bytes.length>1048576)fail(413,'request_too_large','artifact chunk exceeds 1 MiB');
|
|
285
|
-
if(sha256(bytes)!==b.sha256)fail(409,'capture_conflict','artifact chunk digest differs');
|
|
286
283
|
const expired=await api.store.one('SELECT id FROM capture_expiries WHERE team_id=? AND source_id=? AND id=?',[auth.team_id,sourceId,uploadId]);
|
|
287
284
|
if(expired)fail(409,'resync_required','upload reservation expired');
|
|
288
285
|
const upload=await api.store.one('SELECT * FROM uploads WHERE team_id=? AND id=?',[auth.team_id,uploadId]);
|
|
289
286
|
if(await expireStaging(api,auth.team_id,sourceId,uploadId,'uploads',upload))fail(409,'resync_required','upload reservation expired');
|
|
290
287
|
if(upload&&upload.source_id!==sourceId)fail(409,'capture_conflict','upload belongs to another source');
|
|
291
|
-
const old=await api.store.one('SELECT sha256 FROM
|
|
288
|
+
const old=await api.store.one('SELECT sha256 FROM artifact_chunk_data WHERE team_id=? AND upload_id=? AND chunk_index=?',[auth.team_id,uploadId,index]);
|
|
292
289
|
if(old){if(old.sha256!==b.sha256)fail(409,'capture_conflict','artifact chunk replay differs');return send(res,200,result(api,auth,{index,sha256:b.sha256}),requestId);}
|
|
290
|
+
let bytes;
|
|
291
|
+
if (b.reuse === true) {
|
|
292
|
+
// A source may reuse only bytes it already uploaded. Knowing another
|
|
293
|
+
// source's content hash never grants read access to its native history.
|
|
294
|
+
const held = await api.store.one(`SELECT c.bytes FROM artifact_chunk_data c
|
|
295
|
+
JOIN uploads u ON u.team_id=c.team_id AND u.id=c.upload_id
|
|
296
|
+
WHERE c.team_id=? AND c.sha256=? AND u.source_id=? LIMIT 1`, [auth.team_id,b.sha256,sourceId]);
|
|
297
|
+
if (!held) fail(404,'chunk_missing','this source has no retained chunk with that digest');
|
|
298
|
+
bytes = Buffer.from(held.bytes);
|
|
299
|
+
} else {
|
|
300
|
+
try { bytes=Buffer.from(b.bytes_base64,'base64'); } catch { fail(400,'invalid_request','invalid base64'); }
|
|
301
|
+
}
|
|
302
|
+
if(bytes.length>1048576)fail(413,'request_too_large','artifact chunk exceeds 1 MiB');
|
|
303
|
+
if(sha256(bytes)!==b.sha256)fail(409,'capture_conflict','artifact chunk digest differs');
|
|
304
|
+
const blob = await api.store.one('SELECT 1 ok FROM artifact_blobs WHERE team_id=? AND sha256=?',[auth.team_id,b.sha256]);
|
|
293
305
|
// Keep allocation headroom for heartbeats, messages and reclamation. Replays
|
|
294
306
|
// above remain readable; a refused chunk stays in the connector's outbox.
|
|
295
307
|
const usage = api.storageUsage?.();
|
|
296
|
-
if (usage && usage.used + bytes.length >= usage.limit * 0.9) {
|
|
308
|
+
if (usage && usage.used + (blob ? 0 : bytes.length) >= usage.limit * 0.9) {
|
|
297
309
|
res.setHeader('Retry-After', '60');
|
|
298
310
|
fail(503, 'storage_pressure', 'native upload deferred while Teams storage is above its headroom threshold');
|
|
299
311
|
}
|
|
300
|
-
const now=api.clock.nowMs(),response=result(api,auth,{index,sha256:b.sha256});
|
|
312
|
+
const now=api.clock.nowMs(),response=result(api,auth,{index,sha256:b.sha256,reused:b.reuse===true,stored_bytes:blob?0:bytes.length});
|
|
301
313
|
const guards=[
|
|
302
|
-
{sql:"SELECT 1 ok WHERE EXISTS (SELECT 1 FROM credentials c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_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 NOT EXISTS (SELECT 1 FROM capture_expiries WHERE team_id=? AND source_id=? AND id=?) AND NOT EXISTS (SELECT 1 FROM uploads WHERE team_id=? AND id=? AND source_id<>?) AND COALESCE((SELECT committed FROM uploads WHERE team_id=? AND id=?),0)=0 AND COALESCE((SELECT created_at+86400000>? FROM uploads WHERE team_id=? AND id=?),1) AND (SELECT count(*) FROM
|
|
303
|
-
{sql:'SELECT 1 ok WHERE COALESCE((SELECT sum(length(bytes)) FROM
|
|
314
|
+
{sql:"SELECT 1 ok WHERE EXISTS (SELECT 1 FROM credentials c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_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 NOT EXISTS (SELECT 1 FROM capture_expiries WHERE team_id=? AND source_id=? AND id=?) AND NOT EXISTS (SELECT 1 FROM uploads WHERE team_id=? AND id=? AND source_id<>?) AND COALESCE((SELECT committed FROM uploads WHERE team_id=? AND id=?),0)=0 AND COALESCE((SELECT created_at+86400000>? FROM uploads WHERE team_id=? AND id=?),1) AND (SELECT count(*) FROM artifact_chunk_data WHERE team_id=? AND upload_id=?)=?",params:[auth.credential_id,sourceId,now,auth.team_id,sourceId,uploadId,auth.team_id,uploadId,sourceId,auth.team_id,uploadId,now,auth.team_id,uploadId,auth.team_id,uploadId,index],code:'batch_gap',status:409,message:'upload authorization, expiry, or sequence changed'},
|
|
315
|
+
{sql:'SELECT 1 ok WHERE COALESCE((SELECT sum(length(bytes)) FROM artifact_chunk_data WHERE team_id=? AND upload_id=?),0)+?<=?',params:[auth.team_id,uploadId,bytes.length,api.uploadLimitBytes],code:'request_too_large',status:413,message:'artifact exceeds configured upload cap'}
|
|
304
316
|
];
|
|
305
317
|
await guardedMutation(api,res,auth,guards,[
|
|
306
318
|
{sql:'INSERT OR IGNORE INTO uploads(team_id,id,source_id,created_at) VALUES (?,?,?,?)',params:[auth.team_id,uploadId,sourceId,now]},
|
|
307
|
-
{sql:'INSERT INTO
|
|
319
|
+
{sql:'INSERT OR IGNORE INTO artifact_blobs(team_id,sha256,bytes) VALUES (?,?,?)',params:[auth.team_id,b.sha256,bytes]},
|
|
320
|
+
{sql:'INSERT INTO artifact_chunk_refs(team_id,upload_id,chunk_index,sha256) VALUES (?,?,?,?)',params:[auth.team_id,uploadId,index,b.sha256]},
|
|
308
321
|
],{status:200,body:response,targetId:uploadId});
|
|
309
322
|
send(res,200,response,requestId);
|
|
310
323
|
}
|
|
311
324
|
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');
|
|
312
|
-
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
|
|
325
|
+
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_chunk_data 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');}
|
|
313
326
|
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');}
|
|
314
327
|
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]);
|
|
315
328
|
// A capture that carries (schema 9's wire half) names its first carry.count messages by digest instead of sending
|
|
@@ -20,12 +20,17 @@ export async function storageReport({ api, res, auth, url, requestId }) {
|
|
|
20
20
|
p.native_id,p.harness,p.captured_at,
|
|
21
21
|
json_extract(u.manifest_json,'$.role') role,json_extract(u.manifest_json,'$.media_type') media_type,
|
|
22
22
|
json_extract(u.manifest_json,'$.sha256') sha256,
|
|
23
|
-
(SELECT COALESCE(SUM(length(bytes)),0) FROM
|
|
23
|
+
(SELECT COALESCE(SUM(length(bytes)),0) FROM artifact_chunk_data c WHERE c.team_id=u.team_id AND c.upload_id=u.id) bytes,
|
|
24
24
|
EXISTS (SELECT 1 FROM session_artifacts a WHERE a.team_id=u.team_id AND a.upload_id=u.id) version_referenced,
|
|
25
25
|
EXISTS (SELECT 1 FROM continuations c WHERE c.team_id=u.team_id AND instr(c.value_json,u.id)>0) continuation_referenced,
|
|
26
26
|
EXISTS (SELECT 1 FROM captures c WHERE c.team_id=u.team_id AND c.state='staging' AND instr(c.begin_json,u.id)>0) staging_referenced
|
|
27
27
|
FROM page u JOIN sources s ON s.team_id=u.team_id AND s.id=u.source_id
|
|
28
28
|
LEFT JOIN machines m ON m.team_id=s.team_id AND m.id=s.machine_id
|
|
29
29
|
LEFT JOIN provenance p ON p.team_id=u.team_id AND p.upload_id=u.id ORDER BY u.id`, [auth.team_id, after, auth.team_id]);
|
|
30
|
-
|
|
30
|
+
const chunks = await api.store.one(`SELECT
|
|
31
|
+
(SELECT COALESCE(SUM(length(bytes)),0) FROM artifact_blobs WHERE team_id=?) shared_bytes,
|
|
32
|
+
(SELECT COUNT(*) FROM artifact_blobs WHERE team_id=?) shared_chunks,
|
|
33
|
+
(SELECT COUNT(*) FROM artifact_chunk_refs WHERE team_id=?) chunk_references,
|
|
34
|
+
(SELECT COALESCE(SUM(length(bytes)),0) FROM artifact_chunks WHERE team_id=?) legacy_bytes`, Array(4).fill(auth.team_id));
|
|
35
|
+
send(res, 200, result(api, auth, { items: rows, next: rows.length === 100 ? rows.at(-1).id : null, usage: api.storageUsage?.() ?? null, chunks }), requestId);
|
|
31
36
|
}
|
package/server/store-core.mjs
CHANGED
|
@@ -111,7 +111,7 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
111
111
|
const upload=get(`SELECT u.manifest_json FROM session_artifacts a JOIN uploads u ON u.team_id=a.team_id AND u.id=a.upload_id
|
|
112
112
|
WHERE a.team_id=? AND a.session_id=? AND a.upload_id=? AND u.committed=1`,[args.teamId,session.id,args.uploadId]);
|
|
113
113
|
if(!upload)domainError(404,'not_found','artifact not found');
|
|
114
|
-
const chunks=all('SELECT bytes FROM
|
|
114
|
+
const chunks=all('SELECT bytes FROM artifact_chunk_data WHERE team_id=? AND upload_id=? ORDER BY chunk_index',[args.teamId,args.uploadId]);
|
|
115
115
|
return {manifest:JSON.parse(upload.manifest_json),bytes:Buffer.concat(chunks.map(row=>row.bytes))};
|
|
116
116
|
});
|
|
117
117
|
return {
|