@volter/supercode-teams 0.2.141 → 0.2.143
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.d.ts +2 -2
- package/connector/index.mjs +4 -3
- package/package.json +1 -1
- package/server/harness-routes.mjs +7 -0
- package/server/storage-report.mjs +1 -1
- package/worker/index.mjs +12 -38
package/connector/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
43
|
+
/** Size from which translated native bundles are cached locally (default 8 MiB). */
|
|
44
44
|
nativeIdleBytes?: number;
|
|
45
45
|
}
|
|
46
46
|
|
package/connector/index.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.2.143",
|
|
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",
|
|
@@ -290,6 +290,13 @@ async function artifactChunk(api,req,res,auth,sourceId,uploadId,index,requestId)
|
|
|
290
290
|
if(upload&&upload.source_id!==sourceId)fail(409,'capture_conflict','upload belongs to another source');
|
|
291
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]);
|
|
292
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
|
+
}
|
|
293
300
|
const now=api.clock.nowMs(),response=result(api,auth,{index,sha256:b.sha256});
|
|
294
301
|
const guards=[
|
|
295
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'},
|
|
@@ -27,5 +27,5 @@ export async function storageReport({ api, res, auth, url, requestId }) {
|
|
|
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
|
-
send(res, 200, result(api, auth, { items: rows, next: rows.length === 100 ? rows.at(-1).id : null }), requestId);
|
|
30
|
+
send(res, 200, result(api, auth, { items: rows, next: rows.length === 100 ? rows.at(-1).id : null, usage: api.storageUsage?.() ?? null }), requestId);
|
|
31
31
|
}
|
package/worker/index.mjs
CHANGED
|
@@ -72,42 +72,6 @@ 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);
|
|
@@ -118,6 +82,12 @@ export class TeamsServer extends DurableObject {
|
|
|
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 });
|
|
@@ -130,8 +100,10 @@ export class TeamsServer extends DurableObject {
|
|
|
130
100
|
}
|
|
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
|
-
|
|
134
|
-
|
|
103
|
+
const startup = this.api.serverId ? await maintain(this.api).catch(error => { console.error(`maintain startup: ${error.message}`); return null; }) : null;
|
|
104
|
+
// A capacity failure can exhaust an old alarm's retries. Re-arm on every
|
|
105
|
+
// activation, including a retained overdue alarm; drain backlogs promptly.
|
|
106
|
+
await ctx.storage.setAlarm(Date.now() + (startup?.backlog ? 3_000 : MAINTAIN_MS)).catch(error => console.error(`maintain alarm scheduling failed: ${error.message}`));
|
|
135
107
|
});
|
|
136
108
|
}
|
|
137
109
|
|
|
@@ -160,6 +132,8 @@ export class TeamsServer extends DurableObject {
|
|
|
160
132
|
async alarm() {
|
|
161
133
|
await this.ready;
|
|
162
134
|
const done = this.api.serverId ? await maintain(this.api).catch(error => { console.error(`maintain failed: ${error.message}`); return null; }) : null;
|
|
135
|
+
const usage = this.api.storageUsage();
|
|
136
|
+
if (usage.used >= usage.limit * 0.8) console.warn(`Teams storage pressure: ${JSON.stringify(usage)}; native upload admission stops at 90%`);
|
|
163
137
|
// The database's size on every full pass, where `wrangler tail` reads it.
|
|
164
138
|
if (!done?.backlog) {
|
|
165
139
|
// What grows: free pages (space deletes left for reuse), and each busy table's newest rowid (its insert rate).
|