sphica 0.6.15 → 0.6.17

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.15",
4
+ "version": "0.6.17",
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.15",
3
+ "version": "0.6.17",
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",
@@ -0,0 +1,20 @@
1
+ -- Revision 5 → 6 of generation 2: the subagent a delivery ran in (existing rows keep null, the main conversation), and a capture view
2
+ -- that writes it while capture_delivery keeps its columns. `sphica init` runs this in one transaction with foreign keys off, then checks
3
+ -- foreign_key_check. Every statement matches db/schema.sql at revision 6; server/test/migrate.test.ts compares the two.
4
+
5
+ alter table delivery add column agent_id text check (length(agent_id) between 1 and 200);
6
+
7
+ create view capture_delivery_scoped as
8
+ select session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at, null as units from delivery;
9
+ create trigger capture_delivery_scoped_insert instead of insert on capture_delivery_scoped begin
10
+ select raise(abort, 'the unit and the delivered session belong to different projects')
11
+ where exists (select 1 from json_each(coalesce(new.units, '[]')) j
12
+ where (select project_id from unit where id = j.value) is not (select project_id from session where id = new.session_id));
13
+ insert into delivery (session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at)
14
+ values (new.session_id, new.agent_id, new.event, new.outcome, new.reason, new.path, coalesce(new.eligible, 0),
15
+ coalesce(new.omitted, 0), coalesce(new.chars, 0), new.at);
16
+ insert into delivery_unit (delivery_id, unit_id)
17
+ select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
18
+ end;
19
+
20
+ pragma user_version = 6;
package/db/schema.sql CHANGED
@@ -934,8 +934,9 @@ create table work (
934
934
  create index work_open on work (project_id, updated_at desc) where status in ('active', 'blocked', 'paused');
935
935
  create index work_run on work (run_id) where run_id is not null;
936
936
 
937
- -- What a delivery hook emitted or suppressed, and how many eligible units it left out. No source text is copied here.
938
- -- chars is the delivered length without the omission note (Sphica's own text), since the read budget adds it up.
937
+ -- What a delivery hook emitted or suppressed, and how many eligible units it left out (no source text). chars leaves out the omission note,
938
+ -- since the read budget adds it up. A session_start's reason is the start source or 'subagent'; agent_id is the subagent (null: main),
939
+ -- written the way SQLite stores a column ALTER TABLE added, so a migrated database has the same definition.
939
940
  create table delivery (
940
941
  id integer primary key autoincrement not null,
941
942
  session_id text references session (id) on delete cascade,
@@ -947,7 +948,7 @@ create table delivery (
947
948
  omitted integer not null default 0 check (omitted >= 0 and omitted <= eligible),
948
949
  chars integer not null default 0 check (chars >= 0),
949
950
  at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', at) is at)
950
- ) strict;
951
+ , agent_id text check (length(agent_id) between 1 and 200)) strict;
951
952
  create index delivery_session on delivery (session_id, at);
952
953
  create table delivery_unit (
953
954
  delivery_id integer not null references delivery (id) on delete cascade,
@@ -1024,5 +1025,19 @@ create trigger capture_delivery_insert instead of insert on capture_delivery beg
1024
1025
  insert into delivery_unit (delivery_id, unit_id)
1025
1026
  select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
1026
1027
  end;
1028
+ -- The same as capture_delivery with the subagent the delivery ran in. capture_delivery keeps its columns, since an older plugin's
1029
+ -- capture writes through it into a database of this revision
1030
+ create view capture_delivery_scoped as
1031
+ select session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at, null as units from delivery;
1032
+ create trigger capture_delivery_scoped_insert instead of insert on capture_delivery_scoped begin
1033
+ select raise(abort, 'the unit and the delivered session belong to different projects')
1034
+ where exists (select 1 from json_each(coalesce(new.units, '[]')) j
1035
+ where (select project_id from unit where id = j.value) is not (select project_id from session where id = new.session_id));
1036
+ insert into delivery (session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at)
1037
+ values (new.session_id, new.agent_id, new.event, new.outcome, new.reason, new.path, coalesce(new.eligible, 0),
1038
+ coalesce(new.omitted, 0), coalesce(new.chars, 0), new.at);
1039
+ insert into delivery_unit (delivery_id, unit_id)
1040
+ select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
1041
+ end;
1027
1042
 
1028
- pragma user_version = 5;
1043
+ pragma user_version = 6;
package/dist/capture.js CHANGED
@@ -11109,7 +11109,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
11109
11109
 
11110
11110
  // server/src/sqlite.ts
11111
11111
  var SCHEMA_GENERATION = 2;
11112
- var SCHEMA_REVISION = 5;
11112
+ var SCHEMA_REVISION = 6;
11113
11113
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
11114
11114
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
11115
11115
  function requireRuntime() {
@@ -11121,10 +11121,10 @@ function requireFile(file) {
11121
11121
  if (!fs2.existsSync(file))
11122
11122
  throw new Error(`No database at ${file}. Create it with \`sphica init\`.`);
11123
11123
  }
11124
- function prepare(raw, check) {
11124
+ function prepare(raw, check, busyMs = 5000) {
11125
11125
  raw.enableDefensive(true);
11126
11126
  raw.exec("pragma foreign_keys = on");
11127
- raw.exec("pragma busy_timeout = 5000");
11127
+ raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
11128
11128
  if (check === "none")
11129
11129
  return;
11130
11130
  checkGeneration(raw);
@@ -11245,16 +11245,24 @@ var DDL = () => ddl ??= new Set([
11245
11245
  return code;
11246
11246
  }));
11247
11247
  var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
11248
- var CAPTURE_VIEWS = new Set(["capture_session", "capture_message", "capture_edit", "capture_delivery"]);
11248
+ var CAPTURE_VIEWS = new Set([
11249
+ "capture_session",
11250
+ "capture_message",
11251
+ "capture_edit",
11252
+ "capture_delivery",
11253
+ "capture_delivery_scoped"
11254
+ ]);
11249
11255
  var TRIGGER_WRITES = {
11250
11256
  capture_session_insert: new Set(["session"]),
11251
11257
  capture_message_insert: new Set(["source"]),
11252
11258
  capture_edit_insert: new Set(["edit_observation"]),
11253
11259
  capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
11260
+ capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
11254
11261
  source_fts_ai: new Set(["source_fts"])
11255
11262
  };
11256
11263
  var TRIGGER_FUNCTIONS = {
11257
- capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
11264
+ capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
11265
+ capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
11258
11266
  };
11259
11267
  var CAPTURE_READS = {
11260
11268
  project: new Set(["id", "key", "name"]),
@@ -11399,13 +11407,13 @@ function forgetAuthorizer(action, p1, p2) {
11399
11407
  return C2.SQLITE_OK;
11400
11408
  return C2.SQLITE_DENY;
11401
11409
  }
11402
- function connectWriter(role, file = dbFile(), create = false) {
11410
+ function connectWriter(role, file = dbFile(), create = false, busyMs) {
11403
11411
  requireRuntime();
11404
11412
  if (!create)
11405
11413
  requireFile(file);
11406
11414
  const raw = new DatabaseSync2(file);
11407
11415
  try {
11408
- prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
11416
+ prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation", busyMs);
11409
11417
  raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
11410
11418
  } catch (e) {
11411
11419
  raw.close();
@@ -11433,8 +11441,8 @@ function connectWriter(role, file = dbFile(), create = false) {
11433
11441
  }
11434
11442
  return raw;
11435
11443
  }
11436
- function openWriter(role, file = dbFile()) {
11437
- return kyselyOn(() => connectWriter(role, file));
11444
+ function openWriter(role, file = dbFile(), busyMs) {
11445
+ return kyselyOn(() => connectWriter(role, file, false, busyMs));
11438
11446
  }
11439
11447
 
11440
11448
  // server/src/knowledge.ts
package/dist/cli.js CHANGED
@@ -3625,7 +3625,7 @@ ${indent}${caution(note)}` : ""}`);
3625
3625
 
3626
3626
  // server/src/sqlite.ts
3627
3627
  var SCHEMA_GENERATION = 2;
3628
- var SCHEMA_REVISION = 5;
3628
+ var SCHEMA_REVISION = 6;
3629
3629
  var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os2.homedir(), ".sphica");
3630
3630
  var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
3631
3631
  function requireRuntime() {
@@ -3637,10 +3637,10 @@ function requireFile(file) {
3637
3637
  if (!fs3.existsSync(file))
3638
3638
  throw new Error(`No database at ${file}. Create it with \`sphica init\`.`);
3639
3639
  }
3640
- function prepare(raw, check) {
3640
+ function prepare(raw, check, busyMs = 5000) {
3641
3641
  raw.enableDefensive(true);
3642
3642
  raw.exec("pragma foreign_keys = on");
3643
- raw.exec("pragma busy_timeout = 5000");
3643
+ raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
3644
3644
  if (check === "none")
3645
3645
  return;
3646
3646
  checkGeneration(raw);
@@ -14712,16 +14712,24 @@ var DDL = () => ddl ??= new Set([
14712
14712
  return code;
14713
14713
  }));
14714
14714
  var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
14715
- var CAPTURE_VIEWS = new Set(["capture_session", "capture_message", "capture_edit", "capture_delivery"]);
14715
+ var CAPTURE_VIEWS = new Set([
14716
+ "capture_session",
14717
+ "capture_message",
14718
+ "capture_edit",
14719
+ "capture_delivery",
14720
+ "capture_delivery_scoped"
14721
+ ]);
14716
14722
  var TRIGGER_WRITES = {
14717
14723
  capture_session_insert: new Set(["session"]),
14718
14724
  capture_message_insert: new Set(["source"]),
14719
14725
  capture_edit_insert: new Set(["edit_observation"]),
14720
14726
  capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
14727
+ capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
14721
14728
  source_fts_ai: new Set(["source_fts"])
14722
14729
  };
14723
14730
  var TRIGGER_FUNCTIONS = {
14724
- capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
14731
+ capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
14732
+ capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
14725
14733
  };
14726
14734
  var CAPTURE_READS = {
14727
14735
  project: new Set(["id", "key", "name"]),
@@ -14866,13 +14874,13 @@ function forgetAuthorizer(action, p1, p2) {
14866
14874
  return C2.SQLITE_OK;
14867
14875
  return C2.SQLITE_DENY;
14868
14876
  }
14869
- function connectWriter(role, file = dbFile(), create = false) {
14877
+ function connectWriter(role, file = dbFile(), create = false, busyMs) {
14870
14878
  requireRuntime();
14871
14879
  if (!create)
14872
14880
  requireFile(file);
14873
14881
  const raw = new DatabaseSync2(file);
14874
14882
  try {
14875
- prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
14883
+ prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation", busyMs);
14876
14884
  raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
14877
14885
  } catch (e) {
14878
14886
  raw.close();
@@ -14900,8 +14908,8 @@ function connectWriter(role, file = dbFile(), create = false) {
14900
14908
  }
14901
14909
  return raw;
14902
14910
  }
14903
- function openWriter(role, file = dbFile()) {
14904
- return kyselyOn(() => connectWriter(role, file));
14911
+ function openWriter(role, file = dbFile(), busyMs) {
14912
+ return kyselyOn(() => connectWriter(role, file, false, busyMs));
14905
14913
  }
14906
14914
 
14907
14915
  // server/src/admin.ts
package/dist/deliver.js CHANGED
@@ -11134,7 +11134,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
11134
11134
 
11135
11135
  // server/src/sqlite.ts
11136
11136
  var SCHEMA_GENERATION = 2;
11137
- var SCHEMA_REVISION = 5;
11137
+ var SCHEMA_REVISION = 6;
11138
11138
  var sphicaHome = () => process.env.SPHICA_HOME || path3.join(os.homedir(), ".sphica");
11139
11139
  var dbFile = () => process.env.SPHICA_DB || path3.join(sphicaHome(), "sphica.db");
11140
11140
  function requireRuntime() {
@@ -11146,10 +11146,10 @@ function requireFile(file) {
11146
11146
  if (!fs2.existsSync(file))
11147
11147
  throw new Error(`No database at ${file}. Create it with \`sphica init\`.`);
11148
11148
  }
11149
- function prepare(raw, check) {
11149
+ function prepare(raw, check, busyMs = 5000) {
11150
11150
  raw.enableDefensive(true);
11151
11151
  raw.exec("pragma foreign_keys = on");
11152
- raw.exec("pragma busy_timeout = 5000");
11152
+ raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
11153
11153
  if (check === "none")
11154
11154
  return;
11155
11155
  checkGeneration(raw);
@@ -11294,16 +11294,24 @@ var DDL = () => ddl ??= new Set([
11294
11294
  return code;
11295
11295
  }));
11296
11296
  var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
11297
- var CAPTURE_VIEWS = new Set(["capture_session", "capture_message", "capture_edit", "capture_delivery"]);
11297
+ var CAPTURE_VIEWS = new Set([
11298
+ "capture_session",
11299
+ "capture_message",
11300
+ "capture_edit",
11301
+ "capture_delivery",
11302
+ "capture_delivery_scoped"
11303
+ ]);
11298
11304
  var TRIGGER_WRITES = {
11299
11305
  capture_session_insert: new Set(["session"]),
11300
11306
  capture_message_insert: new Set(["source"]),
11301
11307
  capture_edit_insert: new Set(["edit_observation"]),
11302
11308
  capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
11309
+ capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
11303
11310
  source_fts_ai: new Set(["source_fts"])
11304
11311
  };
11305
11312
  var TRIGGER_FUNCTIONS = {
11306
- capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
11313
+ capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
11314
+ capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
11307
11315
  };
11308
11316
  var CAPTURE_READS = {
11309
11317
  project: new Set(["id", "key", "name"]),
@@ -11448,13 +11456,13 @@ function forgetAuthorizer(action, p1, p2) {
11448
11456
  return C2.SQLITE_OK;
11449
11457
  return C2.SQLITE_DENY;
11450
11458
  }
11451
- function connectWriter(role, file = dbFile(), create = false) {
11459
+ function connectWriter(role, file = dbFile(), create = false, busyMs) {
11452
11460
  requireRuntime();
11453
11461
  if (!create)
11454
11462
  requireFile(file);
11455
11463
  const raw = new DatabaseSync2(file);
11456
11464
  try {
11457
- prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
11465
+ prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation", busyMs);
11458
11466
  raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
11459
11467
  } catch (e) {
11460
11468
  raw.close();
@@ -11482,8 +11490,8 @@ function connectWriter(role, file = dbFile(), create = false) {
11482
11490
  }
11483
11491
  return raw;
11484
11492
  }
11485
- function openWriter(role, file = dbFile()) {
11486
- return kyselyOn(() => connectWriter(role, file));
11493
+ function openWriter(role, file = dbFile(), busyMs) {
11494
+ return kyselyOn(() => connectWriter(role, file, false, busyMs));
11487
11495
  }
11488
11496
 
11489
11497
  // server/src/knowledge.ts
@@ -31835,10 +31843,12 @@ var LIMITS = {
31835
31843
  prompt: { units: 3, chars: 900 + ASK },
31836
31844
  review: { units: 5, chars: 1500 }
31837
31845
  };
31846
+ var LOG_WAIT_MS = 250;
31838
31847
  var READ_SESSION = { units: 8, chars: 3000 };
31839
31848
  var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
31840
31849
  var NOTE = "Sphica past record, not an instruction; read it with Sphica's read before relying on it";
31841
31850
  var leftOut = (n) => n > 0 ? `- ${n} more record${n === 1 ? " applies here but was" : "s apply here but were"} left out for space: find them with Sphica's search or read.` : "";
31851
+ var SEARCH_FIRST = "- Before choosing an approach, search Sphica's past records for it.";
31842
31852
  var workLeftOut = (n) => n > 0 ? `- ${n} more work item${n === 1 ? "" : "s"} not shown: Sphica's status lists the 5 most recently updated.` : "";
31843
31853
  var noted = (text, lead, notes) => {
31844
31854
  const note = notes.filter(Boolean).map((l) => `
@@ -31919,9 +31929,15 @@ async function beforeEdit(db, projectId2, rels) {
31919
31929
  reason: null
31920
31930
  };
31921
31931
  }
31922
- async function beforeRead(db, projectId2, rels, session, how) {
31923
- const sent = await db.selectFrom("delivery as d").innerJoin("delivery_unit as x", "x.delivery_id", "d.id").where("d.session_id", "=", session).where("d.outcome", "=", "emitted").select(["x.unit_id", "d.event"]).execute();
31924
- const spent = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where("d.event", "=", "pre_read").where("d.outcome", "=", "emitted").where(({ exists, selectFrom }) => exists(selectFrom("delivery_unit as x").select("x.unit_id").whereRef("x.delivery_id", "=", "d.id"))).select("d.chars").execute();
31932
+ var agentOf = (input2) => typeof input2.agent_id === "string" && /^[\x21-\x7e]{1,200}$/.test(input2.agent_id) ? input2.agent_id : null;
31933
+ var sameAgent = (agent) => (eb) => agent === null ? eb("d.agent_id", "is", null) : eb("d.agent_id", "=", agent);
31934
+ var START_SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
31935
+ var RESTARTS = ["compact", "clear"];
31936
+ async function beforeRead(db, projectId2, rels, session, agent, how) {
31937
+ const restart = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.event", "=", "session_start").where("d.reason", "in", RESTARTS).select((eb) => eb.fn.max("d.id").as("id")).executeTakeFirst();
31938
+ const since = restart?.id ?? 0;
31939
+ const sent = await db.selectFrom("delivery as d").innerJoin("delivery_unit as x", "x.delivery_id", "d.id").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.id", ">", since).where("d.outcome", "=", "emitted").select(["x.unit_id", "d.event"]).execute();
31940
+ const spent = await db.selectFrom("delivery as d").where("d.session_id", "=", session).where(sameAgent(agent)).where("d.id", ">", since).where("d.event", "=", "pre_read").where("d.outcome", "=", "emitted").where(({ exists, selectFrom }) => exists(selectFrom("delivery_unit as x").select("x.unit_id").whereRef("x.delivery_id", "=", "d.id"))).select("d.chars").execute();
31925
31941
  const seen = new Set(sent.map((r) => r.unit_id));
31926
31942
  const readUnits = sent.filter((r) => r.event === "pre_read").length;
31927
31943
  const rows = (await anchoredTo(db, projectId2, rels).where("u.kind", "in", ["decision", "constraint"]).execute()).filter((u) => !seen.has(u.id));
@@ -31973,7 +31989,11 @@ async function namedInCommand(db, projectId2, root, cwd, command) {
31973
31989
  };
31974
31990
  return paths.map((r) => r.path).filter((p) => forms(p).some((t) => new RegExp(`(?:^|[${edge}])${esc2(t)}(?:$|[${edge}:])`).test(command)));
31975
31991
  }
31976
- async function onPrompt(db, projectId2, prompt) {
31992
+ function pathNamed(prompt, root, rel) {
31993
+ const steps = (p) => p.split(/[\\/]/).map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]");
31994
+ return new RegExp(`(?<![A-Za-z0-9_$.\\-/\\\\])(?:(?:\\.[\\\\/])?${steps(rel)}|${steps(path8.join(root, rel))})(?![A-Za-z0-9_$\\-/\\\\]|\\.[A-Za-z0-9_$])`).test(prompt);
31995
+ }
31996
+ async function onPrompt(db, projectId2, root, prompt) {
31977
31997
  const text = prompt.normalize("NFKC");
31978
31998
  const lower = text.toLowerCase();
31979
31999
  const word = (w, s) => new RegExp(`(?<![\\p{L}\\p{N}_$])${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\p{L}\\p{N}_$])`, "u").test(s);
@@ -31986,7 +32006,7 @@ async function onPrompt(db, projectId2, prompt) {
31986
32006
  ]) : [[], []];
31987
32007
  const hits = [];
31988
32008
  for (const u of units) {
31989
- const a = anchors.find((x) => x.unit_id === u.id && (x.symbol && named2(x.symbol) || text.includes(x.path)));
32009
+ const a = anchors.find((x) => x.unit_id === u.id && (x.symbol && named2(x.symbol) || pathNamed(prompt, root, x.path)));
31990
32010
  const o = options.find((x) => x.unit_id === u.id && x.text.length >= 3 && word(x.text.normalize("NFKC").toLowerCase(), lower));
31991
32011
  if (a)
31992
32012
  hits.push({ u, why: ` [names ${a.symbol && named2(a.symbol) ? a.symbol : a.path}]` });
@@ -32047,7 +32067,12 @@ async function atStart(db, projectId2, branch, place) {
32047
32067
  const workLeft = Math.max((workTotal ?? 0) - f.kept.filter((i) => i < work.length).length, 0);
32048
32068
  const broadLeft = Math.max((broadTotal ?? 0) - shownUnits.length, 0);
32049
32069
  return {
32050
- ...noted(f.text, lead, [leftOut(broadLeft), workLeftOut(workLeft), await waiting(db, projectId2, place)]),
32070
+ ...noted(f.text, lead, [
32071
+ leftOut(broadLeft),
32072
+ workLeftOut(workLeft),
32073
+ await waiting(db, projectId2, place),
32074
+ place.subagent ? SEARCH_FIRST : ""
32075
+ ]),
32051
32076
  units: shownUnits,
32052
32077
  eligible: (workTotal ?? 0) + (broadTotal ?? 0),
32053
32078
  omitted: workLeft + broadLeft,
@@ -32115,32 +32140,62 @@ function markOnce(kind, key) {
32115
32140
  function onceUnavailable(session) {
32116
32141
  return markOnce("unavailable", session);
32117
32142
  }
32118
- async function log(file2, projectId2, host, external2, event, plan, outcome, branch) {
32119
- const cap = openWriter("capture", file2);
32143
+ async function write2(trx, e, plan) {
32144
+ const id = sessionId(e.projectId, e.host, e.external);
32145
+ const now = iso(Date.now());
32146
+ await trx.insertInto("capture_session").values({
32147
+ id,
32148
+ project_id: e.projectId,
32149
+ host: e.host,
32150
+ external_id: e.external,
32151
+ branch: e.branch,
32152
+ started_at: now
32153
+ }).execute();
32154
+ await trx.insertInto("capture_delivery_scoped").values({
32155
+ session_id: id,
32156
+ agent_id: e.agent,
32157
+ event: e.event,
32158
+ outcome: plan.text ? "emitted" : "nothing",
32159
+ reason: plan.reason,
32160
+ path: plan.path,
32161
+ eligible: plan.eligible,
32162
+ omitted: plan.omitted,
32163
+ chars: plan.text.length - plan.note.length,
32164
+ at: now,
32165
+ units: JSON.stringify(plan.units)
32166
+ }).execute();
32167
+ }
32168
+ async function log(file2, e, plan) {
32169
+ const cap = openWriter("capture", file2, LOG_WAIT_MS);
32120
32170
  try {
32121
- const id = sessionId(projectId2, host, external2);
32122
- const now = iso(Date.now());
32123
- await cap.insertInto("capture_session").values({ id, project_id: projectId2, host, external_id: external2, branch, started_at: now }).execute();
32124
- await cap.insertInto("capture_delivery").values({
32125
- session_id: id,
32126
- event,
32127
- outcome,
32128
- reason: plan.reason,
32129
- path: plan.path,
32130
- eligible: plan.eligible,
32131
- omitted: plan.omitted,
32132
- chars: plan.text.length - plan.note.length,
32133
- at: now,
32134
- units: JSON.stringify(plan.units)
32135
- }).execute();
32171
+ await inTransaction(cap, (trx) => write2(trx, e, plan));
32172
+ } finally {
32173
+ await cap.destroy().catch(() => {});
32174
+ }
32175
+ }
32176
+ async function lockedPlan(file2, e, make, keep) {
32177
+ const cap = openWriter("capture", file2, LOG_WAIT_MS);
32178
+ let held = false;
32179
+ let plan = null;
32180
+ try {
32181
+ await inTransaction(cap, async (trx) => {
32182
+ held = true;
32183
+ plan = await make();
32184
+ if (keep(plan))
32185
+ await write2(trx, e, plan);
32186
+ });
32187
+ } catch (err) {
32188
+ if (held && !plan)
32189
+ throw err;
32136
32190
  } finally {
32137
32191
  await cap.destroy().catch(() => {});
32138
32192
  }
32193
+ return plan ?? make();
32139
32194
  }
32140
32195
  async function deliver(input2, host = "claude-code", file2 = dbFile()) {
32141
32196
  const name = input2.hook_event_name;
32142
32197
  const call = reviewCall(input2);
32143
- const event = call ? "review" : name === "SessionStart" ? "session_start" : name === "UserPromptSubmit" ? "prompt" : name === "PreToolUse" ? input2.tool_name === "Read" || input2.tool_name === "Bash" && !(host === "codex" && shellPatch(input2)) ? "pre_read" : "pre_edit" : null;
32198
+ const event = call ? "review" : name === "SessionStart" || name === "SubagentStart" ? "session_start" : name === "UserPromptSubmit" ? "prompt" : name === "PreToolUse" ? input2.tool_name === "Read" || input2.tool_name === "Bash" && !(host === "codex" && shellPatch(input2)) ? "pre_read" : "pre_edit" : null;
32144
32199
  if (!event || !input2.session_id)
32145
32200
  return "";
32146
32201
  if (event === "prompt" && !isOwnerTurn(input2, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined))
@@ -32174,19 +32229,38 @@ async function deliver(input2, host = "claude-code", file2 = dbFile()) {
32174
32229
  return "";
32175
32230
  }
32176
32231
  if (event === "session_start" && input2.source === "resume") {
32177
- const said = await db.selectFrom("delivery").select("id").where("session_id", "=", sessionId(pid, host, input2.session_id)).where("event", "=", "session_start").where("outcome", "=", "emitted").executeTakeFirst();
32232
+ const said = await db.selectFrom("delivery").select("id").where("session_id", "=", sessionId(pid, host, input2.session_id)).where("agent_id", "is", null).where("event", "=", "session_start").where((eb) => eb.or([eb("reason", "is", null), eb("reason", "!=", "subagent")])).where("outcome", "=", "emitted").executeTakeFirst();
32178
32233
  if (said)
32179
32234
  return "";
32180
32235
  }
32181
- const plan = event === "pre_edit" ? await beforeEdit(db, pid, rels) : event === "pre_read" ? await beforeRead(db, pid, rels, sessionId(pid, host, input2.session_id), shell ? "named" : "reading") : event === "prompt" ? await onPrompt(db, pid, input2.prompt ?? "") : call ? await beforeReview(db, pid, place.root, call) : await atStart(db, pid, branchOf(place.root), {
32182
- file: file2,
32183
- key: place.key,
32236
+ const reader = db;
32237
+ const session = input2.session_id;
32238
+ const make = async () => event === "pre_edit" ? await beforeEdit(reader, pid, rels) : event === "pre_read" ? await beforeRead(reader, pid, rels, sessionId(pid, host, session), agentOf(input2), shell ? "named" : "reading") : event === "prompt" ? await onPrompt(reader, pid, place.root, input2.prompt ?? "") : call ? await beforeReview(reader, pid, place.root, call) : {
32239
+ ...await atStart(reader, pid, branchOf(place.root), {
32240
+ file: file2,
32241
+ key: place.key,
32242
+ host,
32243
+ subagent: name === "SubagentStart",
32244
+ owner: name !== "SubagentStart" && isOwnerTurn(input2, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined)
32245
+ }),
32246
+ reason: name === "SubagentStart" ? "subagent" : input2.source && START_SOURCES.has(input2.source) ? input2.source : null
32247
+ };
32248
+ const entry = {
32249
+ projectId: pid,
32184
32250
  host,
32185
- owner: isOwnerTurn(input2, undefined, undefined, host === "codex" ? process.env.CODEX_THREAD_ID : undefined)
32186
- });
32251
+ external: session,
32252
+ agent: agentOf(input2),
32253
+ event,
32254
+ branch: branchOf(place.root)
32255
+ };
32256
+ const keep = (p) => !(onPath && !p.text);
32257
+ if (event === "pre_read" || event === "session_start")
32258
+ return (await lockedPlan(file2, entry, make, keep)).text;
32259
+ const plan = await make();
32187
32260
  if (call && plan.text && toldBefore(`${host}\x00${input2.session_id}`, plan.once ?? plan.text))
32188
32261
  return "";
32189
- await log(file2, pid, host, input2.session_id, event, plan, plan.text ? "emitted" : "nothing", branchOf(place.root)).catch(() => {});
32262
+ if (keep(plan))
32263
+ await log(file2, entry, plan).catch(() => {});
32190
32264
  return plan.text;
32191
32265
  } catch (e) {
32192
32266
  if (e instanceof RevisionMismatch)
@@ -45869,7 +45869,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
45869
45869
 
45870
45870
  // server/src/sqlite.ts
45871
45871
  var SCHEMA_GENERATION = 2;
45872
- var SCHEMA_REVISION = 5;
45872
+ var SCHEMA_REVISION = 6;
45873
45873
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
45874
45874
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
45875
45875
  function requireRuntime() {
@@ -45881,10 +45881,10 @@ function requireFile(file2) {
45881
45881
  if (!fs2.existsSync(file2))
45882
45882
  throw new Error(`No database at ${file2}. Create it with \`sphica init\`.`);
45883
45883
  }
45884
- function prepare(raw, check2) {
45884
+ function prepare(raw, check2, busyMs = 5000) {
45885
45885
  raw.enableDefensive(true);
45886
45886
  raw.exec("pragma foreign_keys = on");
45887
- raw.exec("pragma busy_timeout = 5000");
45887
+ raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
45888
45888
  if (check2 === "none")
45889
45889
  return;
45890
45890
  checkGeneration(raw);
@@ -46005,16 +46005,24 @@ var DDL = () => ddl ??= new Set([
46005
46005
  return code;
46006
46006
  }));
46007
46007
  var readsDataVersion = (p1, p2) => p1 === "data_version" && p2 === null;
46008
- var CAPTURE_VIEWS = new Set(["capture_session", "capture_message", "capture_edit", "capture_delivery"]);
46008
+ var CAPTURE_VIEWS = new Set([
46009
+ "capture_session",
46010
+ "capture_message",
46011
+ "capture_edit",
46012
+ "capture_delivery",
46013
+ "capture_delivery_scoped"
46014
+ ]);
46009
46015
  var TRIGGER_WRITES = {
46010
46016
  capture_session_insert: new Set(["session"]),
46011
46017
  capture_message_insert: new Set(["source"]),
46012
46018
  capture_edit_insert: new Set(["edit_observation"]),
46013
46019
  capture_delivery_insert: new Set(["delivery", "delivery_unit"]),
46020
+ capture_delivery_scoped_insert: new Set(["delivery", "delivery_unit"]),
46014
46021
  source_fts_ai: new Set(["source_fts"])
46015
46022
  };
46016
46023
  var TRIGGER_FUNCTIONS = {
46017
- capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
46024
+ capture_delivery_insert: new Set(["coalesce", "json_each", "last_insert_rowid"]),
46025
+ capture_delivery_scoped_insert: new Set(["coalesce", "json_each", "last_insert_rowid"])
46018
46026
  };
46019
46027
  var CAPTURE_READS = {
46020
46028
  project: new Set(["id", "key", "name"]),
@@ -46159,13 +46167,13 @@ function forgetAuthorizer(action, p1, p2) {
46159
46167
  return C2.SQLITE_OK;
46160
46168
  return C2.SQLITE_DENY;
46161
46169
  }
46162
- function connectWriter(role, file2 = dbFile(), create = false) {
46170
+ function connectWriter(role, file2 = dbFile(), create = false, busyMs) {
46163
46171
  requireRuntime();
46164
46172
  if (!create)
46165
46173
  requireFile(file2);
46166
46174
  const raw = new DatabaseSync2(file2);
46167
46175
  try {
46168
- prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
46176
+ prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation", busyMs);
46169
46177
  raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
46170
46178
  } catch (e) {
46171
46179
  raw.close();
@@ -46193,8 +46201,8 @@ function connectWriter(role, file2 = dbFile(), create = false) {
46193
46201
  }
46194
46202
  return raw;
46195
46203
  }
46196
- function openWriter(role, file2 = dbFile()) {
46197
- return kyselyOn(() => connectWriter(role, file2));
46204
+ function openWriter(role, file2 = dbFile(), busyMs) {
46205
+ return kyselyOn(() => connectWriter(role, file2, false, busyMs));
46198
46206
  }
46199
46207
 
46200
46208
  // server/src/extract.ts
package/dist/mcp.js CHANGED
@@ -46030,7 +46030,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
46030
46030
 
46031
46031
  // server/src/sqlite.ts
46032
46032
  var SCHEMA_GENERATION = 2;
46033
- var SCHEMA_REVISION = 5;
46033
+ var SCHEMA_REVISION = 6;
46034
46034
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
46035
46035
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
46036
46036
  function requireRuntime() {
@@ -46042,10 +46042,10 @@ function requireFile(file2) {
46042
46042
  if (!fs2.existsSync(file2))
46043
46043
  throw new Error(`No database at ${file2}. Create it with \`sphica init\`.`);
46044
46044
  }
46045
- function prepare(raw, check2) {
46045
+ function prepare(raw, check2, busyMs = 5000) {
46046
46046
  raw.enableDefensive(true);
46047
46047
  raw.exec("pragma foreign_keys = on");
46048
- raw.exec("pragma busy_timeout = 5000");
46048
+ raw.exec(`pragma busy_timeout = ${Math.trunc(busyMs)}`);
46049
46049
  if (check2 === "none")
46050
46050
  return;
46051
46051
  checkGeneration(raw);
package/hooks/codex.json CHANGED
@@ -18,6 +18,18 @@
18
18
  ]
19
19
  }
20
20
  ],
21
+ "SubagentStart": [
22
+ {
23
+ "hooks": [
24
+ {
25
+ "type": "command",
26
+ "command": "node \"${PLUGIN_ROOT}/dist/deliver.js\" codex",
27
+ "commandWindows": "powershell.exe -NoProfile -NonInteractive -Command node $env:PLUGIN_ROOT/dist/deliver.js codex",
28
+ "timeout": 10
29
+ }
30
+ ]
31
+ }
32
+ ],
21
33
  "UserPromptSubmit": [
22
34
  {
23
35
  "hooks": [
package/hooks/hooks.json CHANGED
@@ -20,6 +20,17 @@
20
20
  ]
21
21
  }
22
22
  ],
23
+ "SubagentStart": [
24
+ {
25
+ "hooks": [
26
+ {
27
+ "type": "command",
28
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/deliver.js\"",
29
+ "timeout": 5
30
+ }
31
+ ]
32
+ }
33
+ ],
23
34
  "UserPromptSubmit": [
24
35
  {
25
36
  "hooks": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
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",