@stitchdb/cli 0.7.2 → 0.7.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +32 -10
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -502,6 +502,10 @@ async function cmdHook(args) {
502
502
  // Stitch hook entries — never touching non-Stitch ones.
503
503
  if (eventName === 'SessionStart') {
504
504
  ensureHooksUpToDate();
505
+ // Catch-up distillation: if a previous session ended with undistilled
506
+ // turns (user exited under threshold), distill them now in the
507
+ // background so they surface in this session's context very soon.
508
+ maybeAutoDistill(threadName, { force: true }).catch(() => { });
505
509
  }
506
510
  // Strategy: prefer distilled memories (dense facts) over raw turns. Only
507
511
  // include raw turns for the last 5 to give the agent immediate continuation.
@@ -964,9 +968,13 @@ async function cmdLink(args) {
964
968
  // Triggered manually (`stitch distill`), and automatically by the Stop hook
965
969
  // when conditions are met (cooldown + new-turn threshold).
966
970
  const DISTILL_STATE_FILE = path.join(CONFIG_DIR, 'distill-state.json');
967
- const DISTILL_COOLDOWN_MS = 10 * 60 * 1000; // distill at most once per 10 min per thread
968
- const DISTILL_MIN_NEW_TURNS = 5; // need 5 new turns before bothering
971
+ // No time-based cooldown purely turn-count driven. The previous time gate
972
+ // could leave turns undistilled if the user exited Claude before the next
973
+ // firing window. Now: every Stop checks the pending-turn count, and if the
974
+ // user exits below threshold, the next SessionStart catches up.
975
+ const DISTILL_MIN_NEW_TURNS = 5; // distill once 5+ new turns are pending
969
976
  const DISTILL_BATCH_SIZE = 30; // turns per distillation pass
977
+ const DISTILL_DEBOUNCE_MS = 90 * 1000; // small in-flight guard so a flurry of Stops doesn't fire N spawns
970
978
  function loadDistillState() {
971
979
  try {
972
980
  return JSON.parse(fs.readFileSync(DISTILL_STATE_FILE, 'utf8'));
@@ -1203,14 +1211,24 @@ async function distillClear(args) {
1203
1211
  }
1204
1212
  console.log(`Cleared ${deleted} memories.`);
1205
1213
  }
1206
- // Triggered from the Stop hook (fire-and-forget, never blocks the user).
1207
- async function maybeAutoDistill(thread) {
1214
+ // Triggered from Stop hook AND SessionStart catch-up. Fire-and-forget.
1215
+ //
1216
+ // Decision logic:
1217
+ // - If `pending = currentTurns - lastDistilledTurns` >= DISTILL_MIN_NEW_TURNS,
1218
+ // spawn a distill pass. No time-based cooldown — leaving turns undistilled
1219
+ // because the user happened to exit before the next firing window is the
1220
+ // wrong behaviour.
1221
+ // - SessionStart calls this with `force = true` when ANY pending turns exist
1222
+ // so a session that ended mid-batch (e.g. 3 pending) still gets caught up.
1223
+ // - DISTILL_DEBOUNCE_MS is a tiny in-process guard so a flurry of Stop
1224
+ // events from a single agent run doesn't trigger N parallel claude -p
1225
+ // spawns; once one is in flight, others within the window become no-ops.
1226
+ async function maybeAutoDistill(thread, opts = {}) {
1208
1227
  const state = loadDistillState();
1209
1228
  const meta = state.threads[thread] || { lastDistilledAt: 0, lastTurnAt: 0, lastTurnCount: 0 };
1210
- // Cool-down: don't distill more than once per 30 min per thread.
1211
- if (Date.now() - meta.lastDistilledAt < DISTILL_COOLDOWN_MS)
1229
+ // In-flight debounce: if we just spawned one, skip.
1230
+ if (Date.now() - meta.lastDistilledAt < DISTILL_DEBOUNCE_MS)
1212
1231
  return;
1213
- // Need at least N new turns since last pass.
1214
1232
  const cfg = loadConfig();
1215
1233
  const stitch = client(cfg);
1216
1234
  let recallSize = 0;
@@ -1221,17 +1239,21 @@ async function maybeAutoDistill(thread) {
1221
1239
  catch {
1222
1240
  return;
1223
1241
  }
1224
- if (recallSize - meta.lastTurnCount < DISTILL_MIN_NEW_TURNS)
1242
+ const pending = recallSize - meta.lastTurnCount;
1243
+ // Default path: only distill once enough turns have piled up.
1244
+ // SessionStart catch-up: distill if there's ANY pending turn.
1245
+ const threshold = opts.force ? 1 : DISTILL_MIN_NEW_TURNS;
1246
+ if (pending < threshold)
1225
1247
  return;
1226
1248
  // Mark BEFORE running so we don't double-fire on overlapping Stop events.
1227
1249
  state.threads[thread] = { lastDistilledAt: Date.now(), lastTurnAt: Date.now(), lastTurnCount: recallSize };
1228
1250
  saveDistillState(state);
1229
- // Detach: spawn a background process so the Stop hook returns immediately.
1230
- // The detached child runs `stitch distill` for this thread.
1251
+ // Detach: spawn a background process so the hook returns immediately.
1231
1252
  try {
1232
1253
  const child = spawn(process.argv[0], [process.argv[1] || (await import('node:url')).fileURLToPath(import.meta.url), 'distill', '--thread', thread, '--n', String(DISTILL_BATCH_SIZE)], {
1233
1254
  detached: true,
1234
1255
  stdio: 'ignore',
1256
+ env: { ...process.env, STITCH_HOOKS_DISABLED: '1' },
1235
1257
  });
1236
1258
  child.unref();
1237
1259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stitchdb/cli",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Stitch CLI — manage memory + run agents from your terminal",
5
5
  "type": "module",
6
6
  "bin": {