@volter/supercode-teams 0.2.140 → 0.2.142

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.
@@ -38,9 +38,9 @@ export interface TeamsConnectorOptions {
38
38
  fetch?: typeof globalThis.fetch;
39
39
  /** Milliseconds between heartbeats, sent on their own timer independent of the capture pass (default 15000). */
40
40
  heartbeatMs?: number;
41
- /** A native file at least nativeIdleBytes long gets its native bundle only after this long without a write (default 60000). */
41
+ /** A native file gets its native bundle only after this long without a write (default 60000). */
42
42
  nativeIdleMs?: number;
43
- /** Size from which a native file's bundle waits for it to be idle (default 8 MiB). */
43
+ /** Size from which translated native bundles are cached locally (default 8 MiB). */
44
44
  nativeIdleBytes?: number;
45
45
  }
46
46
 
@@ -72,7 +72,8 @@ const CHUNK_BYTES=1024*1024;
72
72
  // harness server's only request reader while it runs; a live Claude Code transcript is appended to on every
73
73
  // message and tool result, so translating it mid-turn is wasted (the file has moved by the time it finishes).
74
74
  // A minute is four beats with no write: the turn is over, or waiting on the user or one long tool call.
75
- // Smaller files translate in well under a second and keep getting a bundle with every capture.
75
+ // Native bundles wait for a quiet file at every size; uploading a whole small
76
+ // transcript on every live capture also grows quadratically with the session.
76
77
  const NATIVE_IDLE_MS=60000,NATIVE_IDLE_BYTES=8*1024*1024;
77
78
  // Loading a transcript reads all of it, and a live one changes on every message: a 135 MB Claude transcript took
78
79
  // over 30 s a load on a Windows PC, on every beat, and translating it held the harness server past 272 s while every
@@ -405,11 +406,11 @@ export class TeamsConnector {
405
406
  return false;
406
407
  }
407
408
  // Whether a capture may attach the native bundle of a file in this state: its translation is cached (and did
408
- // not fail), or it is small, or it has been idle for nativeIdleMs.
409
+ // not fail), or it has been idle for nativeIdleMs.
409
410
  nativeReady(key,stat){
410
411
  const cached=this.state.native_cache[key];
411
412
  if(cached?.stamp===fileStamp(stat))return !cached.failed;
412
- return stat.size<this.nativeIdleBytes||this.clock.nowMs()-stat.mtimeMs>=this.nativeIdleMs;
413
+ return this.clock.nowMs()-stat.mtimeMs>=this.nativeIdleMs;
413
414
  }
414
415
  // Native artifact bytes are spilled to content-addressed files under the state directory, never into
415
416
  // connector.json: a 129 MB transcript was ~172 MB of base64 held in the outbox and rewritten on every save().
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.140",
3
+ "version": "0.2.142",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",
@@ -7,6 +7,7 @@
7
7
  // idempotency check, and last among a team's routes. Each hook returns true when it
8
8
  // answered the request.
9
9
  import { bearer, hashToken, canRead, canManage, isAdmin } from '@volter/teams/server/auth';
10
+ import { storageReport } from './storage-report.mjs';
10
11
  import { validateListQuery, validateSearchQuery, validateReadQuery, validateCaptureBegin, validateCapturePart, canonicalJson, carryDigest } from '../contracts/index.mjs';
11
12
  import { normalizeSearch, textFromContent, cursorHash, cursorToken } from './catalog.mjs';
12
13
  import { captureDigest, captureMessages, handleOtlpRoute, sha256, validateArtifactName } from './ingest.mjs';
