@bunshin-cc/bunshin 0.4.2 → 0.4.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/bin.js +91 -15
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -2387,6 +2387,17 @@ var init_store2 = __esm({
2387
2387
  this.writeSession(s);
2388
2388
  return s;
2389
2389
  }
2390
+ listSessions() {
2391
+ const dir = this.p("sessions");
2392
+ if (!fileExists(dir)) return [];
2393
+ const out = [];
2394
+ for (const f of fs4.readdirSync(dir)) {
2395
+ if (!f.endsWith(".json") || f === "current.json") continue;
2396
+ const raw = readJson(path3.join(dir, f), null);
2397
+ if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
2398
+ }
2399
+ return out;
2400
+ }
2390
2401
  readCurrent() {
2391
2402
  return readJson(this.p("sessions", "current.json"), null);
2392
2403
  }
@@ -3790,8 +3801,8 @@ __export(sync_exports, {
3790
3801
  import fs9 from "node:fs";
3791
3802
  import path8 from "node:path";
3792
3803
  function episodePayload(e) {
3793
- const { orgId: _o, uploaded: _u, servedReported: _s, ...rest } = e;
3794
- void _o, _u, _s;
3804
+ const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, ...rest } = e;
3805
+ void _o, _u, _s, _rs, _rl;
3795
3806
  return rest;
3796
3807
  }
3797
3808
  function suggestionPayload(s) {
@@ -3831,15 +3842,18 @@ async function syncV2(store, opts = {}) {
3831
3842
  await call("POST", "/episodes", { episode: episodePayload(e) });
3832
3843
  const fresh = v2.readEpisode(e.id) ?? e;
3833
3844
  const next = { ...fresh, uploaded: JSON.stringify(episodePayload(fresh)) === JSON.stringify(episodePayload(e)) ? true : fresh.uploaded };
3834
- if (!fresh.servedReported) {
3835
- const served = e.sectionsServed.filter((id) => !isLocalId(id));
3836
- const loaded = e.sectionsLoaded.filter((id) => !isLocalId(id));
3837
- try {
3838
- if (served.length) await call("POST", "/recall/served", { sectionIds: served });
3839
- if (loaded.length) await call("POST", "/recall/served", { sectionIds: loaded, loaded: true });
3840
- next.servedReported = true;
3841
- } catch {
3842
- }
3845
+ const legacy = fresh.servedReported && !fresh.reportedServed;
3846
+ const doneServed = new Set(legacy ? e.sectionsServed : fresh.reportedServed ?? []);
3847
+ const doneLoaded = new Set(legacy ? e.sectionsLoaded : fresh.reportedLoaded ?? []);
3848
+ const served = e.sectionsServed.filter((id) => !isLocalId(id) && !doneServed.has(id));
3849
+ const loaded = e.sectionsLoaded.filter((id) => !isLocalId(id) && !doneLoaded.has(id));
3850
+ try {
3851
+ if (served.length) await call("POST", "/recall/served", { sectionIds: served });
3852
+ if (loaded.length) await call("POST", "/recall/served", { sectionIds: loaded, loaded: true });
3853
+ next.reportedServed = [...doneServed, ...served];
3854
+ next.reportedLoaded = [...doneLoaded, ...loaded];
3855
+ delete next.servedReported;
3856
+ } catch {
3843
3857
  }
3844
3858
  v2.writeEpisode(next);
3845
3859
  report.uploadedEpisodes++;
@@ -8030,6 +8044,10 @@ init_store2();
8030
8044
  init_view();
8031
8045
  var NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
8032
8046
  var NUDGE_INTERVAL_MS = 60 * 6e4;
8047
+ var CHECKPOINT_MS = 30 * 6e4;
8048
+ var LEFTOVER_IDLE_MS = 60 * 6e4;
8049
+ var LEFTOVER_MAX_AGE_MS = 14 * 24 * 60 * 6e4;
8050
+ var LEFTOVERS_PER_START = 5;
8033
8051
  var MAX_PUSH = 3;
8034
8052
  var additional = (hookEventName, text2) => text2.trim() ? { hookSpecificOutput: { hookEventName, additionalContext: text2.trim() } } : {};
8035
8053
  function touch(v2, ctx, patch = () => {
@@ -8072,6 +8090,11 @@ async function onSessionStart(ctx) {
8072
8090
  v2.writeCurrent({ sessionId: id, cwd: ctx.cwd, at: now });
8073
8091
  const config = ctx.store.config();
8074
8092
  if (canCall(config)) {
8093
+ try {
8094
+ const swept = checkpointLeftovers(ctx.store, id);
8095
+ if (swept.length) v2.log("v2-leftovers", { session: id, episodes: swept });
8096
+ } catch {
8097
+ }
8075
8098
  const budget = Math.min(3e3, ctx.deadline - Date.now() - 400);
8076
8099
  if (budget > 100) {
8077
8100
  try {
@@ -8254,6 +8277,16 @@ function onStop(ctx) {
8254
8277
  const config = ctx.store.config();
8255
8278
  if (ctx.input.stop_hook_active) return {};
8256
8279
  const scan = scanTranscript(ctx.input.transcript_path ?? session.transcriptPath);
8280
+ if (canCall(config) && checkpointDue(session, scan, now)) {
8281
+ try {
8282
+ const { episode } = buildEpisode(ctx.store, session, void 0, now, { scan });
8283
+ v2.writeEpisode(episode);
8284
+ markCheckpoint(v2, session, scan, now);
8285
+ ctx.detach(["--root", ctx.store.root, "sync", "--quiet"], { cwd: ctx.cwd });
8286
+ v2.log("v2-checkpoint", { session: session.sessionId, episode: episode.id });
8287
+ } catch {
8288
+ }
8289
+ }
8257
8290
  const check = learningCheck(v2, session, scan.facts.toolCalls, now);
8258
8291
  if (check) return check;
8259
8292
  if (!config.reflect.nudge) return {};
@@ -8285,11 +8318,11 @@ function learningCheck(v2, session, toolCalls, now) {
8285
8318
  const reason = doc.body.replace(/^\s*<!--\s*bunshin:[\s\S]*?-->\s*/, "").trim().replace(/\{delta\}/g, String(toolCalls - last));
8286
8319
  return { decision: "block", reason };
8287
8320
  }
8288
- function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new Date()) {
8321
+ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new Date(), opts = {}) {
8289
8322
  const v2 = new V2Store(store);
8290
8323
  const config = store.config();
8291
8324
  const view = localView(v2, config, session.cwd, { index: false });
8292
- const scan = scanTranscript(transcriptPath);
8325
+ const scan = opts.scan ?? scanTranscript(transcriptPath ?? session.transcriptPath);
8293
8326
  const outcome = v2.readOutcome(session.sessionId);
8294
8327
  const signals = { ...sessionSignals({ scan, startedAt: session.startedAt, lastActivityAt: session.lastActivityAt }), ...outcome ? { outcome: outcome.status } : {} };
8295
8328
  const gate = sessionGate(signals, { now: now.toISOString(), cacheTtlMs: config.reflect.cacheTtlMs });
@@ -8306,7 +8339,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
8306
8339
  harness: ["claude-code", "codex", "pi", "other"].includes(session.harness) ? session.harness : config.harness,
8307
8340
  task: session.task,
8308
8341
  startedAt: session.startedAt,
8309
- endedAt: now.toISOString(),
8342
+ endedAt: opts.endedAt ?? now.toISOString(),
8310
8343
  sectionsServed: session.served,
8311
8344
  sectionsLoaded: session.loaded,
8312
8345
  captured: session.captured,
@@ -8319,10 +8352,53 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
8319
8352
  ...snippets.length ? { snippets } : {},
8320
8353
  ...outcome ? { outcome } : {},
8321
8354
  ...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
8322
- uploaded: false
8355
+ uploaded: false,
8356
+ ...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
8357
+ ...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
8358
+ ...previous?.servedReported ? { servedReported: true } : {}
8323
8359
  };
8324
8360
  return { episode, gate, scan };
8325
8361
  }
8362
+ var progressOf = (scan) => scan.facts.toolCalls + scan.facts.turns.length;
8363
+ function checkpointDue(session, scan, now) {
8364
+ const since = Date.parse(session.checkpointAt ?? session.startedAt);
8365
+ if (!Number.isFinite(since) || now.getTime() - since < CHECKPOINT_MS) return false;
8366
+ return progressOf(scan) > (session.checkpointProgress ?? 0);
8367
+ }
8368
+ function markCheckpoint(v2, session, scan, now) {
8369
+ v2.updateSession(session.sessionId, session.cwd, (s) => {
8370
+ s.checkpointAt = now.toISOString();
8371
+ s.checkpointProgress = progressOf(scan);
8372
+ s.checkpointActivity = s.lastActivityAt;
8373
+ });
8374
+ }
8375
+ function checkpointLeftovers(store, currentSessionId, now = /* @__PURE__ */ new Date()) {
8376
+ const v2 = new V2Store(store);
8377
+ const out = [];
8378
+ const candidates = v2.listSessions().filter((s) => {
8379
+ if (s.sessionId === currentSessionId || !s.transcriptPath) return false;
8380
+ const idle = now.getTime() - Date.parse(s.lastActivityAt);
8381
+ if (!(idle >= LEFTOVER_IDLE_MS && idle <= LEFTOVER_MAX_AGE_MS)) return false;
8382
+ return s.checkpointActivity !== s.lastActivityAt;
8383
+ }).sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt));
8384
+ for (const s of candidates) {
8385
+ if (out.length >= LEFTOVERS_PER_START) break;
8386
+ const config = store.config();
8387
+ const id = episodeIdFor(localView(v2, config, s.cwd, { index: false }).ctx.id, s.sessionId);
8388
+ const prev = v2.readEpisode(id);
8389
+ if (prev && prev.endedAt >= s.lastActivityAt) {
8390
+ v2.updateSession(s.sessionId, s.cwd, (x) => {
8391
+ x.checkpointActivity = x.lastActivityAt;
8392
+ });
8393
+ continue;
8394
+ }
8395
+ const { episode, scan } = buildEpisode(store, s, void 0, now, { endedAt: s.lastActivityAt });
8396
+ v2.writeEpisode(episode);
8397
+ markCheckpoint(v2, s, scan, now);
8398
+ out.push(episode.id);
8399
+ }
8400
+ return out;
8401
+ }
8326
8402
  function recordTrialEvidence(v2, session, outcome) {
8327
8403
  if (!outcome || outcome.status === "unknown") return 0;
8328
8404
  const used = /* @__PURE__ */ new Set([...session.loaded, ...session.pushed]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {