sphica 0.6.27 → 0.6.29

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.27",
4
+ "version": "0.6.29",
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.27",
3
+ "version": "0.6.29",
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
@@ -11774,32 +11774,103 @@ function captureNotice(file = dbFile()) {
11774
11774
  return panel(`sphica: the database rejected ${plural(s.rejected, "record")}`, [rejectedDir(), `Move them back to ${spoolDir()} to resend`], "Fix them first, then check with sphica doctor");
11775
11775
  return null;
11776
11776
  }
11777
- var baselineFile = (host, session) => path6.join(baselineDir(), `${digest(`${host}\x00${session}`)}.json`);
11778
- function readBaseline(file) {
11777
+ var turnDir = (host, session) => path6.join(baselineDir(), digest(`${host}\x00${session}`));
11778
+ var startFile = (dir, turn) => path6.join(dir, `${digest(turn)}.json`);
11779
+ var isStart = (v) => {
11780
+ const s = v;
11781
+ return !!s && typeof s === "object" && typeof s.turn === "string" && typeof s.running === "boolean" && (s.seq === null || Number.isSafeInteger(s.seq)) && (s.entries === null || typeof s.entries === "object" && !Array.isArray(s.entries)) && (s.head === null || typeof s.head === "string");
11782
+ };
11783
+ function readStart(file) {
11779
11784
  try {
11780
- return JSON.parse(fs5.readFileSync(file, "utf8"));
11785
+ const s = JSON.parse(fs5.readFileSync(file, "utf8"));
11786
+ return isStart(s) ? s : null;
11781
11787
  } catch {
11782
11788
  return null;
11783
11789
  }
11784
11790
  }
11785
- function writeBaseline(file, b) {
11786
- fs5.mkdirSync(path6.dirname(file), { recursive: true, mode: 448 });
11787
- const tmp = `${file}.${process.pid}.tmp`;
11788
- fs5.writeFileSync(tmp, JSON.stringify(b), { mode: 384 });
11791
+ function readStarts(dir) {
11792
+ let names;
11793
+ try {
11794
+ names = fs5.readdirSync(dir);
11795
+ } catch (e) {
11796
+ return e.code === "ENOENT" ? [] : null;
11797
+ }
11798
+ const starts = [];
11799
+ for (const name of names) {
11800
+ if (name.startsWith("."))
11801
+ continue;
11802
+ const s = readStart(path6.join(dir, name));
11803
+ if (!s)
11804
+ return null;
11805
+ starts.push(s);
11806
+ }
11807
+ return starts;
11808
+ }
11809
+ function newestTurn(starts) {
11810
+ if (!starts?.length || starts.some((s) => s.seq === null))
11811
+ return null;
11812
+ const top = Math.max(...starts.map((s) => s.seq));
11813
+ const at = starts.filter((s) => s.seq === top);
11814
+ return at.length === 1 ? at[0]?.turn ?? null : null;
11815
+ }
11816
+ function writeStart(dir, s, file = startFile(dir, s.turn)) {
11817
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
11818
+ const tmp = path6.join(dir, `.${path6.basename(file)}.${process.pid}.tmp`);
11819
+ fs5.writeFileSync(tmp, JSON.stringify(s), { mode: 384 });
11789
11820
  fs5.renameSync(tmp, file);
11790
11821
  }
11822
+ function openTurn(dir, turn, root) {
11823
+ if (readStart(startFile(dir, turn))?.running)
11824
+ return null;
11825
+ const starts = readStarts(dir);
11826
+ const seq = starts && !starts.some((s) => s.seq === null) ? Math.max(0, ...starts.map((s) => s.seq)) + 1 : null;
11827
+ const now = snapshot(root);
11828
+ return () => writeStart(dir, { head: now?.head ?? null, entries: now?.entries ?? null, running: true, turn, seq });
11829
+ }
11830
+ function closeTurn(dir, turn, root) {
11831
+ const own = readStart(startFile(dir, turn));
11832
+ if (!own?.entries)
11833
+ return null;
11834
+ const before = { head: own.head, entries: own.entries };
11835
+ const now = snapshot(root);
11836
+ return () => {
11837
+ if (newestTurn(readStarts(dir)) !== turn || JSON.stringify(readStart(startFile(dir, turn))) !== JSON.stringify(own))
11838
+ return [];
11839
+ writeStart(dir, { ...own, ...now ?? {}, running: false });
11840
+ return now ? changed(root, before, now) : [];
11841
+ };
11842
+ }
11843
+ function stopTurn(dir, turn) {
11844
+ const own = readStart(startFile(dir, turn));
11845
+ if (own?.entries || own?.running)
11846
+ writeStart(dir, { ...own, head: null, entries: null, running: false });
11847
+ }
11791
11848
  function pruneBaselines() {
11792
11849
  const cutoff = Date.now() - HOLD_DAYS * 24 * 60 * 60 * 1000;
11793
- let files;
11850
+ const fresh = (file) => (fs5.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) >= cutoff;
11851
+ let names;
11794
11852
  try {
11795
- files = fs5.readdirSync(baselineDir());
11853
+ names = fs5.readdirSync(baselineDir());
11796
11854
  } catch {
11797
11855
  return;
11798
11856
  }
11799
- for (const f of files) {
11800
- const file = path6.join(baselineDir(), f);
11801
- if ((fs5.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) < cutoff)
11802
- fs5.rmSync(file, { force: true });
11857
+ for (const name of names) {
11858
+ const at = path6.join(baselineDir(), name);
11859
+ if (!fs5.statSync(at, { throwIfNoEntry: false })?.isDirectory()) {
11860
+ if (!fresh(at))
11861
+ fs5.rmSync(at, { force: true });
11862
+ } else {
11863
+ const files = fs5.readdirSync(at);
11864
+ if (files.length && !files.some((f) => fresh(path6.join(at, f))))
11865
+ fs5.rmSync(at, { recursive: true, force: true });
11866
+ }
11867
+ }
11868
+ }
11869
+ function attempt(fn) {
11870
+ try {
11871
+ return fn();
11872
+ } catch {
11873
+ return;
11803
11874
  }
11804
11875
  }
11805
11876
  function onHook(host, input) {
@@ -11813,17 +11884,17 @@ function onHook(host, input) {
11813
11884
  fs5.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
11814
11885
  `);
11815
11886
  }
11816
- const place2 = identify(input.cwd ?? process.cwd());
11817
- const now = place2 && snapshot(place2.root);
11818
- if (now)
11819
- writeBaseline(baselineFile(host, String(input.session_id)), { ...now, running: false });
11820
- pruneBaselines();
11887
+ attempt(pruneBaselines);
11821
11888
  return { flush: false, notice: captureNotice() };
11822
11889
  }
11823
11890
  if (!owner())
11824
11891
  return { flush: false };
11825
- if (event === "Interrupt")
11892
+ if (event === "Interrupt") {
11893
+ const turn2 = input.prompt_id ?? input.turn_id;
11894
+ if (turn2)
11895
+ attempt(() => stopTurn(turnDir(host, String(input.session_id)), turn2));
11826
11896
  return { flush: true };
11897
+ }
11827
11898
  const place = identify(input.cwd ?? process.cwd());
11828
11899
  if (!place)
11829
11900
  return { flush: false };
@@ -11847,30 +11918,26 @@ function onHook(host, input) {
11847
11918
  const id = `${key}:${digest(kept.body)}`;
11848
11919
  spool({ ...base, at: when, kind: "message", id, speaker, ...kept });
11849
11920
  };
11850
- const baseline = baselineFile(host, base.session);
11851
- if (event === "UserPromptSubmit") {
11852
- if (!readBaseline(baseline)?.running) {
11853
- const now = snapshot(place.root);
11854
- if (now)
11855
- writeBaseline(baseline, { ...now, running: true });
11856
- }
11857
- }
11921
+ const dir = turnDir(host, base.session);
11858
11922
  if (event === "UserPromptSubmit" && input.prompt) {
11859
11923
  const prompt = input.prompt.trimStart();
11860
11924
  if (!INJECTED.some((r) => r.test(prompt)))
11861
11925
  say(`${turn}:owner`, "owner", prompt);
11862
11926
  }
11927
+ if (event === "UserPromptSubmit")
11928
+ attempt(() => openTurn(dir, turn, place.root)?.());
11863
11929
  if (event === "Stop") {
11864
11930
  if (input.last_assistant_message)
11865
11931
  say(`${turn}:assistant`, "assistant", input.last_assistant_message);
11866
- const now = snapshot(place.root);
11867
- if (now) {
11868
- const before = readBaseline(baseline);
11869
- if (before)
11870
- for (const p of changed(place.root, before, now))
11871
- spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
11872
- writeBaseline(baseline, { ...now, running: false });
11873
- }
11932
+ const paths = attempt(() => {
11933
+ const finish = closeTurn(dir, turn, place.root);
11934
+ if (finish)
11935
+ return finish();
11936
+ stopTurn(dir, turn);
11937
+ return [];
11938
+ });
11939
+ for (const p of paths ?? [])
11940
+ spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
11874
11941
  return { flush: true };
11875
11942
  }
11876
11943
  if (event === "PostToolUse") {
@@ -11930,7 +11997,7 @@ function readState() {
11930
11997
  function lock() {
11931
11998
  const file = path6.join(spoolDir(), ".lock");
11932
11999
  fs5.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
11933
- for (let attempt = 0;attempt < 2; attempt++) {
12000
+ for (let attempt2 = 0;attempt2 < 2; attempt2++) {
11934
12001
  try {
11935
12002
  fs5.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
11936
12003
  return () => {
@@ -12228,15 +12295,18 @@ export {
12228
12295
  answersOf,
12229
12296
  branchOf,
12230
12297
  captureNotice,
12298
+ closeTurn,
12231
12299
  current,
12232
12300
  fit,
12233
12301
  flush,
12234
12302
  isOwnerTurn,
12235
12303
  onHook,
12304
+ openTurn,
12236
12305
  readInput,
12237
12306
  readState,
12238
12307
  rejectedDir,
12239
12308
  spoolDir,
12309
+ turnDir,
12240
12310
  unregisteredDir,
12241
12311
  write
12242
12312
  };
package/dist/cli.js CHANGED
@@ -15237,32 +15237,103 @@ function captureNotice(file = dbFile()) {
15237
15237
  return panel(`sphica: the database rejected ${plural(s.rejected, "record")}`, [rejectedDir(), `Move them back to ${spoolDir()} to resend`], "Fix them first, then check with sphica doctor");
15238
15238
  return null;
15239
15239
  }
15240
- var baselineFile = (host, session) => path8.join(baselineDir(), `${digest(`${host}\x00${session}`)}.json`);
15241
- function readBaseline(file) {
15240
+ var turnDir = (host, session) => path8.join(baselineDir(), digest(`${host}\x00${session}`));
15241
+ var startFile = (dir, turn) => path8.join(dir, `${digest(turn)}.json`);
15242
+ var isStart = (v) => {
15243
+ const s = v;
15244
+ return !!s && typeof s === "object" && typeof s.turn === "string" && typeof s.running === "boolean" && (s.seq === null || Number.isSafeInteger(s.seq)) && (s.entries === null || typeof s.entries === "object" && !Array.isArray(s.entries)) && (s.head === null || typeof s.head === "string");
15245
+ };
15246
+ function readStart(file) {
15242
15247
  try {
15243
- return JSON.parse(fs7.readFileSync(file, "utf8"));
15248
+ const s = JSON.parse(fs7.readFileSync(file, "utf8"));
15249
+ return isStart(s) ? s : null;
15244
15250
  } catch {
15245
15251
  return null;
15246
15252
  }
15247
15253
  }
15248
- function writeBaseline(file, b) {
15249
- fs7.mkdirSync(path8.dirname(file), { recursive: true, mode: 448 });
15250
- const tmp = `${file}.${process.pid}.tmp`;
15251
- fs7.writeFileSync(tmp, JSON.stringify(b), { mode: 384 });
15254
+ function readStarts(dir) {
15255
+ let names;
15256
+ try {
15257
+ names = fs7.readdirSync(dir);
15258
+ } catch (e) {
15259
+ return e.code === "ENOENT" ? [] : null;
15260
+ }
15261
+ const starts = [];
15262
+ for (const name of names) {
15263
+ if (name.startsWith("."))
15264
+ continue;
15265
+ const s = readStart(path8.join(dir, name));
15266
+ if (!s)
15267
+ return null;
15268
+ starts.push(s);
15269
+ }
15270
+ return starts;
15271
+ }
15272
+ function newestTurn(starts) {
15273
+ if (!starts?.length || starts.some((s) => s.seq === null))
15274
+ return null;
15275
+ const top = Math.max(...starts.map((s) => s.seq));
15276
+ const at = starts.filter((s) => s.seq === top);
15277
+ return at.length === 1 ? at[0]?.turn ?? null : null;
15278
+ }
15279
+ function writeStart(dir, s, file = startFile(dir, s.turn)) {
15280
+ fs7.mkdirSync(dir, { recursive: true, mode: 448 });
15281
+ const tmp = path8.join(dir, `.${path8.basename(file)}.${process.pid}.tmp`);
15282
+ fs7.writeFileSync(tmp, JSON.stringify(s), { mode: 384 });
15252
15283
  fs7.renameSync(tmp, file);
15253
15284
  }
15285
+ function openTurn(dir, turn, root) {
15286
+ if (readStart(startFile(dir, turn))?.running)
15287
+ return null;
15288
+ const starts = readStarts(dir);
15289
+ const seq = starts && !starts.some((s) => s.seq === null) ? Math.max(0, ...starts.map((s) => s.seq)) + 1 : null;
15290
+ const now = snapshot(root);
15291
+ return () => writeStart(dir, { head: now?.head ?? null, entries: now?.entries ?? null, running: true, turn, seq });
15292
+ }
15293
+ function closeTurn(dir, turn, root) {
15294
+ const own = readStart(startFile(dir, turn));
15295
+ if (!own?.entries)
15296
+ return null;
15297
+ const before = { head: own.head, entries: own.entries };
15298
+ const now = snapshot(root);
15299
+ return () => {
15300
+ if (newestTurn(readStarts(dir)) !== turn || JSON.stringify(readStart(startFile(dir, turn))) !== JSON.stringify(own))
15301
+ return [];
15302
+ writeStart(dir, { ...own, ...now ?? {}, running: false });
15303
+ return now ? changed(root, before, now) : [];
15304
+ };
15305
+ }
15306
+ function stopTurn(dir, turn) {
15307
+ const own = readStart(startFile(dir, turn));
15308
+ if (own?.entries || own?.running)
15309
+ writeStart(dir, { ...own, head: null, entries: null, running: false });
15310
+ }
15254
15311
  function pruneBaselines() {
15255
15312
  const cutoff = Date.now() - HOLD_DAYS * 24 * 60 * 60 * 1000;
15256
- let files;
15313
+ const fresh = (file) => (fs7.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) >= cutoff;
15314
+ let names;
15257
15315
  try {
15258
- files = fs7.readdirSync(baselineDir());
15316
+ names = fs7.readdirSync(baselineDir());
15259
15317
  } catch {
15260
15318
  return;
15261
15319
  }
15262
- for (const f of files) {
15263
- const file = path8.join(baselineDir(), f);
15264
- if ((fs7.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) < cutoff)
15265
- fs7.rmSync(file, { force: true });
15320
+ for (const name of names) {
15321
+ const at = path8.join(baselineDir(), name);
15322
+ if (!fs7.statSync(at, { throwIfNoEntry: false })?.isDirectory()) {
15323
+ if (!fresh(at))
15324
+ fs7.rmSync(at, { force: true });
15325
+ } else {
15326
+ const files = fs7.readdirSync(at);
15327
+ if (files.length && !files.some((f) => fresh(path8.join(at, f))))
15328
+ fs7.rmSync(at, { recursive: true, force: true });
15329
+ }
15330
+ }
15331
+ }
15332
+ function attempt(fn) {
15333
+ try {
15334
+ return fn();
15335
+ } catch {
15336
+ return;
15266
15337
  }
15267
15338
  }
15268
15339
  function onHook(host, input) {
@@ -15276,17 +15347,17 @@ function onHook(host, input) {
15276
15347
  fs7.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
15277
15348
  `);
15278
15349
  }
15279
- const place2 = identify(input.cwd ?? process.cwd());
15280
- const now = place2 && snapshot(place2.root);
15281
- if (now)
15282
- writeBaseline(baselineFile(host, String(input.session_id)), { ...now, running: false });
15283
- pruneBaselines();
15350
+ attempt(pruneBaselines);
15284
15351
  return { flush: false, notice: captureNotice() };
15285
15352
  }
15286
15353
  if (!owner())
15287
15354
  return { flush: false };
15288
- if (event === "Interrupt")
15355
+ if (event === "Interrupt") {
15356
+ const turn2 = input.prompt_id ?? input.turn_id;
15357
+ if (turn2)
15358
+ attempt(() => stopTurn(turnDir(host, String(input.session_id)), turn2));
15289
15359
  return { flush: true };
15360
+ }
15290
15361
  const place = identify(input.cwd ?? process.cwd());
15291
15362
  if (!place)
15292
15363
  return { flush: false };
@@ -15310,30 +15381,26 @@ function onHook(host, input) {
15310
15381
  const id = `${key}:${digest(kept.body)}`;
15311
15382
  spool({ ...base, at: when, kind: "message", id, speaker, ...kept });
15312
15383
  };
15313
- const baseline = baselineFile(host, base.session);
15314
- if (event === "UserPromptSubmit") {
15315
- if (!readBaseline(baseline)?.running) {
15316
- const now = snapshot(place.root);
15317
- if (now)
15318
- writeBaseline(baseline, { ...now, running: true });
15319
- }
15320
- }
15384
+ const dir = turnDir(host, base.session);
15321
15385
  if (event === "UserPromptSubmit" && input.prompt) {
15322
15386
  const prompt = input.prompt.trimStart();
15323
15387
  if (!INJECTED.some((r2) => r2.test(prompt)))
15324
15388
  say(`${turn}:owner`, "owner", prompt);
15325
15389
  }
15390
+ if (event === "UserPromptSubmit")
15391
+ attempt(() => openTurn(dir, turn, place.root)?.());
15326
15392
  if (event === "Stop") {
15327
15393
  if (input.last_assistant_message)
15328
15394
  say(`${turn}:assistant`, "assistant", input.last_assistant_message);
15329
- const now = snapshot(place.root);
15330
- if (now) {
15331
- const before = readBaseline(baseline);
15332
- if (before)
15333
- for (const p of changed(place.root, before, now))
15334
- spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
15335
- writeBaseline(baseline, { ...now, running: false });
15336
- }
15395
+ const paths = attempt(() => {
15396
+ const finish = closeTurn(dir, turn, place.root);
15397
+ if (finish)
15398
+ return finish();
15399
+ stopTurn(dir, turn);
15400
+ return [];
15401
+ });
15402
+ for (const p of paths ?? [])
15403
+ spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
15337
15404
  return { flush: true };
15338
15405
  }
15339
15406
  if (event === "PostToolUse") {
@@ -15393,7 +15460,7 @@ function readState() {
15393
15460
  function lock() {
15394
15461
  const file = path8.join(spoolDir(), ".lock");
15395
15462
  fs7.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
15396
- for (let attempt = 0;attempt < 2; attempt++) {
15463
+ for (let attempt2 = 0;attempt2 < 2; attempt2++) {
15397
15464
  try {
15398
15465
  fs7.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
15399
15466
  return () => {
package/dist/deliver.js CHANGED
@@ -11806,32 +11806,103 @@ function captureNotice(file = dbFile()) {
11806
11806
  return panel(`sphica: the database rejected ${plural(s.rejected, "record")}`, [rejectedDir(), `Move them back to ${spoolDir()} to resend`], "Fix them first, then check with sphica doctor");
11807
11807
  return null;
11808
11808
  }
11809
- var baselineFile = (host, session) => path6.join(baselineDir(), `${digest(`${host}\x00${session}`)}.json`);
11810
- function readBaseline(file) {
11809
+ var turnDir = (host, session) => path6.join(baselineDir(), digest(`${host}\x00${session}`));
11810
+ var startFile = (dir, turn) => path6.join(dir, `${digest(turn)}.json`);
11811
+ var isStart = (v) => {
11812
+ const s = v;
11813
+ return !!s && typeof s === "object" && typeof s.turn === "string" && typeof s.running === "boolean" && (s.seq === null || Number.isSafeInteger(s.seq)) && (s.entries === null || typeof s.entries === "object" && !Array.isArray(s.entries)) && (s.head === null || typeof s.head === "string");
11814
+ };
11815
+ function readStart(file) {
11811
11816
  try {
11812
- return JSON.parse(fs5.readFileSync(file, "utf8"));
11817
+ const s = JSON.parse(fs5.readFileSync(file, "utf8"));
11818
+ return isStart(s) ? s : null;
11813
11819
  } catch {
11814
11820
  return null;
11815
11821
  }
11816
11822
  }
11817
- function writeBaseline(file, b) {
11818
- fs5.mkdirSync(path6.dirname(file), { recursive: true, mode: 448 });
11819
- const tmp = `${file}.${process.pid}.tmp`;
11820
- fs5.writeFileSync(tmp, JSON.stringify(b), { mode: 384 });
11823
+ function readStarts(dir) {
11824
+ let names;
11825
+ try {
11826
+ names = fs5.readdirSync(dir);
11827
+ } catch (e) {
11828
+ return e.code === "ENOENT" ? [] : null;
11829
+ }
11830
+ const starts = [];
11831
+ for (const name of names) {
11832
+ if (name.startsWith("."))
11833
+ continue;
11834
+ const s = readStart(path6.join(dir, name));
11835
+ if (!s)
11836
+ return null;
11837
+ starts.push(s);
11838
+ }
11839
+ return starts;
11840
+ }
11841
+ function newestTurn(starts) {
11842
+ if (!starts?.length || starts.some((s) => s.seq === null))
11843
+ return null;
11844
+ const top = Math.max(...starts.map((s) => s.seq));
11845
+ const at = starts.filter((s) => s.seq === top);
11846
+ return at.length === 1 ? at[0]?.turn ?? null : null;
11847
+ }
11848
+ function writeStart(dir, s, file = startFile(dir, s.turn)) {
11849
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
11850
+ const tmp = path6.join(dir, `.${path6.basename(file)}.${process.pid}.tmp`);
11851
+ fs5.writeFileSync(tmp, JSON.stringify(s), { mode: 384 });
11821
11852
  fs5.renameSync(tmp, file);
11822
11853
  }
11854
+ function openTurn(dir, turn, root) {
11855
+ if (readStart(startFile(dir, turn))?.running)
11856
+ return null;
11857
+ const starts = readStarts(dir);
11858
+ const seq = starts && !starts.some((s) => s.seq === null) ? Math.max(0, ...starts.map((s) => s.seq)) + 1 : null;
11859
+ const now = snapshot(root);
11860
+ return () => writeStart(dir, { head: now?.head ?? null, entries: now?.entries ?? null, running: true, turn, seq });
11861
+ }
11862
+ function closeTurn(dir, turn, root) {
11863
+ const own = readStart(startFile(dir, turn));
11864
+ if (!own?.entries)
11865
+ return null;
11866
+ const before = { head: own.head, entries: own.entries };
11867
+ const now = snapshot(root);
11868
+ return () => {
11869
+ if (newestTurn(readStarts(dir)) !== turn || JSON.stringify(readStart(startFile(dir, turn))) !== JSON.stringify(own))
11870
+ return [];
11871
+ writeStart(dir, { ...own, ...now ?? {}, running: false });
11872
+ return now ? changed(root, before, now) : [];
11873
+ };
11874
+ }
11875
+ function stopTurn(dir, turn) {
11876
+ const own = readStart(startFile(dir, turn));
11877
+ if (own?.entries || own?.running)
11878
+ writeStart(dir, { ...own, head: null, entries: null, running: false });
11879
+ }
11823
11880
  function pruneBaselines() {
11824
11881
  const cutoff = Date.now() - HOLD_DAYS * 24 * 60 * 60 * 1000;
11825
- let files;
11882
+ const fresh = (file) => (fs5.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) >= cutoff;
11883
+ let names;
11826
11884
  try {
11827
- files = fs5.readdirSync(baselineDir());
11885
+ names = fs5.readdirSync(baselineDir());
11828
11886
  } catch {
11829
11887
  return;
11830
11888
  }
11831
- for (const f of files) {
11832
- const file = path6.join(baselineDir(), f);
11833
- if ((fs5.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) < cutoff)
11834
- fs5.rmSync(file, { force: true });
11889
+ for (const name of names) {
11890
+ const at = path6.join(baselineDir(), name);
11891
+ if (!fs5.statSync(at, { throwIfNoEntry: false })?.isDirectory()) {
11892
+ if (!fresh(at))
11893
+ fs5.rmSync(at, { force: true });
11894
+ } else {
11895
+ const files = fs5.readdirSync(at);
11896
+ if (files.length && !files.some((f) => fresh(path6.join(at, f))))
11897
+ fs5.rmSync(at, { recursive: true, force: true });
11898
+ }
11899
+ }
11900
+ }
11901
+ function attempt(fn) {
11902
+ try {
11903
+ return fn();
11904
+ } catch {
11905
+ return;
11835
11906
  }
11836
11907
  }
11837
11908
  function onHook(host, input) {
@@ -11845,17 +11916,17 @@ function onHook(host, input) {
11845
11916
  fs5.appendFileSync(file, `export SPHICA_PARENT_SESSION=${input.session_id}
11846
11917
  `);
11847
11918
  }
11848
- const place2 = identify(input.cwd ?? process.cwd());
11849
- const now = place2 && snapshot(place2.root);
11850
- if (now)
11851
- writeBaseline(baselineFile(host, String(input.session_id)), { ...now, running: false });
11852
- pruneBaselines();
11919
+ attempt(pruneBaselines);
11853
11920
  return { flush: false, notice: captureNotice() };
11854
11921
  }
11855
11922
  if (!owner())
11856
11923
  return { flush: false };
11857
- if (event === "Interrupt")
11924
+ if (event === "Interrupt") {
11925
+ const turn2 = input.prompt_id ?? input.turn_id;
11926
+ if (turn2)
11927
+ attempt(() => stopTurn(turnDir(host, String(input.session_id)), turn2));
11858
11928
  return { flush: true };
11929
+ }
11859
11930
  const place = identify(input.cwd ?? process.cwd());
11860
11931
  if (!place)
11861
11932
  return { flush: false };
@@ -11879,30 +11950,26 @@ function onHook(host, input) {
11879
11950
  const id = `${key}:${digest(kept.body)}`;
11880
11951
  spool({ ...base, at: when, kind: "message", id, speaker, ...kept });
11881
11952
  };
11882
- const baseline = baselineFile(host, base.session);
11883
- if (event === "UserPromptSubmit") {
11884
- if (!readBaseline(baseline)?.running) {
11885
- const now = snapshot(place.root);
11886
- if (now)
11887
- writeBaseline(baseline, { ...now, running: true });
11888
- }
11889
- }
11953
+ const dir = turnDir(host, base.session);
11890
11954
  if (event === "UserPromptSubmit" && input.prompt) {
11891
11955
  const prompt = input.prompt.trimStart();
11892
11956
  if (!INJECTED.some((r) => r.test(prompt)))
11893
11957
  say(`${turn}:owner`, "owner", prompt);
11894
11958
  }
11959
+ if (event === "UserPromptSubmit")
11960
+ attempt(() => openTurn(dir, turn, place.root)?.());
11895
11961
  if (event === "Stop") {
11896
11962
  if (input.last_assistant_message)
11897
11963
  say(`${turn}:assistant`, "assistant", input.last_assistant_message);
11898
- const now = snapshot(place.root);
11899
- if (now) {
11900
- const before = readBaseline(baseline);
11901
- if (before)
11902
- for (const p of changed(place.root, before, now))
11903
- spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
11904
- writeBaseline(baseline, { ...now, running: false });
11905
- }
11964
+ const paths = attempt(() => {
11965
+ const finish = closeTurn(dir, turn, place.root);
11966
+ if (finish)
11967
+ return finish();
11968
+ stopTurn(dir, turn);
11969
+ return [];
11970
+ });
11971
+ for (const p of paths ?? [])
11972
+ spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
11906
11973
  return { flush: true };
11907
11974
  }
11908
11975
  if (event === "PostToolUse") {
@@ -11962,7 +12029,7 @@ function readState() {
11962
12029
  function lock() {
11963
12030
  const file = path6.join(spoolDir(), ".lock");
11964
12031
  fs5.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
11965
- for (let attempt = 0;attempt < 2; attempt++) {
12032
+ for (let attempt2 = 0;attempt2 < 2; attempt2++) {
11966
12033
  try {
11967
12034
  fs5.writeFileSync(file, String(process.pid), { flag: "wx", mode: 384 });
11968
12035
  return () => {
@@ -46723,32 +46723,103 @@ function captureNotice(file2 = dbFile()) {
46723
46723
  return panel(`sphica: the database rejected ${plural2(s.rejected, "record")}`, [rejectedDir(), `Move them back to ${spoolDir()} to resend`], "Fix them first, then check with sphica doctor");
46724
46724
  return null;
46725
46725
  }
46726
- var baselineFile = (host, session) => path6.join(baselineDir(), `${digest(`${host}\x00${session}`)}.json`);
46727
- function readBaseline(file2) {
46726
+ var turnDir = (host, session) => path6.join(baselineDir(), digest(`${host}\x00${session}`));
46727
+ var startFile = (dir, turn) => path6.join(dir, `${digest(turn)}.json`);
46728
+ var isStart = (v) => {
46729
+ const s = v;
46730
+ return !!s && typeof s === "object" && typeof s.turn === "string" && typeof s.running === "boolean" && (s.seq === null || Number.isSafeInteger(s.seq)) && (s.entries === null || typeof s.entries === "object" && !Array.isArray(s.entries)) && (s.head === null || typeof s.head === "string");
46731
+ };
46732
+ function readStart(file2) {
46728
46733
  try {
46729
- return JSON.parse(fs6.readFileSync(file2, "utf8"));
46734
+ const s = JSON.parse(fs6.readFileSync(file2, "utf8"));
46735
+ return isStart(s) ? s : null;
46730
46736
  } catch {
46731
46737
  return null;
46732
46738
  }
46733
46739
  }
46734
- function writeBaseline(file2, b) {
46735
- fs6.mkdirSync(path6.dirname(file2), { recursive: true, mode: 448 });
46736
- const tmp = `${file2}.${process.pid}.tmp`;
46737
- fs6.writeFileSync(tmp, JSON.stringify(b), { mode: 384 });
46740
+ function readStarts(dir) {
46741
+ let names;
46742
+ try {
46743
+ names = fs6.readdirSync(dir);
46744
+ } catch (e) {
46745
+ return e.code === "ENOENT" ? [] : null;
46746
+ }
46747
+ const starts2 = [];
46748
+ for (const name of names) {
46749
+ if (name.startsWith("."))
46750
+ continue;
46751
+ const s = readStart(path6.join(dir, name));
46752
+ if (!s)
46753
+ return null;
46754
+ starts2.push(s);
46755
+ }
46756
+ return starts2;
46757
+ }
46758
+ function newestTurn(starts2) {
46759
+ if (!starts2?.length || starts2.some((s) => s.seq === null))
46760
+ return null;
46761
+ const top = Math.max(...starts2.map((s) => s.seq));
46762
+ const at = starts2.filter((s) => s.seq === top);
46763
+ return at.length === 1 ? at[0]?.turn ?? null : null;
46764
+ }
46765
+ function writeStart(dir, s, file2 = startFile(dir, s.turn)) {
46766
+ fs6.mkdirSync(dir, { recursive: true, mode: 448 });
46767
+ const tmp = path6.join(dir, `.${path6.basename(file2)}.${process.pid}.tmp`);
46768
+ fs6.writeFileSync(tmp, JSON.stringify(s), { mode: 384 });
46738
46769
  fs6.renameSync(tmp, file2);
46739
46770
  }
46771
+ function openTurn(dir, turn, root) {
46772
+ if (readStart(startFile(dir, turn))?.running)
46773
+ return null;
46774
+ const starts2 = readStarts(dir);
46775
+ const seq = starts2 && !starts2.some((s) => s.seq === null) ? Math.max(0, ...starts2.map((s) => s.seq)) + 1 : null;
46776
+ const now = snapshot(root);
46777
+ return () => writeStart(dir, { head: now?.head ?? null, entries: now?.entries ?? null, running: true, turn, seq });
46778
+ }
46779
+ function closeTurn(dir, turn, root) {
46780
+ const own2 = readStart(startFile(dir, turn));
46781
+ if (!own2?.entries)
46782
+ return null;
46783
+ const before = { head: own2.head, entries: own2.entries };
46784
+ const now = snapshot(root);
46785
+ return () => {
46786
+ if (newestTurn(readStarts(dir)) !== turn || JSON.stringify(readStart(startFile(dir, turn))) !== JSON.stringify(own2))
46787
+ return [];
46788
+ writeStart(dir, { ...own2, ...now ?? {}, running: false });
46789
+ return now ? changed(root, before, now) : [];
46790
+ };
46791
+ }
46792
+ function stopTurn(dir, turn) {
46793
+ const own2 = readStart(startFile(dir, turn));
46794
+ if (own2?.entries || own2?.running)
46795
+ writeStart(dir, { ...own2, head: null, entries: null, running: false });
46796
+ }
46740
46797
  function pruneBaselines() {
46741
46798
  const cutoff = Date.now() - HOLD_DAYS * 24 * 60 * 60 * 1000;
46742
- let files;
46799
+ const fresh = (file2) => (fs6.statSync(file2, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) >= cutoff;
46800
+ let names;
46743
46801
  try {
46744
- files = fs6.readdirSync(baselineDir());
46802
+ names = fs6.readdirSync(baselineDir());
46745
46803
  } catch {
46746
46804
  return;
46747
46805
  }
46748
- for (const f of files) {
46749
- const file2 = path6.join(baselineDir(), f);
46750
- if ((fs6.statSync(file2, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()) < cutoff)
46751
- fs6.rmSync(file2, { force: true });
46806
+ for (const name of names) {
46807
+ const at = path6.join(baselineDir(), name);
46808
+ if (!fs6.statSync(at, { throwIfNoEntry: false })?.isDirectory()) {
46809
+ if (!fresh(at))
46810
+ fs6.rmSync(at, { force: true });
46811
+ } else {
46812
+ const files = fs6.readdirSync(at);
46813
+ if (files.length && !files.some((f) => fresh(path6.join(at, f))))
46814
+ fs6.rmSync(at, { recursive: true, force: true });
46815
+ }
46816
+ }
46817
+ }
46818
+ function attempt(fn) {
46819
+ try {
46820
+ return fn();
46821
+ } catch {
46822
+ return;
46752
46823
  }
46753
46824
  }
46754
46825
  function onHook(host, input2) {
@@ -46762,17 +46833,17 @@ function onHook(host, input2) {
46762
46833
  fs6.appendFileSync(file2, `export SPHICA_PARENT_SESSION=${input2.session_id}
46763
46834
  `);
46764
46835
  }
46765
- const place2 = identify(input2.cwd ?? process.cwd());
46766
- const now = place2 && snapshot(place2.root);
46767
- if (now)
46768
- writeBaseline(baselineFile(host, String(input2.session_id)), { ...now, running: false });
46769
- pruneBaselines();
46836
+ attempt(pruneBaselines);
46770
46837
  return { flush: false, notice: captureNotice() };
46771
46838
  }
46772
46839
  if (!owner())
46773
46840
  return { flush: false };
46774
- if (event === "Interrupt")
46841
+ if (event === "Interrupt") {
46842
+ const turn2 = input2.prompt_id ?? input2.turn_id;
46843
+ if (turn2)
46844
+ attempt(() => stopTurn(turnDir(host, String(input2.session_id)), turn2));
46775
46845
  return { flush: true };
46846
+ }
46776
46847
  const place = identify(input2.cwd ?? process.cwd());
46777
46848
  if (!place)
46778
46849
  return { flush: false };
@@ -46796,30 +46867,26 @@ function onHook(host, input2) {
46796
46867
  const id = `${key}:${digest(kept.body)}`;
46797
46868
  spool({ ...base, at: when, kind: "message", id, speaker, ...kept });
46798
46869
  };
46799
- const baseline = baselineFile(host, base.session);
46800
- if (event === "UserPromptSubmit") {
46801
- if (!readBaseline(baseline)?.running) {
46802
- const now = snapshot(place.root);
46803
- if (now)
46804
- writeBaseline(baseline, { ...now, running: true });
46805
- }
46806
- }
46870
+ const dir = turnDir(host, base.session);
46807
46871
  if (event === "UserPromptSubmit" && input2.prompt) {
46808
46872
  const prompt = input2.prompt.trimStart();
46809
46873
  if (!INJECTED.some((r) => r.test(prompt)))
46810
46874
  say(`${turn}:owner`, "owner", prompt);
46811
46875
  }
46876
+ if (event === "UserPromptSubmit")
46877
+ attempt(() => openTurn(dir, turn, place.root)?.());
46812
46878
  if (event === "Stop") {
46813
46879
  if (input2.last_assistant_message)
46814
46880
  say(`${turn}:assistant`, "assistant", input2.last_assistant_message);
46815
- const now = snapshot(place.root);
46816
- if (now) {
46817
- const before = readBaseline(baseline);
46818
- if (before)
46819
- for (const p of changed(place.root, before, now))
46820
- spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
46821
- writeBaseline(baseline, { ...now, running: false });
46822
- }
46881
+ const paths = attempt(() => {
46882
+ const finish = closeTurn(dir, turn, place.root);
46883
+ if (finish)
46884
+ return finish();
46885
+ stopTurn(dir, turn);
46886
+ return [];
46887
+ });
46888
+ for (const p of paths ?? [])
46889
+ spool({ ...base, kind: "edit", event: null, path: p, via: "status" });
46823
46890
  return { flush: true };
46824
46891
  }
46825
46892
  if (event === "PostToolUse") {
@@ -46879,7 +46946,7 @@ function readState() {
46879
46946
  function lock() {
46880
46947
  const file2 = path6.join(spoolDir(), ".lock");
46881
46948
  fs6.mkdirSync(spoolDir(), { recursive: true, mode: 448 });
46882
- for (let attempt = 0;attempt < 2; attempt++) {
46949
+ for (let attempt2 = 0;attempt2 < 2; attempt2++) {
46883
46950
  try {
46884
46951
  fs6.writeFileSync(file2, String(process.pid), { flag: "wx", mode: 384 });
46885
46952
  return () => {
@@ -47810,7 +47877,6 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
47810
47877
  ];
47811
47878
  const others = new Map((linked.length ? await db.selectFrom("unit").select(["id", "key", "kind", "lifecycle"]).where("project_id", "=", target.projectId).where("key", "in", linked).execute() : []).map((u) => [u.key, u]));
47812
47879
  const holders = new Map((others.size ? await db.selectFrom("unit_link as l").innerJoin("unit as n", "n.id", "l.from_unit").select(["l.to_unit", "n.key", "n.lifecycle"]).where("l.kind", "=", "supersedes").where("l.to_unit", "in", [...others.values()].map((o) => o.id)).where("n.lifecycle", "<>", "withdrawn").where("n.extraction", "=", "supported").where("n.unsourced", "=", 0).execute() : []).map((h) => [h.to_unit, h]));
47813
- const maintainers = new Set(record3.units.some((u) => u.supersedes || u.conflicts.length) ? (await db.selectFrom("source").select("author_login").distinct().where("project_id", "=", target.projectId).where("author_association", "in", [...MAINTAINERS]).execute()).flatMap((r) => r.author_login ? [r.author_login] : []) : []);
47814
47880
  const units = [];
47815
47881
  const claimed = new Set;
47816
47882
  for (const [i, u] of record3.units.entries()) {
@@ -47866,7 +47932,7 @@ async function checkRecord(db, target, raw, facts = prepareRecord(target.root, r
47866
47932
  if (target.origin !== "trace" && (u.supersedes || u.conflicts.length)) {
47867
47933
  const trusted = [...u.evidence, ...u.adoption].some((q) => {
47868
47934
  const s = sources.get(Number(q.source.slice(1)));
47869
- return s && (s.kind === "session_message" || s.author_kind === "owner" || MAINTAINERS.has(s.author_association ?? "") || s.author_login !== null && maintainers.has(s.author_login));
47935
+ return s && (s.kind === "session_message" || s.author_kind === "owner" || MAINTAINERS.has(s.author_association ?? ""));
47870
47936
  });
47871
47937
  if (!trusted)
47872
47938
  errors3.push(`${key}: supersedes and conflicts from ${target.origin} need the owner's or a maintainer's words, or the owner's session`);
@@ -49488,7 +49554,7 @@ var reply = (t, isError = false) => ({
49488
49554
  ...isError ? { isError: true } : {}
49489
49555
  });
49490
49556
  async function projectOf(cwd, meta3) {
49491
- const workspace = process.env.CLAUDE_PROJECT_DIR || hostWorkspace(meta3);
49557
+ const workspace = hostWorkspace(meta3) || process.env.CLAUDE_PROJECT_DIR;
49492
49558
  if (!workspace)
49493
49559
  throw new Error("The host did not say which workspace this session is in, so nothing is written (update Claude Code or Codex)");
49494
49560
  const place = writePlace(workspace, cwd);
package/hooks/hooks.json CHANGED
@@ -49,7 +49,7 @@
49
49
  "args": [
50
50
  "${CLAUDE_PLUGIN_ROOT}/dist/capture.js"
51
51
  ],
52
- "async": true
52
+ "timeout": 30
53
53
  }
54
54
  ]
55
55
  },
@@ -90,7 +90,7 @@
90
90
  "args": [
91
91
  "${CLAUDE_PLUGIN_ROOT}/dist/capture.js"
92
92
  ],
93
- "async": true
93
+ "timeout": 30
94
94
  }
95
95
  ]
96
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.27",
3
+ "version": "0.6.29",
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",