@@ -77,6 +78,7 @@ async function beforeIdempotency({ api, req, res, auth, url, path, sub, requestI
77
78
 
78
79
  async function team({ api, req, res, auth, url, path, sub, requestId }) {
79
80
  let m;
81
+ if(req.method==='GET'&&sub==='/storage/uploads'){await storageReport({api,res,auth,url,requestId});return true;}
80
82
  if(req.method==='GET'&&sub==='/sessions'){await listSessions(api,res,auth,url,requestId,false);return true;}
81
83
  if(req.method==='GET'&&sub==='/sessions/search'){await listSessions(api,res,auth,url,requestId,true);return true;}
82
84
  if(req.method==='GET'&&sub==='/sessions/events'){await handleCatalogEvents({api,req,res,auth,url,requestId,qobj,canReadMerged,readerProjection,fail});return true;}
@@ -288,6 +290,13 @@ async function artifactChunk(api,req,res,auth,sourceId,uploadId,index,requestId)
288
290
  if(upload&&upload.source_id!==sourceId)fail(409,'capture_conflict','upload belongs to another source');
289
291
  const old=await api.store.one('SELECT sha256 FROM artifact_chunks WHERE team_id=? AND upload_id=? AND chunk_index=?',[auth.team_id,uploadId,index]);
290
292
  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);}
293
+ // Keep allocation headroom for heartbeats, messages and reclamation. Replays
294
+ // above remain readable; a refused chunk stays in the connector's outbox.
295
+ const usage = api.storageUsage?.();
296
+ if (usage && usage.used + bytes.length >= usage.limit * 0.9) {
297
+ res.setHeader('Retry-After', '60');
298
+ fail(503, 'storage_pressure', 'native upload deferred while Teams storage is above its headroom threshold');
299
+ }
291
300
  const now=api.clock.nowMs(),response=result(api,auth,{index,sha256:b.sha256});
292
301
  const guards=[
293
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 artifact_chunks 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'},
@@ -0,0 +1,31 @@
1
+ // Administrative, read-only retention inventory. No artifact bytes, native
2
+ // paths, prompts or credentials leave this door; cursors never write storage.
3
+ import { isAdmin } from '@volter/teams/server/auth';
4
+ import { fail, result, send } from '@volter/teams/server/http';
5
+
6
+ export async function storageReport({ api, res, auth, url, requestId }) {
7
+ if (!isAdmin(auth)) fail(403, 'forbidden', 'storage inventory requires a team administrator');
8
+ for (const key of url.searchParams.keys()) if (key !== 'after') fail(400, 'invalid_request', `unknown query field ${key}`);
9
+ const after = url.searchParams.get('after') ?? '';
10
+ if (after && !/^upload_[a-zA-Z0-9_-]{1,100}$/.test(after)) fail(400, 'invalid_request', 'invalid upload cursor');
11
+ const rows = await api.store.query(`WITH page AS MATERIALIZED (
12
+ SELECT * FROM uploads WHERE team_id=? AND id>? ORDER BY id LIMIT 100
13
+ ), provenance AS MATERIALIZED (
14
+ SELECT c.team_id,json_extract(a.value,'$.upload_id') upload_id,
15
+ c.native_id,c.harness,MIN(c.created_at) captured_at
16
+ FROM captures c,json_each(c.commit_json,'$.artifacts') a
17
+ WHERE c.team_id=? AND json_extract(a.value,'$.upload_id') IN (SELECT id FROM page)
18
+ GROUP BY c.team_id,json_extract(a.value,'$.upload_id')
19
+ ) SELECT u.id,u.source_id,s.machine_id,m.name machine,u.created_at,u.committed,
20
+ p.native_id,p.harness,p.captured_at,
21
+ json_extract(u.manifest_json,'$.role') role,json_extract(u.manifest_json,'$.media_type') media_type,
22
+ json_extract(u.manifest_json,'$.sha256') sha256,
23
+ (SELECT COALESCE(SUM(length(bytes)),0) FROM artifact_chunks c WHERE c.team_id=u.team_id AND c.upload_id=u.id) bytes,
24
+ EXISTS (SELECT 1 FROM session_artifacts a WHERE a.team_id=u.team_id AND a.upload_id=u.id) version_referenced,
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
+ 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
+ FROM page u JOIN sources s ON s.team_id=u.team_id AND s.id=u.source_id
28
+ LEFT JOIN machines m ON m.team_id=s.team_id AND m.id=s.machine_id
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
+ send(res, 200, result(api, auth, { items: rows, next: rows.length === 100 ? rows.at(-1).id : null, usage: api.storageUsage?.() ?? null }), requestId);
31
+ }
package/worker/index.mjs CHANGED
@@ -72,52 +72,22 @@ function durableSql(storage) {
72
72
  return { all, get, run, tx, exec: (query) => sql.exec(query) };
73
73
  }
74
74
 
75
- /** Aggregate storage diagnostics only: no record contents or credentials. */
76
- function logStorageBreakdown(sql) {
77
- console.log(`storage: database_bytes=${sql.databaseSize}`);
78
- try {
79
- for (const row of sql.exec('SELECT name,SUM(pgsize) bytes,COUNT(*) pages FROM dbstat GROUP BY name ORDER BY SUM(pgsize) DESC')) console.log(`storage table: ${JSON.stringify(row)}`);
80
- return;
81
- } catch (error) { console.log(`storage: physical table allocation unavailable: ${error.message}`); }
82
- // Some hosts omit dbstat. Logical payload lengths exclude indexes/page
83
- // overhead; TEXT lengths count characters and BLOB lengths count bytes.
84
- const quote = name => `"${name.replaceAll('"', '""')}"`;
85
- for (const { name } of sql.exec("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")) {
86
- try {
87
- const columns = [...sql.exec(`PRAGMA table_info(${quote(name)})`)];
88
- const payload = columns.filter(column => /TEXT|BLOB/i.test(column.type)).map(column => `COALESCE(length(${quote(column.name)}),0)`);
89
- const [row] = sql.exec(`SELECT COUNT(*) rows,COALESCE(SUM(${payload.join('+') || '0'}),0) payload_units FROM ${quote(name)}`);
90
- console.log(`storage table logical: ${JSON.stringify({ name, ...row })}`);
91
- } catch (error) { console.log(`storage table ${name}: ${error.message}`); }
92
- }
93
- try {
94
- const now = Date.now();
95
- const rows = sql.exec(`WITH chunk_sizes AS MATERIALIZED (SELECT team_id,upload_id,COUNT(*) chunks,SUM(length(bytes)) bytes FROM artifact_chunks GROUP BY team_id,upload_id),
96
- staging AS MATERIALIZED (SELECT team_id,begin_json FROM captures WHERE state='staging'),
97
- classified AS MATERIALIZED (SELECT u.team_id,u.id,u.created_at,
98
- CASE WHEN EXISTS (SELECT 1 FROM session_artifacts a WHERE a.team_id=u.team_id AND a.upload_id=u.id) THEN 'version-referenced'
99
- WHEN EXISTS (SELECT 1 FROM continuations c WHERE c.team_id=u.team_id AND instr(c.value_json,u.id)>0) THEN 'continuation-referenced'
100
- WHEN EXISTS (SELECT 1 FROM staging c WHERE c.team_id=u.team_id AND instr(c.begin_json,u.id)>0) THEN 'staging-referenced'
101
- WHEN u.created_at+86400000>? THEN 'unreferenced-under-one-day'
102
- WHEN u.committed=1 THEN 'eligible-committed' ELSE 'expired-staging' END category
103
- FROM uploads u)
104
- SELECT u.category,COUNT(*) uploads,SUM(c.chunks) chunks,SUM(c.bytes) blob_bytes,
105
- MIN(u.created_at) oldest_at,MAX(u.created_at) newest_at
106
- FROM chunk_sizes c LEFT JOIN classified u ON u.team_id=c.team_id AND u.id=c.upload_id GROUP BY u.category`, now);
107
- for (const row of rows) console.log(`storage uploads: ${JSON.stringify(row)}`);
108
- } catch (error) { console.log(`storage upload classification: ${error.message}`); }
109
- }
110
-
111
75
  export class TeamsServer extends DurableObject {
112
76
  constructor(ctx, env) {
113
77
  super(ctx, env);
114
78
  this.ready = ctx.blockConcurrencyWhile(async () => {
115
79
  // Read before migrations/maintenance: allocation failures must not hide
116
80
  // the table responsible, including hosts with a lower storage limit.
117
- logStorageBreakdown(ctx.storage.sql);
81
+ console.log(`storage: database_bytes=${ctx.storage.sql.databaseSize}`);
118
82
  const db = durableSql(ctx.storage);
119
83
  migrate({ exec: db.exec, tx: db.tx, version: () => ctx.storage.kv.get('schema_version') ?? 0, setVersion: (n) => ctx.storage.kv.put('schema_version', n) });
120
84
  this.api = createTeamsApi({ publicOrigin: normalizePublicOrigin(env.PUBLIC_ORIGIN), identityIssuer: env.VOLTER_ISSUER || null, identityOrganizations: identityOrganizationsFromEnv(env), delegatedIssuer: env.DELEGATED_ISSUER || null });
85
+ this.api.storageUsage = () => {
86
+ const sql = ctx.storage.sql;
87
+ let free = 0, page = 0;
88
+ try { free = sql.exec('PRAGMA freelist_count').one().freelist_count; page = sql.exec('PRAGMA page_size').one().page_size; } catch { /* conservative allocation measure if host omits the pragmas */ }
89
+ return { allocated: sql.databaseSize, used: Math.max(0, sql.databaseSize - free * page), limit: 10 * 1024 ** 3 };
90
+ };
121
91
  this.api.attach({ store: new DirectStore(createStoreHandlers(db)), harnessRoutes });
122
92
  const existing = await this.api.store.one("SELECT value FROM server_meta WHERE key='server_id'");
123
93
  if (existing) this.api.attach({ serverId: existing.value });
@@ -131,7 +101,7 @@ export class TeamsServer extends DurableObject {
131
101
  this.handler = createHttpHandler(this.api);
132
102
  // Retention must also run after a restart: a full object may have failed to re-arm its alarm.
133
103
  if (this.api.serverId) await maintain(this.api).catch(error => console.error(`maintain startup: ${error.message}`));
134
- if (!(await ctx.storage.getAlarm())) await ctx.storage.setAlarm(Date.now() + MAINTAIN_MS);
104
+ if (!(await ctx.storage.getAlarm())) await ctx.storage.setAlarm(Date.now() + MAINTAIN_MS).catch(error => console.error(`maintain alarm scheduling failed: ${error.message}`));
135
105
  });
136
106
  }
137
107
 
@@ -160,6 +130,8 @@ export class TeamsServer extends DurableObject {
160
130
  async alarm() {
161
131
  await this.ready;
162
132
  const done = this.api.serverId ? await maintain(this.api).catch(error => { console.error(`maintain failed: ${error.message}`); return null; }) : null;
133
+ const usage = this.api.storageUsage();
134
+ if (usage.used >= usage.limit * 0.8) console.warn(`Teams storage pressure: ${JSON.stringify(usage)}; native upload admission stops at 90%`);
163
135
  // The database's size on every full pass, where `wrangler tail` reads it.
164
136
  if (!done?.backlog) {
165
137
  // What grows: free pages (space deletes left for reuse), and each busy table's newest rowid (its insert rate).