sphica 0.6.0 → 0.6.2

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.2",
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.2",
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/db/schema.sql CHANGED
@@ -667,6 +667,7 @@ create table work (
667
667
  create index work_open on work (project_id, updated_at desc) where status in ('active', 'blocked', 'paused');
668
668
 
669
669
  -- What a delivery hook emitted or suppressed, and how many eligible units it left out. No source text is copied here.
670
+ -- chars is the delivered length without the omission note (Sphica's own text), since the read budget adds it up.
670
671
  create table delivery (
671
672
  id integer primary key autoincrement not null,
672
673
  session_id text references session (id) on delete cascade,
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) {