@volter/supercode-teams 0.2.148 → 0.2.149

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.148",
3
+ "version": "0.2.149",
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",
@@ -307,8 +307,8 @@ async function artifactChunk(api,req,res,auth,sourceId,uploadId,index,requestId)
307
307
  const blob = await api.store.one('SELECT 1 ok FROM artifact_blobs WHERE team_id=? AND sha256=?',[auth.team_id,b.sha256]);
308
308
  // Keep allocation headroom for heartbeats, messages and reclamation. Replays
309
309
  // above remain readable; a refused chunk stays in the connector's outbox.
310
- const usage = api.storageUsage?.();
311
- if (usage && usage.used + (blob ? 0 : bytes.length) >= usage.limit * 0.9) {
310
+ const usage = await api.storageUsage?.();
311
+ if (usage && (usage.used + (blob ? 0 : bytes.length) >= usage.limit * 0.9 || (!blob && usage.available_bytes != null && usage.available_bytes - bytes.length < (usage.min_free_bytes ?? 0)))) {
312
312
  res.setHeader('Retry-After', '60');
313
313
  fail(503, 'storage_pressure', 'native upload deferred while Teams storage is above its headroom threshold');
314
314
  }
package/server/index.mjs CHANGED
@@ -25,11 +25,11 @@ export function createTeamsServer({dataDir,listen={host:'127.0.0.1',port:7331},p
25
25
  // The harness's own routes on this server (harness-routes.mjs); the handler owns the neutral ones.
26
26
  api.attach({harnessRoutes});
27
27
  const events=api.events;
28
- let store,http,upgrade,lockFd,maintenanceTimer,started=false;
28
+ let store,http,upgrade,lockFd,maintenanceTimer,maintenanceWork,started=false;
29
29
  api.start=async()=>{
30
30
  if(started)throw new Error('Teams server already started'); mkdirSync(dataDir,{recursive:true,mode:0o700});chmodSync(dataDir,0o700);
31
31
  const lock=join(dataDir,'server.lock'); const acquire=()=>{lockFd=openSync(lock,'wx',0o600);writeFileSync(lockFd,JSON.stringify({pid:process.pid,started_at:clock.nowMs()}));};try{acquire();}catch(e){let recover=false;try{const owner=JSON.parse(readFileSync(lock,'utf8'));if(Number.isInteger(owner.pid)&&owner.pid>1){try{process.kill(owner.pid,0);}catch(error){recover=error.code==='ESRCH';}}}catch{}if(!recover)throw new Error(`Teams data directory is already owned: ${dataDir}`,{cause:e});unlinkSync(lock);acquire();}
32
- try { const dbPath=join(dataDir,'teams.sqlite'); store=new TeamsStore(dbPath); api.attach({store});
32
+ try { const dbPath=join(dataDir,'teams.sqlite'); store=new TeamsStore(dbPath,{storageLimitBytes:Number(process.env.TEAMS_STORAGE_LIMIT_BYTES)||10*1024**3,storageMinFreeBytes:Number(process.env.TEAMS_STORAGE_MIN_FREE_BYTES)||0}); api.attach({store}); api.storageUsage=()=>store.call('storageUsage');
33
33
  const existing=await store.one("SELECT value FROM server_meta WHERE key='server_id'");
34
34
  const credentialPath=join(dataDir,'bootstrap-credential.json'),pendingPath=join(dataDir,'bootstrap-credential.pending.json');
35
35
  let serverId;
@@ -43,10 +43,23 @@ export function createTeamsServer({dataDir,listen={host:'127.0.0.1',port:7331},p
43
43
  if(!existsSync(credentialPath)&&existsSync(pendingPath)){const pending=JSON.parse(readFileSync(pendingPath,'utf8'));if(pending.server_id!==serverId)throw new Error('bootstrap credential recovery does not match server identity');renameSync(pendingPath,credentialPath);chmodSync(credentialPath,0o600);}
44
44
  }
45
45
  api.attach({serverId});
46
- await maintain(api);maintenanceTimer=setInterval(()=>{void maintain(api).catch(()=>{});},60000);maintenanceTimer.unref?.();
46
+ const maintenance=async()=>{
47
+ let next=60000;
48
+ try{
49
+ const result=await maintain(api);next=result?.backlog?3000:60000;
50
+ if(!result?.backlog){const reclaimed=await store.call('reclaimStorage');if(reclaimed.after.allocated<reclaimed.before.allocated)console.log(`Teams storage reclaimed: ${JSON.stringify(reclaimed)}`);}
51
+ const usage=await api.storageUsage();
52
+ if(usage.used>=usage.limit*0.8||usage.available_bytes<usage.min_free_bytes+usage.limit*0.2)console.warn(`Teams storage pressure: ${JSON.stringify(usage)}; native admission stops at90% or the configured disk reserve`);
53
+ }catch(error){console.error(`Teams maintenance failed: ${error.message}`);next=3000;}
54
+ if(started){maintenanceTimer=setTimeout(()=>{maintenanceWork=maintenance();},next);maintenanceTimer.unref?.();}
55
+ return next;
56
+ };
57
+ const delay=await maintenance();
58
+ // The initial pass precedes listen; subsequent passes never overlap.
59
+ maintenanceTimer=setTimeout(()=>{maintenanceWork=maintenance();},delay);maintenanceTimer.unref?.();
47
60
  chmodSync(dbPath,0o600); http=createHttpServer(createHttpHandler(api));upgrade=createMachineUpgradeHandler(api,{authenticate});http.on('upgrade',upgrade); await new Promise((resolve,reject)=>{http.once('error',reject);http.listen(listen.port??0,listen.host??'127.0.0.1',resolve);});started=true;const addr=http.address();api.attach({localOrigin:`http://${addr.address.includes(':')?`[${addr.address}]`:addr.address}:${addr.port}`});return {address:`${addr.address}:${addr.port}`,server_id:serverId};
48
61
  } catch(e){if(maintenanceTimer){clearInterval(maintenanceTimer);maintenanceTimer=undefined;}await store?.close().catch(()=>{});store=null;api.attach({store:null});if(lockFd!==undefined){closeSync(lockFd);lockFd=undefined;}try{unlinkSync(lock);}catch{}throw e;}
49
62
  };
50
- api.close=async()=>{if(!started&&lockFd===undefined)return;started=false;if(maintenanceTimer){clearInterval(maintenanceTimer);maintenanceTimer=undefined;}events.close();closeAttachmentRelays(api,'access_revoked');upgrade?.close();await new Promise(resolve=>http?.close(()=>resolve()));await store?.close();store=null;if(lockFd!==undefined){closeSync(lockFd);lockFd=undefined;}try{unlinkSync(join(dataDir,'server.lock'));}catch{}api.attach({store:null});};
63
+ api.close=async()=>{if(!started&&lockFd===undefined)return;started=false;if(maintenanceTimer){clearInterval(maintenanceTimer);maintenanceTimer=undefined;}events.close();closeAttachmentRelays(api,'access_revoked');upgrade?.close();await new Promise(resolve=>http?.close(()=>resolve()));await maintenanceWork;await store?.close();store=null;if(lockFd!==undefined){closeSync(lockFd);lockFd=undefined;}try{unlinkSync(join(dataDir,'server.lock'));}catch{}api.attach({store:null});};
51
64
  return api;
52
65
  }
@@ -32,5 +32,5 @@ export async function storageReport({ api, res, auth, url, requestId }) {
32
32
  (SELECT COUNT(*) FROM artifact_blobs WHERE team_id=?) shared_chunks,
33
33
  (SELECT COUNT(*) FROM artifact_chunk_refs WHERE team_id=?) chunk_references,
34
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);
35
+ send(res, 200, result(api, auth, { items: rows, next: rows.length === 100 ? rows.at(-1).id : null, usage: await api.storageUsage?.() ?? null, chunks }), requestId);
36
36
  }
@@ -1,5 +1,6 @@
1
1
  import { parentPort, workerData, isMainThread, Worker } from 'node:worker_threads';
2
2
  import { DatabaseSync } from 'node:sqlite';
3
+ import { statfsSync,statSync } from 'node:fs';
3
4
  import { createStoreHandlers, migrate, StoreClient } from './store-core.mjs';
4
5
 
5
6
  // The Node host's store: node:sqlite in a worker thread, so a long statement never
@@ -12,12 +13,27 @@ if (!isMainThread) {
12
13
  const run = (sql, p=[]) => db.prepare(sql).run(...p);
13
14
  const tx = fn => { db.exec('BEGIN IMMEDIATE'); try { const out=fn(); db.exec('COMMIT'); return out; } catch(e) { db.exec('ROLLBACK'); throw e; } };
14
15
  migrate({ exec: (sql) => db.exec(sql), tx, version: () => db.prepare('PRAGMA user_version').get().user_version, setVersion: (n) => db.exec(`PRAGMA user_version=${n}`) });
15
- const handlers = { ...createStoreHandlers({ all, get, run, tx }), close: () => { db.close(); return true; } };
16
+ const usage=()=>{
17
+ const size=get('PRAGMA page_size').page_size,pages=get('PRAGMA page_count').page_count,free=get('PRAGMA freelist_count').freelist_count;
18
+ const disk=statfsSync(workerData.dbPath);let wal=0;try{wal=statSync(`${workerData.dbPath}-wal`).size;}catch{}
19
+ return {allocated:pages*size,free_bytes:free*size,used:(pages-free)*size,used_is_upper_bound:false,wal_bytes:wal,available_bytes:disk.bavail*disk.bsize,limit:workerData.storageLimitBytes,min_free_bytes:workerData.storageMinFreeBytes};
20
+ };
21
+ const reclaim=()=>{
22
+ db.exec('PRAGMA wal_checkpoint(PASSIVE)');
23
+ const before=usage();
24
+ // Legacy databases have auto_vacuum=NONE. Compact through the owning store
25
+ // after retention drains, with room for SQLite's temporary live-page copy.
26
+ if(before.free_bytes>=512*1024*1024&&before.free_bytes>before.allocated/2&&before.available_bytes>before.used+64*1024*1024){
27
+ db.exec('VACUUM');db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
28
+ }
29
+ return {before,after:usage()};
30
+ };
31
+ const handlers = { ...createStoreHandlers({ all, get, run, tx }), storageUsage:usage, reclaimStorage:reclaim, close: () => { db.close(); return true; } };
16
32
  parentPort.on('message', ({id,op,args}) => { try { parentPort.postMessage({id,result:handlers[op](args)}); } catch(error) { parentPort.postMessage({id,error:{message:error.message,code:error.code,status:error.status,stack:error.stack}}); } });
17
33
  }
18
34
 
19
35
  export class TeamsStore extends StoreClient {
20
- constructor(dbPath) { super(); this.worker=new Worker(new URL(import.meta.url),{workerData:{dbPath}}); this.next=1; this.pending=new Map(); this.worker.on('message',m=>{const p=this.pending.get(m.id); if(!p)return; this.pending.delete(m.id); m.error?p.reject(Object.assign(new Error(m.error.message),m.error)):p.resolve(m.result);}); this.worker.on('error',e=>{for(const p of this.pending.values())p.reject(e);this.pending.clear();}); }
36
+ constructor(dbPath,{storageLimitBytes=10*1024**3,storageMinFreeBytes=0}={}) { super(); this.worker=new Worker(new URL(import.meta.url),{workerData:{dbPath,storageLimitBytes,storageMinFreeBytes}}); this.next=1; this.pending=new Map(); this.worker.on('message',m=>{const p=this.pending.get(m.id); if(!p)return; this.pending.delete(m.id); m.error?p.reject(Object.assign(new Error(m.error.message),m.error)):p.resolve(m.result);}); this.worker.on('error',e=>{for(const p of this.pending.values())p.reject(e);this.pending.clear();}); }
21
37
  call(op,args={}) { return new Promise((resolve,reject)=>{const id=this.next++;this.pending.set(id,{resolve,reject});this.worker.postMessage({id,op,args});}); }
22
38
  async close() { await this.call('close'); await this.worker.terminate(); }
23
39
  }