@bunshin-cc/bunshin 0.4.1 → 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 +106 -16
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -879,6 +879,8 @@ function firstHeading(text2) {
879
879
  function applyTextOp(text2, op, legacyAnchor = "") {
880
880
  const anchor = "anchor" in op && op.anchor || legacyAnchor;
881
881
  switch (op.op) {
882
+ case "situation":
883
+ return { ok: true, text: text2 };
882
884
  case "rewrite":
883
885
  return { ok: true, text: op.text };
884
886
  case "create":
@@ -1744,6 +1746,11 @@ ${robot ? `- Values that belong to THIS robot or cell (calibration, pixel column
1744
1746
  - Code that should change (a hard-coded constant, a helper) \u2192 describe it as an append to the
1745
1747
  skill's section, stating the change; reconciliation turns it into a harness patch.` : `- Machine specifics and personal preferences \u2192 your own user layer.`}
1746
1748
  - Never include secrets, credentials or customer data.
1749
+ - A retrieval miss: if a document in the list above would have helped, but the agent did not use
1750
+ it (the user had to point at it, asked why it was not used, or the agent found it only late), add
1751
+ op "situation" to that document: the request as the user actually phrased it, made general (names
1752
+ and ticket ids replaced by what they stand for), e.g. "run me through an e2e test and what it
1753
+ does". Recall learns from it, so the next such request finds the document.
1747
1754
 
1748
1755
  ## Output
1749
1756
 
@@ -1755,7 +1762,8 @@ At most ${max} suggestions. Schema:
1755
1762
  | {"op": "create", "section": {"kind": "skill"|"procedure"|"spec"|"verification"|"rule"|"guardrail"|"reference",
1756
1763
  "title": string, "body": string, "when": string,
1757
1764
  "paths"?: string[], "entities"?: string[], "skills"?: string[]}}
1758
- | {"op": "param", "key": string, "value": string|number|boolean},
1765
+ | {"op": "param", "key": string, "value": string|number|boolean}
1766
+ | {"op": "situation", "text": string},
1759
1767
  "rationale": string, "urgency": "normal"|"urgent"}]}`;
1760
1768
  }
1761
1769
  function extractJson(text2) {
@@ -1797,6 +1805,10 @@ function parseOp(raw) {
1797
1805
  section: { kind, title, body, when: when || title, paths: strList(s.paths), entities: strList(s.entities).map((e) => e.toLowerCase()), skills: strList(s.skills) }
1798
1806
  };
1799
1807
  }
1808
+ case "situation": {
1809
+ const text2 = clean(o.text);
1810
+ return text2 ? { op: "situation", text: text2.slice(0, 500) } : null;
1811
+ }
1800
1812
  case "param": {
1801
1813
  const key = typeof o.key === "string" ? o.key.trim() : "";
1802
1814
  const v = o.value;
@@ -2375,6 +2387,17 @@ var init_store2 = __esm({
2375
2387
  this.writeSession(s);
2376
2388
  return s;
2377
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
+ }
2378
2401
  readCurrent() {
2379
2402
  return readJson(this.p("sessions", "current.json"), null);
2380
2403
  }
@@ -3599,6 +3622,8 @@ function summarize(op) {
3599
3622
  return clip(op.content === null ? `delete ${op.path}` : `file ${op.path}`);
3600
3623
  case "retire":
3601
3624
  return clip(`retire: ${op.reason}`);
3625
+ case "situation":
3626
+ return clip(`also applies to: ${op.text}`);
3602
3627
  }
3603
3628
  }
3604
3629
  function describeTarget(s, view) {
@@ -3776,8 +3801,8 @@ __export(sync_exports, {
3776
3801
  import fs9 from "node:fs";
3777
3802
  import path8 from "node:path";
3778
3803
  function episodePayload(e) {
3779
- const { orgId: _o, uploaded: _u, servedReported: _s, ...rest } = e;
3780
- 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;
3781
3806
  return rest;
3782
3807
  }
3783
3808
  function suggestionPayload(s) {
@@ -3817,15 +3842,18 @@ async function syncV2(store, opts = {}) {
3817
3842
  await call("POST", "/episodes", { episode: episodePayload(e) });
3818
3843
  const fresh = v2.readEpisode(e.id) ?? e;
3819
3844
  const next = { ...fresh, uploaded: JSON.stringify(episodePayload(fresh)) === JSON.stringify(episodePayload(e)) ? true : fresh.uploaded };
3820
- if (!fresh.servedReported) {
3821
- const served = e.sectionsServed.filter((id) => !isLocalId(id));
3822
- const loaded = e.sectionsLoaded.filter((id) => !isLocalId(id));
3823
- try {
3824
- if (served.length) await call("POST", "/recall/served", { sectionIds: served });
3825
- if (loaded.length) await call("POST", "/recall/served", { sectionIds: loaded, loaded: true });
3826
- next.servedReported = true;
3827
- } catch {
3828
- }
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 {
3829
3857
  }
3830
3858
  v2.writeEpisode(next);
3831
3859
  report.uploadedEpisodes++;
@@ -8016,6 +8044,10 @@ init_store2();
8016
8044
  init_view();
8017
8045
  var NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
8018
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;
8019
8051
  var MAX_PUSH = 3;
8020
8052
  var additional = (hookEventName, text2) => text2.trim() ? { hookSpecificOutput: { hookEventName, additionalContext: text2.trim() } } : {};
8021
8053
  function touch(v2, ctx, patch = () => {
@@ -8058,6 +8090,11 @@ async function onSessionStart(ctx) {
8058
8090
  v2.writeCurrent({ sessionId: id, cwd: ctx.cwd, at: now });
8059
8091
  const config = ctx.store.config();
8060
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
+ }
8061
8098
  const budget = Math.min(3e3, ctx.deadline - Date.now() - 400);
8062
8099
  if (budget > 100) {
8063
8100
  try {
@@ -8240,6 +8277,16 @@ function onStop(ctx) {
8240
8277
  const config = ctx.store.config();
8241
8278
  if (ctx.input.stop_hook_active) return {};
8242
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
+ }
8243
8290
  const check = learningCheck(v2, session, scan.facts.toolCalls, now);
8244
8291
  if (check) return check;
8245
8292
  if (!config.reflect.nudge) return {};
@@ -8271,11 +8318,11 @@ function learningCheck(v2, session, toolCalls, now) {
8271
8318
  const reason = doc.body.replace(/^\s*<!--\s*bunshin:[\s\S]*?-->\s*/, "").trim().replace(/\{delta\}/g, String(toolCalls - last));
8272
8319
  return { decision: "block", reason };
8273
8320
  }
8274
- function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new Date()) {
8321
+ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new Date(), opts = {}) {
8275
8322
  const v2 = new V2Store(store);
8276
8323
  const config = store.config();
8277
8324
  const view = localView(v2, config, session.cwd, { index: false });
8278
- const scan = scanTranscript(transcriptPath);
8325
+ const scan = opts.scan ?? scanTranscript(transcriptPath ?? session.transcriptPath);
8279
8326
  const outcome = v2.readOutcome(session.sessionId);
8280
8327
  const signals = { ...sessionSignals({ scan, startedAt: session.startedAt, lastActivityAt: session.lastActivityAt }), ...outcome ? { outcome: outcome.status } : {} };
8281
8328
  const gate = sessionGate(signals, { now: now.toISOString(), cacheTtlMs: config.reflect.cacheTtlMs });
@@ -8292,7 +8339,7 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
8292
8339
  harness: ["claude-code", "codex", "pi", "other"].includes(session.harness) ? session.harness : config.harness,
8293
8340
  task: session.task,
8294
8341
  startedAt: session.startedAt,
8295
- endedAt: now.toISOString(),
8342
+ endedAt: opts.endedAt ?? now.toISOString(),
8296
8343
  sectionsServed: session.served,
8297
8344
  sectionsLoaded: session.loaded,
8298
8345
  captured: session.captured,
@@ -8305,10 +8352,53 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
8305
8352
  ...snippets.length ? { snippets } : {},
8306
8353
  ...outcome ? { outcome } : {},
8307
8354
  ...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
8308
- uploaded: false
8355
+ uploaded: false,
8356
+ ...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
8357
+ ...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
8358
+ ...previous?.servedReported ? { servedReported: true } : {}
8309
8359
  };
8310
8360
  return { episode, gate, scan };
8311
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
+ }
8312
8402
  function recordTrialEvidence(v2, session, outcome) {
8313
8403
  if (!outcome || outcome.status === "unknown") return 0;
8314
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.1",
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": {