sphica 0.6.0 → 0.6.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "sphica",
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
6
6
  "author": {
7
7
  "name": "iroha924",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
5
5
  "author": {
6
6
  "name": "iroha924",
package/dist/capture.js CHANGED
@@ -11801,6 +11801,8 @@ function lock() {
11801
11801
  return null;
11802
11802
  }
11803
11803
  var BATCH = 500;
11804
+ var FLUSH_BUDGET_MS = 30000;
11805
+ var TOOL_FLUSH_BUDGET_MS = 2000;
11804
11806
  var ROWS = 1000;
11805
11807
  var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
11806
11808
  async function write(db, batch, projects) {
@@ -11876,98 +11878,135 @@ async function write(db, batch, projects) {
11876
11878
  }
11877
11879
  var REJECTED = new Set([18, 19, 20, 25]);
11878
11880
  var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
11879
- async function flush(file = dbFile()) {
11880
- const unlock = lock();
11881
- if (!unlock)
11882
- return { sent: 0, deferred: 0, rejected: 0, busy: true };
11883
- const dir = spoolDir();
11884
- let client = null;
11881
+ var queued = (from) => {
11882
+ try {
11883
+ return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
11884
+ } catch {
11885
+ return [];
11886
+ }
11887
+ };
11888
+ async function sendBatch(db, names) {
11889
+ const held = unregisteredDir();
11890
+ const records = [];
11891
+ for (const { name, from } of names) {
11892
+ let r;
11893
+ try {
11894
+ r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
11895
+ } catch {
11896
+ fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11897
+ fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
11898
+ continue;
11899
+ }
11900
+ if (r)
11901
+ records.push({ name, from, r });
11902
+ else
11903
+ fs4.rmSync(path5.join(from, name), { force: true });
11904
+ }
11905
+ const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
11906
+ const known = records.filter((x) => projects.has(x.r.project));
11907
+ const strayed = records.filter((x) => !projects.has(x.r.project));
11908
+ let sent = 0;
11909
+ const bad = [];
11885
11910
  try {
11886
- const held = unregisteredDir();
11887
- const list = (from) => {
11911
+ sent = await write(db, known.map((x) => x.r), projects);
11912
+ } catch (e) {
11913
+ if (!rejected(e))
11914
+ throw e;
11915
+ for (const x of known) {
11888
11916
  try {
11889
- return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((name) => ({ name, from }));
11890
- } catch {
11891
- return [];
11917
+ sent += await write(db, [x.r], projects);
11918
+ } catch (e2) {
11919
+ if (!rejected(e2))
11920
+ throw e2;
11921
+ bad.push(x);
11892
11922
  }
11893
- };
11894
- const names = [...list(dir), ...list(held)].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).slice(0, BATCH);
11895
- if (names.length === 0)
11896
- return { sent: 0, deferred: 0, rejected: 0 };
11897
- const records = [];
11898
- for (const { name, from } of names) {
11899
- let r;
11923
+ }
11924
+ }
11925
+ if (bad.length) {
11926
+ fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11927
+ for (const x of bad) {
11900
11928
  try {
11901
- r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
11902
- } catch {
11903
- fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11904
- fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
11905
- continue;
11906
- }
11907
- if (r)
11908
- records.push({ name, from, r });
11909
- else
11910
- fs4.rmSync(path5.join(from, name), { force: true });
11911
- }
11912
- const db = openWriter("capture", file);
11913
- client = db;
11914
- const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
11915
- const known = records.filter((x) => projects.has(x.r.project));
11916
- const strayed = records.filter((x) => !projects.has(x.r.project));
11917
- let sent = 0;
11918
- const bad = [];
11919
- try {
11920
- sent = await write(db, known.map((x) => x.r), projects);
11921
- } catch (e) {
11922
- if (!rejected(e))
11923
- throw e;
11924
- for (const x of known) {
11925
- try {
11926
- sent += await write(db, [x.r], projects);
11927
- } catch (e2) {
11928
- if (!rejected(e2))
11929
- throw e2;
11930
- bad.push(x);
11931
- }
11929
+ fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
11930
+ } catch (e) {
11931
+ if (e.code !== "ENOENT")
11932
+ throw e;
11932
11933
  }
11933
11934
  }
11934
- if (bad.length) {
11935
- fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11936
- for (const x of bad) {
11937
- try {
11938
- fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
11939
- } catch (e) {
11940
- if (e.code !== "ENOENT")
11941
- throw e;
11942
- }
11935
+ }
11936
+ if (strayed.length) {
11937
+ fs4.mkdirSync(held, { recursive: true, mode: 448 });
11938
+ for (const x of strayed) {
11939
+ if (x.from === held)
11940
+ continue;
11941
+ try {
11942
+ fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
11943
+ } catch (e) {
11944
+ if (e.code !== "ENOENT")
11945
+ throw e;
11943
11946
  }
11944
11947
  }
11945
- if (strayed.length) {
11946
- fs4.mkdirSync(held, { recursive: true, mode: 448 });
11947
- for (const x of strayed) {
11948
- if (x.from === held)
11949
- continue;
11950
- try {
11951
- fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
11952
- } catch (e) {
11953
- if (e.code !== "ENOENT")
11954
- throw e;
11948
+ }
11949
+ const moved = new Set([...bad, ...strayed].map((x) => x.name));
11950
+ for (const x of records)
11951
+ if (!moved.has(x.name))
11952
+ fs4.rmSync(path5.join(x.from, x.name), { force: true });
11953
+ return { sent, rejected: bad.length };
11954
+ }
11955
+ async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
11956
+ const deadline = Date.now() + budgetMs;
11957
+ const dir = spoolDir();
11958
+ const held = unregisteredDir();
11959
+ const total = { sent: 0, deferred: 0, rejected: 0 };
11960
+ let batches = 0;
11961
+ let queueBatches = 0;
11962
+ for (let first = true;; first = false) {
11963
+ let unlock = lock();
11964
+ while (!unlock && Date.now() < deadline) {
11965
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
11966
+ if (Date.now() < deadline)
11967
+ unlock = lock();
11968
+ }
11969
+ if (!unlock)
11970
+ return first ? { ...total, busy: true } : total;
11971
+ let client = null;
11972
+ const send = async (names) => {
11973
+ client ??= openWriter("capture", file);
11974
+ const r = await sendBatch(client, names);
11975
+ total.sent += r.sent;
11976
+ total.rejected += r.rejected;
11977
+ batches++;
11978
+ };
11979
+ const late = () => batches > 0 && Date.now() >= deadline;
11980
+ const sentBefore = batches;
11981
+ try {
11982
+ if (first) {
11983
+ prune(held);
11984
+ for (const part of chunks(queued(held), BATCH)) {
11985
+ if (late())
11986
+ break;
11987
+ await send(part);
11955
11988
  }
11956
11989
  }
11990
+ while (queueBatches === 0 || !late()) {
11991
+ const part = queued(dir).slice(0, BATCH);
11992
+ if (part.length === 0)
11993
+ break;
11994
+ await send(part);
11995
+ queueBatches++;
11996
+ }
11997
+ prune(held);
11998
+ total.deferred = queued(held).length;
11999
+ if (batches > sentBefore)
12000
+ writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
12001
+ } catch (e) {
12002
+ writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
12003
+ throw e;
12004
+ } finally {
12005
+ await client?.destroy().catch(() => {});
12006
+ unlock();
11957
12007
  }
11958
- const moved = new Set([...bad, ...strayed].map((x) => x.name));
11959
- for (const x of records)
11960
- if (!moved.has(x.name))
11961
- fs4.rmSync(path5.join(x.from, x.name), { force: true });
11962
- prune(held);
11963
- writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
11964
- return { sent, deferred: strayed.length, rejected: bad.length };
11965
- } catch (e) {
11966
- writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
11967
- throw e;
11968
- } finally {
11969
- await client?.destroy().catch(() => {});
11970
- unlock();
12008
+ if (Date.now() >= deadline || queued(dir).length === 0)
12009
+ return total;
11971
12010
  }
11972
12011
  }
11973
12012
  async function readInput(stream) {
@@ -11997,6 +12036,7 @@ if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
11997
12036
  }
11998
12037
  export {
11999
12038
  MAX_MESSAGE,
12039
+ TOOL_FLUSH_BUDGET_MS,
12000
12040
  answersOf,
12001
12041
  branchOf,
12002
12042
  captureNotice,
package/dist/cli.js CHANGED
@@ -15132,6 +15132,7 @@ function lock() {
15132
15132
  return null;
15133
15133
  }
15134
15134
  var BATCH = 500;
15135
+ var FLUSH_BUDGET_MS = 30000;
15135
15136
  var ROWS = 1000;
15136
15137
  var chunks = (xs, n2 = ROWS) => Array.from({ length: Math.ceil(xs.length / n2) }, (_, i2) => xs.slice(i2 * n2, (i2 + 1) * n2));
15137
15138
  async function write(db, batch, projects) {
@@ -15207,98 +15208,135 @@ async function write(db, batch, projects) {
15207
15208
  }
15208
15209
  var REJECTED = new Set([18, 19, 20, 25]);
15209
15210
  var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
15210
- async function flush(file = dbFile()) {
15211
- const unlock = lock();
15212
- if (!unlock)
15213
- return { sent: 0, deferred: 0, rejected: 0, busy: true };
15214
- const dir = spoolDir();
15215
- let client = null;
15211
+ var queued = (from) => {
15212
+ try {
15213
+ return fs6.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
15214
+ } catch {
15215
+ return [];
15216
+ }
15217
+ };
15218
+ async function sendBatch(db, names) {
15219
+ const held = unregisteredDir();
15220
+ const records = [];
15221
+ for (const { name, from } of names) {
15222
+ let r2;
15223
+ try {
15224
+ r2 = current(JSON.parse(fs6.readFileSync(path7.join(from, name), "utf8")));
15225
+ } catch {
15226
+ fs6.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
15227
+ fs6.renameSync(path7.join(from, name), path7.join(rejectedDir(), name));
15228
+ continue;
15229
+ }
15230
+ if (r2)
15231
+ records.push({ name, from, r: r2 });
15232
+ else
15233
+ fs6.rmSync(path7.join(from, name), { force: true });
15234
+ }
15235
+ const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
15236
+ const known = records.filter((x) => projects.has(x.r.project));
15237
+ const strayed = records.filter((x) => !projects.has(x.r.project));
15238
+ let sent = 0;
15239
+ const bad = [];
15216
15240
  try {
15217
- const held = unregisteredDir();
15218
- const list = (from) => {
15241
+ sent = await write(db, known.map((x) => x.r), projects);
15242
+ } catch (e) {
15243
+ if (!rejected(e))
15244
+ throw e;
15245
+ for (const x of known) {
15219
15246
  try {
15220
- return fs6.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((name) => ({ name, from }));
15221
- } catch {
15222
- return [];
15247
+ sent += await write(db, [x.r], projects);
15248
+ } catch (e2) {
15249
+ if (!rejected(e2))
15250
+ throw e2;
15251
+ bad.push(x);
15223
15252
  }
15224
- };
15225
- const names = [...list(dir), ...list(held)].sort((a2, b) => a2.name < b.name ? -1 : a2.name > b.name ? 1 : 0).slice(0, BATCH);
15226
- if (names.length === 0)
15227
- return { sent: 0, deferred: 0, rejected: 0 };
15228
- const records = [];
15229
- for (const { name, from } of names) {
15230
- let r2;
15253
+ }
15254
+ }
15255
+ if (bad.length) {
15256
+ fs6.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
15257
+ for (const x of bad) {
15231
15258
  try {
15232
- r2 = current(JSON.parse(fs6.readFileSync(path7.join(from, name), "utf8")));
15233
- } catch {
15234
- fs6.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
15235
- fs6.renameSync(path7.join(from, name), path7.join(rejectedDir(), name));
15236
- continue;
15237
- }
15238
- if (r2)
15239
- records.push({ name, from, r: r2 });
15240
- else
15241
- fs6.rmSync(path7.join(from, name), { force: true });
15242
- }
15243
- const db = openWriter("capture", file);
15244
- client = db;
15245
- const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
15246
- const known = records.filter((x) => projects.has(x.r.project));
15247
- const strayed = records.filter((x) => !projects.has(x.r.project));
15248
- let sent = 0;
15249
- const bad = [];
15250
- try {
15251
- sent = await write(db, known.map((x) => x.r), projects);
15252
- } catch (e) {
15253
- if (!rejected(e))
15254
- throw e;
15255
- for (const x of known) {
15256
- try {
15257
- sent += await write(db, [x.r], projects);
15258
- } catch (e2) {
15259
- if (!rejected(e2))
15260
- throw e2;
15261
- bad.push(x);
15262
- }
15259
+ fs6.renameSync(path7.join(x.from, x.name), path7.join(rejectedDir(), x.name));
15260
+ } catch (e) {
15261
+ if (e.code !== "ENOENT")
15262
+ throw e;
15263
15263
  }
15264
15264
  }
15265
- if (bad.length) {
15266
- fs6.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
15267
- for (const x of bad) {
15268
- try {
15269
- fs6.renameSync(path7.join(x.from, x.name), path7.join(rejectedDir(), x.name));
15270
- } catch (e) {
15271
- if (e.code !== "ENOENT")
15272
- throw e;
15273
- }
15265
+ }
15266
+ if (strayed.length) {
15267
+ fs6.mkdirSync(held, { recursive: true, mode: 448 });
15268
+ for (const x of strayed) {
15269
+ if (x.from === held)
15270
+ continue;
15271
+ try {
15272
+ fs6.renameSync(path7.join(x.from, x.name), path7.join(held, x.name));
15273
+ } catch (e) {
15274
+ if (e.code !== "ENOENT")
15275
+ throw e;
15274
15276
  }
15275
15277
  }
15276
- if (strayed.length) {
15277
- fs6.mkdirSync(held, { recursive: true, mode: 448 });
15278
- for (const x of strayed) {
15279
- if (x.from === held)
15280
- continue;
15281
- try {
15282
- fs6.renameSync(path7.join(x.from, x.name), path7.join(held, x.name));
15283
- } catch (e) {
15284
- if (e.code !== "ENOENT")
15285
- throw e;
15278
+ }
15279
+ const moved = new Set([...bad, ...strayed].map((x) => x.name));
15280
+ for (const x of records)
15281
+ if (!moved.has(x.name))
15282
+ fs6.rmSync(path7.join(x.from, x.name), { force: true });
15283
+ return { sent, rejected: bad.length };
15284
+ }
15285
+ async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
15286
+ const deadline = Date.now() + budgetMs;
15287
+ const dir = spoolDir();
15288
+ const held = unregisteredDir();
15289
+ const total = { sent: 0, deferred: 0, rejected: 0 };
15290
+ let batches = 0;
15291
+ let queueBatches = 0;
15292
+ for (let first = true;; first = false) {
15293
+ let unlock = lock();
15294
+ while (!unlock && Date.now() < deadline) {
15295
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
15296
+ if (Date.now() < deadline)
15297
+ unlock = lock();
15298
+ }
15299
+ if (!unlock)
15300
+ return first ? { ...total, busy: true } : total;
15301
+ let client = null;
15302
+ const send = async (names) => {
15303
+ client ??= openWriter("capture", file);
15304
+ const r2 = await sendBatch(client, names);
15305
+ total.sent += r2.sent;
15306
+ total.rejected += r2.rejected;
15307
+ batches++;
15308
+ };
15309
+ const late = () => batches > 0 && Date.now() >= deadline;
15310
+ const sentBefore = batches;
15311
+ try {
15312
+ if (first) {
15313
+ prune(held);
15314
+ for (const part of chunks(queued(held), BATCH)) {
15315
+ if (late())
15316
+ break;
15317
+ await send(part);
15286
15318
  }
15287
15319
  }
15320
+ while (queueBatches === 0 || !late()) {
15321
+ const part = queued(dir).slice(0, BATCH);
15322
+ if (part.length === 0)
15323
+ break;
15324
+ await send(part);
15325
+ queueBatches++;
15326
+ }
15327
+ prune(held);
15328
+ total.deferred = queued(held).length;
15329
+ if (batches > sentBefore)
15330
+ writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
15331
+ } catch (e) {
15332
+ writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
15333
+ throw e;
15334
+ } finally {
15335
+ await client?.destroy().catch(() => {});
15336
+ unlock();
15288
15337
  }
15289
- const moved = new Set([...bad, ...strayed].map((x) => x.name));
15290
- for (const x of records)
15291
- if (!moved.has(x.name))
15292
- fs6.rmSync(path7.join(x.from, x.name), { force: true });
15293
- prune(held);
15294
- writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
15295
- return { sent, deferred: strayed.length, rejected: bad.length };
15296
- } catch (e) {
15297
- writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
15298
- throw e;
15299
- } finally {
15300
- await client?.destroy().catch(() => {});
15301
- unlock();
15338
+ if (Date.now() >= deadline || queued(dir).length === 0)
15339
+ return total;
15302
15340
  }
15303
15341
  }
15304
15342
  async function readInput(stream) {
package/dist/deliver.js CHANGED
@@ -11847,6 +11847,7 @@ function lock() {
11847
11847
  return null;
11848
11848
  }
11849
11849
  var BATCH = 500;
11850
+ var FLUSH_BUDGET_MS = 30000;
11850
11851
  var ROWS = 1000;
11851
11852
  var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
11852
11853
  async function write(db, batch, projects) {
@@ -11922,98 +11923,135 @@ async function write(db, batch, projects) {
11922
11923
  }
11923
11924
  var REJECTED = new Set([18, 19, 20, 25]);
11924
11925
  var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
11925
- async function flush(file = dbFile()) {
11926
- const unlock = lock();
11927
- if (!unlock)
11928
- return { sent: 0, deferred: 0, rejected: 0, busy: true };
11929
- const dir = spoolDir();
11930
- let client = null;
11926
+ var queued = (from) => {
11927
+ try {
11928
+ return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
11929
+ } catch {
11930
+ return [];
11931
+ }
11932
+ };
11933
+ async function sendBatch(db, names) {
11934
+ const held = unregisteredDir();
11935
+ const records = [];
11936
+ for (const { name, from } of names) {
11937
+ let r;
11938
+ try {
11939
+ r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
11940
+ } catch {
11941
+ fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11942
+ fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
11943
+ continue;
11944
+ }
11945
+ if (r)
11946
+ records.push({ name, from, r });
11947
+ else
11948
+ fs4.rmSync(path5.join(from, name), { force: true });
11949
+ }
11950
+ const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
11951
+ const known = records.filter((x) => projects.has(x.r.project));
11952
+ const strayed = records.filter((x) => !projects.has(x.r.project));
11953
+ let sent = 0;
11954
+ const bad = [];
11931
11955
  try {
11932
- const held = unregisteredDir();
11933
- const list = (from) => {
11956
+ sent = await write(db, known.map((x) => x.r), projects);
11957
+ } catch (e) {
11958
+ if (!rejected(e))
11959
+ throw e;
11960
+ for (const x of known) {
11934
11961
  try {
11935
- return fs4.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((name) => ({ name, from }));
11936
- } catch {
11937
- return [];
11962
+ sent += await write(db, [x.r], projects);
11963
+ } catch (e2) {
11964
+ if (!rejected(e2))
11965
+ throw e2;
11966
+ bad.push(x);
11938
11967
  }
11939
- };
11940
- const names = [...list(dir), ...list(held)].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).slice(0, BATCH);
11941
- if (names.length === 0)
11942
- return { sent: 0, deferred: 0, rejected: 0 };
11943
- const records = [];
11944
- for (const { name, from } of names) {
11945
- let r;
11968
+ }
11969
+ }
11970
+ if (bad.length) {
11971
+ fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11972
+ for (const x of bad) {
11946
11973
  try {
11947
- r = current(JSON.parse(fs4.readFileSync(path5.join(from, name), "utf8")));
11948
- } catch {
11949
- fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11950
- fs4.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
11951
- continue;
11952
- }
11953
- if (r)
11954
- records.push({ name, from, r });
11955
- else
11956
- fs4.rmSync(path5.join(from, name), { force: true });
11957
- }
11958
- const db = openWriter("capture", file);
11959
- client = db;
11960
- const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
11961
- const known = records.filter((x) => projects.has(x.r.project));
11962
- const strayed = records.filter((x) => !projects.has(x.r.project));
11963
- let sent = 0;
11964
- const bad = [];
11965
- try {
11966
- sent = await write(db, known.map((x) => x.r), projects);
11967
- } catch (e) {
11968
- if (!rejected(e))
11969
- throw e;
11970
- for (const x of known) {
11971
- try {
11972
- sent += await write(db, [x.r], projects);
11973
- } catch (e2) {
11974
- if (!rejected(e2))
11975
- throw e2;
11976
- bad.push(x);
11977
- }
11974
+ fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
11975
+ } catch (e) {
11976
+ if (e.code !== "ENOENT")
11977
+ throw e;
11978
11978
  }
11979
11979
  }
11980
- if (bad.length) {
11981
- fs4.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
11982
- for (const x of bad) {
11983
- try {
11984
- fs4.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
11985
- } catch (e) {
11986
- if (e.code !== "ENOENT")
11987
- throw e;
11988
- }
11980
+ }
11981
+ if (strayed.length) {
11982
+ fs4.mkdirSync(held, { recursive: true, mode: 448 });
11983
+ for (const x of strayed) {
11984
+ if (x.from === held)
11985
+ continue;
11986
+ try {
11987
+ fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
11988
+ } catch (e) {
11989
+ if (e.code !== "ENOENT")
11990
+ throw e;
11989
11991
  }
11990
11992
  }
11991
- if (strayed.length) {
11992
- fs4.mkdirSync(held, { recursive: true, mode: 448 });
11993
- for (const x of strayed) {
11994
- if (x.from === held)
11995
- continue;
11996
- try {
11997
- fs4.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
11998
- } catch (e) {
11999
- if (e.code !== "ENOENT")
12000
- throw e;
11993
+ }
11994
+ const moved = new Set([...bad, ...strayed].map((x) => x.name));
11995
+ for (const x of records)
11996
+ if (!moved.has(x.name))
11997
+ fs4.rmSync(path5.join(x.from, x.name), { force: true });
11998
+ return { sent, rejected: bad.length };
11999
+ }
12000
+ async function flush(file = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
12001
+ const deadline = Date.now() + budgetMs;
12002
+ const dir = spoolDir();
12003
+ const held = unregisteredDir();
12004
+ const total = { sent: 0, deferred: 0, rejected: 0 };
12005
+ let batches = 0;
12006
+ let queueBatches = 0;
12007
+ for (let first = true;; first = false) {
12008
+ let unlock = lock();
12009
+ while (!unlock && Date.now() < deadline) {
12010
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
12011
+ if (Date.now() < deadline)
12012
+ unlock = lock();
12013
+ }
12014
+ if (!unlock)
12015
+ return first ? { ...total, busy: true } : total;
12016
+ let client = null;
12017
+ const send = async (names) => {
12018
+ client ??= openWriter("capture", file);
12019
+ const r = await sendBatch(client, names);
12020
+ total.sent += r.sent;
12021
+ total.rejected += r.rejected;
12022
+ batches++;
12023
+ };
12024
+ const late = () => batches > 0 && Date.now() >= deadline;
12025
+ const sentBefore = batches;
12026
+ try {
12027
+ if (first) {
12028
+ prune(held);
12029
+ for (const part of chunks(queued(held), BATCH)) {
12030
+ if (late())
12031
+ break;
12032
+ await send(part);
12001
12033
  }
12002
12034
  }
12035
+ while (queueBatches === 0 || !late()) {
12036
+ const part = queued(dir).slice(0, BATCH);
12037
+ if (part.length === 0)
12038
+ break;
12039
+ await send(part);
12040
+ queueBatches++;
12041
+ }
12042
+ prune(held);
12043
+ total.deferred = queued(held).length;
12044
+ if (batches > sentBefore)
12045
+ writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
12046
+ } catch (e) {
12047
+ writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
12048
+ throw e;
12049
+ } finally {
12050
+ await client?.destroy().catch(() => {});
12051
+ unlock();
12003
12052
  }
12004
- const moved = new Set([...bad, ...strayed].map((x) => x.name));
12005
- for (const x of records)
12006
- if (!moved.has(x.name))
12007
- fs4.rmSync(path5.join(x.from, x.name), { force: true });
12008
- prune(held);
12009
- writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
12010
- return { sent, deferred: strayed.length, rejected: bad.length };
12011
- } catch (e) {
12012
- writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
12013
- throw e;
12014
- } finally {
12015
- await client?.destroy().catch(() => {});
12016
- unlock();
12053
+ if (Date.now() >= deadline || queued(dir).length === 0)
12054
+ return total;
12017
12055
  }
12018
12056
  }
12019
12057
  async function readInput(stream) {
@@ -46683,6 +46683,8 @@ function lock() {
46683
46683
  return null;
46684
46684
  }
46685
46685
  var BATCH = 500;
46686
+ var FLUSH_BUDGET_MS = 30000;
46687
+ var TOOL_FLUSH_BUDGET_MS = 2000;
46686
46688
  var ROWS = 1000;
46687
46689
  var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
46688
46690
  async function write(db, batch, projects) {
@@ -46758,98 +46760,135 @@ async function write(db, batch, projects) {
46758
46760
  }
46759
46761
  var REJECTED = new Set([18, 19, 20, 25]);
46760
46762
  var rejected = (e) => REJECTED.has(sqliteCode(e) ?? -1);
46761
- async function flush(file2 = dbFile()) {
46762
- const unlock = lock();
46763
- if (!unlock)
46764
- return { sent: 0, deferred: 0, rejected: 0, busy: true };
46765
- const dir = spoolDir();
46766
- let client = null;
46763
+ var queued = (from) => {
46767
46764
  try {
46768
- const held = unregisteredDir();
46769
- const list = (from) => {
46765
+ return fs5.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().map((name) => ({ name, from }));
46766
+ } catch {
46767
+ return [];
46768
+ }
46769
+ };
46770
+ async function sendBatch(db, names) {
46771
+ const held = unregisteredDir();
46772
+ const records = [];
46773
+ for (const { name, from } of names) {
46774
+ let r;
46775
+ try {
46776
+ r = current(JSON.parse(fs5.readFileSync(path5.join(from, name), "utf8")));
46777
+ } catch {
46778
+ fs5.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
46779
+ fs5.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
46780
+ continue;
46781
+ }
46782
+ if (r)
46783
+ records.push({ name, from, r });
46784
+ else
46785
+ fs5.rmSync(path5.join(from, name), { force: true });
46786
+ }
46787
+ const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
46788
+ const known = records.filter((x) => projects.has(x.r.project));
46789
+ const strayed = records.filter((x) => !projects.has(x.r.project));
46790
+ let sent = 0;
46791
+ const bad = [];
46792
+ try {
46793
+ sent = await write(db, known.map((x) => x.r), projects);
46794
+ } catch (e) {
46795
+ if (!rejected(e))
46796
+ throw e;
46797
+ for (const x of known) {
46770
46798
  try {
46771
- return fs5.readdirSync(from).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((name) => ({ name, from }));
46772
- } catch {
46773
- return [];
46799
+ sent += await write(db, [x.r], projects);
46800
+ } catch (e2) {
46801
+ if (!rejected(e2))
46802
+ throw e2;
46803
+ bad.push(x);
46774
46804
  }
46775
- };
46776
- const names = [...list(dir), ...list(held)].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).slice(0, BATCH);
46777
- if (names.length === 0)
46778
- return { sent: 0, deferred: 0, rejected: 0 };
46779
- const records = [];
46780
- for (const { name, from } of names) {
46781
- let r;
46805
+ }
46806
+ }
46807
+ if (bad.length) {
46808
+ fs5.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
46809
+ for (const x of bad) {
46782
46810
  try {
46783
- r = current(JSON.parse(fs5.readFileSync(path5.join(from, name), "utf8")));
46784
- } catch {
46785
- fs5.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
46786
- fs5.renameSync(path5.join(from, name), path5.join(rejectedDir(), name));
46787
- continue;
46788
- }
46789
- if (r)
46790
- records.push({ name, from, r });
46791
- else
46792
- fs5.rmSync(path5.join(from, name), { force: true });
46793
- }
46794
- const db = openWriter("capture", file2);
46795
- client = db;
46796
- const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
46797
- const known = records.filter((x) => projects.has(x.r.project));
46798
- const strayed = records.filter((x) => !projects.has(x.r.project));
46799
- let sent = 0;
46800
- const bad = [];
46801
- try {
46802
- sent = await write(db, known.map((x) => x.r), projects);
46803
- } catch (e) {
46804
- if (!rejected(e))
46805
- throw e;
46806
- for (const x of known) {
46807
- try {
46808
- sent += await write(db, [x.r], projects);
46809
- } catch (e2) {
46810
- if (!rejected(e2))
46811
- throw e2;
46812
- bad.push(x);
46813
- }
46811
+ fs5.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
46812
+ } catch (e) {
46813
+ if (e.code !== "ENOENT")
46814
+ throw e;
46814
46815
  }
46815
46816
  }
46816
- if (bad.length) {
46817
- fs5.mkdirSync(rejectedDir(), { recursive: true, mode: 448 });
46818
- for (const x of bad) {
46819
- try {
46820
- fs5.renameSync(path5.join(x.from, x.name), path5.join(rejectedDir(), x.name));
46821
- } catch (e) {
46822
- if (e.code !== "ENOENT")
46823
- throw e;
46824
- }
46817
+ }
46818
+ if (strayed.length) {
46819
+ fs5.mkdirSync(held, { recursive: true, mode: 448 });
46820
+ for (const x of strayed) {
46821
+ if (x.from === held)
46822
+ continue;
46823
+ try {
46824
+ fs5.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
46825
+ } catch (e) {
46826
+ if (e.code !== "ENOENT")
46827
+ throw e;
46825
46828
  }
46826
46829
  }
46827
- if (strayed.length) {
46828
- fs5.mkdirSync(held, { recursive: true, mode: 448 });
46829
- for (const x of strayed) {
46830
- if (x.from === held)
46831
- continue;
46832
- try {
46833
- fs5.renameSync(path5.join(x.from, x.name), path5.join(held, x.name));
46834
- } catch (e) {
46835
- if (e.code !== "ENOENT")
46836
- throw e;
46830
+ }
46831
+ const moved = new Set([...bad, ...strayed].map((x) => x.name));
46832
+ for (const x of records)
46833
+ if (!moved.has(x.name))
46834
+ fs5.rmSync(path5.join(x.from, x.name), { force: true });
46835
+ return { sent, rejected: bad.length };
46836
+ }
46837
+ async function flush(file2 = dbFile(), budgetMs = FLUSH_BUDGET_MS) {
46838
+ const deadline = Date.now() + budgetMs;
46839
+ const dir = spoolDir();
46840
+ const held = unregisteredDir();
46841
+ const total = { sent: 0, deferred: 0, rejected: 0 };
46842
+ let batches = 0;
46843
+ let queueBatches = 0;
46844
+ for (let first = true;; first = false) {
46845
+ let unlock = lock();
46846
+ while (!unlock && Date.now() < deadline) {
46847
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
46848
+ if (Date.now() < deadline)
46849
+ unlock = lock();
46850
+ }
46851
+ if (!unlock)
46852
+ return first ? { ...total, busy: true } : total;
46853
+ let client = null;
46854
+ const send = async (names) => {
46855
+ client ??= openWriter("capture", file2);
46856
+ const r = await sendBatch(client, names);
46857
+ total.sent += r.sent;
46858
+ total.rejected += r.rejected;
46859
+ batches++;
46860
+ };
46861
+ const late2 = () => batches > 0 && Date.now() >= deadline;
46862
+ const sentBefore = batches;
46863
+ try {
46864
+ if (first) {
46865
+ prune(held);
46866
+ for (const part of chunks(queued(held), BATCH)) {
46867
+ if (late2())
46868
+ break;
46869
+ await send(part);
46837
46870
  }
46838
46871
  }
46872
+ while (queueBatches === 0 || !late2()) {
46873
+ const part = queued(dir).slice(0, BATCH);
46874
+ if (part.length === 0)
46875
+ break;
46876
+ await send(part);
46877
+ queueBatches++;
46878
+ }
46879
+ prune(held);
46880
+ total.deferred = queued(held).length;
46881
+ if (batches > sentBefore)
46882
+ writeState({ flushedAt: new Date().toISOString(), error: null, deferred: total.deferred });
46883
+ } catch (e) {
46884
+ writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
46885
+ throw e;
46886
+ } finally {
46887
+ await client?.destroy().catch(() => {});
46888
+ unlock();
46839
46889
  }
46840
- const moved = new Set([...bad, ...strayed].map((x) => x.name));
46841
- for (const x of records)
46842
- if (!moved.has(x.name))
46843
- fs5.rmSync(path5.join(x.from, x.name), { force: true });
46844
- prune(held);
46845
- writeState({ flushedAt: new Date().toISOString(), error: null, deferred: strayed.length });
46846
- return { sent, deferred: strayed.length, rejected: bad.length };
46847
- } catch (e) {
46848
- writeState({ flushedAt: new Date().toISOString(), error: reason(e).slice(0, 300) });
46849
- throw e;
46850
- } finally {
46851
- await client?.destroy().catch(() => {});
46852
- unlock();
46890
+ if (Date.now() >= deadline || queued(dir).length === 0)
46891
+ return total;
46853
46892
  }
46854
46893
  }
46855
46894
  async function readInput(stream) {
@@ -48137,7 +48176,7 @@ async function liveUnits(db, projectId2, limit = 40) {
48137
48176
  // server/src/extract.ts
48138
48177
  var newRunId = () => crypto2.randomBytes(9).toString("base64url");
48139
48178
  async function pendingText(db, projectId2) {
48140
- await flush().catch(() => {});
48179
+ await flush(undefined, TOOL_FLUSH_BUDGET_MS).catch(() => {});
48141
48180
  const rows = await pendingSessions(db, projectId2);
48142
48181
  if (!rows.length)
48143
48182
  return "Every captured session has been traced.";
@@ -48159,14 +48198,14 @@ async function sessionOf(db, projectId2, given) {
48159
48198
  return s.id;
48160
48199
  }
48161
48200
  async function beginTrace(db, projectId2, session) {
48162
- await flush().catch(() => {});
48201
+ await flush(undefined, TOOL_FLUSH_BUDGET_MS).catch(() => {});
48163
48202
  const s = await sessionOf(db, projectId2, session);
48164
48203
  const run = newRunId();
48165
48204
  await openRun(db, { projectId: projectId2, origin: "trace", target: `session:${s}`, sessionId: s, draftId: run });
48166
48205
  return run;
48167
48206
  }
48168
48207
  async function beginGlean(db, projectId2, session) {
48169
- await flush().catch(() => {});
48208
+ await flush(undefined, TOOL_FLUSH_BUDGET_MS).catch(() => {});
48170
48209
  const s = await sessionOf(db, projectId2, session);
48171
48210
  const run = newRunId();
48172
48211
  await openRun(db, { projectId: projectId2, origin: "glean", target: "glean", sessionId: s, draftId: run });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
5
5
  "license": "MIT",
6
6
  "type": "module",