@bunshin-cc/bunshin 0.4.3 → 0.4.4

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 +203 -124
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1694,6 +1694,45 @@ function sessionGate(s, opts = {}) {
1694
1694
  if (!cacheWarm) return { reflect: false, worth, reason: `${reason}; cache cold \u2192 cloud`, signals };
1695
1695
  return { reflect: true, worth, reason, signals };
1696
1696
  }
1697
+ function earlierSection(earlier, digest2) {
1698
+ const where = digest2 ? "The digest below holds only the part of the session not yet reflected on." : earlier.from ? `Consider only what happened from this point of the conversation on: ${earlier.from}. Everything before it was already reflected on.` : "Consider only the most recent part of the conversation; the rest was already reflected on.";
1699
+ const lessons = earlier.lessons.length ? earlier.lessons.map((l) => `- ${l}`).join("\n") : "- (none were worth keeping)";
1700
+ return `
1701
+ ## Already reflected on
1702
+
1703
+ This session was reflected on before. ${where}
1704
+ Lessons already taken from it. Never propose these again, nor a rewording of them:
1705
+ ${lessons}
1706
+ `;
1707
+ }
1708
+ function summarizeOp(op) {
1709
+ const clip = (s) => {
1710
+ const one = s.replace(/\s+/g, " ").trim();
1711
+ return one.length > 100 ? `${one.slice(0, 97)}\u2026` : one;
1712
+ };
1713
+ switch (op.op) {
1714
+ case "append":
1715
+ return clip(op.condition ? `${op.condition}: ${op.text}` : op.text);
1716
+ case "replace":
1717
+ return clip(`${op.find} \u2192 ${op.text}`);
1718
+ case "create":
1719
+ return clip(op.section.title);
1720
+ case "param":
1721
+ return `${op.key} = ${String(op.value)}`;
1722
+ case "rewrite":
1723
+ return clip(`rewrite (${op.text.length} chars)`);
1724
+ case "file":
1725
+ return clip(op.content === null ? `delete ${op.path}` : `file ${op.path}`);
1726
+ case "retire":
1727
+ return clip(`retire: ${op.reason}`);
1728
+ case "situation":
1729
+ return clip(`also applies to: ${op.text}`);
1730
+ }
1731
+ }
1732
+ function lessonLine(s) {
1733
+ const where = s.target.sectionId ?? (s.op.op === "create" ? "new document" : s.target.layer ?? "library");
1734
+ return `${where}: ${summarizeOp(s.op)}`;
1735
+ }
1697
1736
  function buildReflectPrompt(input) {
1698
1737
  const max = input.maxSuggestions ?? 4;
1699
1738
  const index = input.index.length ? input.index.map((s) => {
@@ -1707,7 +1746,7 @@ function buildReflectPrompt(input) {
1707
1746
 
1708
1747
  You are a reflection pass over the work in this conversation. Do not continue the task and do not
1709
1748
  call tools. Your only output is one JSON object matching the schema at the end.
1710
- ${input.transcriptDigest ? `
1749
+ ${input.earlier ? earlierSection(input.earlier, !!input.transcriptDigest) : ""}${input.transcriptDigest ? `
1711
1750
  ## Session transcript (digest)
1712
1751
 
1713
1752
  ${input.transcriptDigest}
@@ -2830,7 +2869,7 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
2830
2869
  } catch {
2831
2870
  continue;
2832
2871
  }
2833
- if (!(born >= since) && !only) continue;
2872
+ if (!(born >= since - BORN_SLACK_MS) && !only) continue;
2834
2873
  const files = {};
2835
2874
  for (const rel of listFiles(dir)) {
2836
2875
  if (rel === "SKILL.md" || rel === MANIFEST) continue;
@@ -3101,7 +3140,7 @@ function materialize(sections, dir) {
3101
3140
  }
3102
3141
  return { files };
3103
3142
  }
3104
- var settingsFile, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, LAYER_RANK, docMarker;
3143
+ var settingsFile, sha, BLOCK_BEGIN, BLOCK_END, MANIFEST, MAX_FILE, isExecutable, LAYER_RANK, docMarker, BORN_SLACK_MS;
3105
3144
  var init_deploy = __esm({
3106
3145
  "src/v2/deploy.ts"() {
3107
3146
  "use strict";
@@ -3123,6 +3162,7 @@ var init_deploy = __esm({
3123
3162
  };
3124
3163
  LAYER_RANK = { org: 0, dept: 1, team: 2, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
3125
3164
  docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
3165
+ BORN_SLACK_MS = 2e3;
3126
3166
  }
3127
3167
  });
3128
3168
 
@@ -3603,28 +3643,7 @@ function appendedText(op) {
3603
3643
  return op.condition ? `- **${op.condition}:** ${op.text.trim()}` : op.text.trim();
3604
3644
  }
3605
3645
  function summarize(op) {
3606
- const clip = (s) => {
3607
- const one = s.replace(/\s+/g, " ").trim();
3608
- return one.length > 100 ? `${one.slice(0, 97)}\u2026` : one;
3609
- };
3610
- switch (op.op) {
3611
- case "append":
3612
- return clip(op.condition ? `${op.condition}: ${op.text}` : op.text);
3613
- case "replace":
3614
- return clip(`${op.find} \u2192 ${op.text}`);
3615
- case "create":
3616
- return clip(op.section.title);
3617
- case "param":
3618
- return `${op.key} = ${String(op.value)}`;
3619
- case "rewrite":
3620
- return clip(`rewrite (${op.text.length} chars)`);
3621
- case "file":
3622
- return clip(op.content === null ? `delete ${op.path}` : `file ${op.path}`);
3623
- case "retire":
3624
- return clip(`retire: ${op.reason}`);
3625
- case "situation":
3626
- return clip(`also applies to: ${op.text}`);
3627
- }
3646
+ return summarizeOp(op);
3628
3647
  }
3629
3648
  function describeTarget(s, view) {
3630
3649
  if (s.op.op === "param") return view.ctx.kind === "robot" ? "params.json" : "private note";
@@ -3790,6 +3809,112 @@ var init_apply = __esm({
3790
3809
  }
3791
3810
  });
3792
3811
 
3812
+ // src/v2/signals.ts
3813
+ import fs9 from "node:fs";
3814
+ function isCorrection(text2) {
3815
+ const t = text2.trim().slice(0, 600);
3816
+ return !!t && CORRECTION_RE.test(t);
3817
+ }
3818
+ function codeOf(input) {
3819
+ let v = input;
3820
+ if (typeof v === "string") {
3821
+ try {
3822
+ v = JSON.parse(v);
3823
+ } catch {
3824
+ return null;
3825
+ }
3826
+ }
3827
+ if (v && typeof v === "object" && typeof v.code === "string") return v.code;
3828
+ return null;
3829
+ }
3830
+ function scanTranscript(file) {
3831
+ const empty = { facts: { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 }, snippets: [] };
3832
+ if (!file) return empty;
3833
+ let raw;
3834
+ try {
3835
+ raw = fs9.readFileSync(file, "utf8");
3836
+ } catch {
3837
+ return empty;
3838
+ }
3839
+ const out = { facts: parseTranscript(file), snippets: [] };
3840
+ for (const text2 of raw.split("\n")) {
3841
+ if (!text2.trim()) continue;
3842
+ let line;
3843
+ try {
3844
+ line = JSON.parse(text2);
3845
+ } catch {
3846
+ continue;
3847
+ }
3848
+ const at = typeof line.timestamp === "string" && !Number.isNaN(Date.parse(line.timestamp)) ? line.timestamp : void 0;
3849
+ if (at && !out.firstAt) out.firstAt = at;
3850
+ const isModel = line.type === "assistant" || line.type === "response_item" && (line.payload?.type === "message" || line.payload?.type === "function_call");
3851
+ if (at && isModel) out.lastModelAt = at;
3852
+ if (line.type === "assistant" && Array.isArray(line.message?.content)) {
3853
+ for (const b of line.message.content) {
3854
+ if (b?.type === "tool_use") {
3855
+ const code2 = codeOf(b.input);
3856
+ if (code2) out.snippets.push(code2);
3857
+ }
3858
+ }
3859
+ } else if (line.type === "response_item" && (line.payload?.type === "function_call" || line.payload?.type === "custom_tool_call")) {
3860
+ const code2 = codeOf(line.payload.input ?? line.payload.arguments);
3861
+ if (code2) out.snippets.push(code2);
3862
+ }
3863
+ }
3864
+ return out;
3865
+ }
3866
+ function sinceTurn(scan, from) {
3867
+ if (from <= 0) return scan;
3868
+ const turns = scan.facts.turns.slice(from);
3869
+ return {
3870
+ ...scan,
3871
+ facts: {
3872
+ turns,
3873
+ userMessages: turns.filter((t) => t.role === "user").length,
3874
+ toolCalls: turns.filter((t) => t.role === "tool" && t.tool !== "result").length,
3875
+ toolFailures: turns.filter((t) => t.role === "tool" && t.tool === "result" && t.isError).length
3876
+ }
3877
+ };
3878
+ }
3879
+ function reflectCursor(session, turns) {
3880
+ const at = Math.max(session.reflectedTurns ?? 0, session.serverReflectedTurns ?? 0);
3881
+ return at > turns ? 0 : at;
3882
+ }
3883
+ function sessionSignals({ scan, startedAt, lastActivityAt, partial }) {
3884
+ const users = scan.facts.turns.filter((t) => t.role === "user");
3885
+ const corrections = users.slice(partial ? 0 : 1).filter((t) => isCorrection(t.text)).length;
3886
+ const last = [scan.lastModelAt, lastActivityAt].filter((x) => !!x).sort().at(-1) ?? lastActivityAt;
3887
+ const first = [scan.firstAt, startedAt].filter((x) => !!x).sort()[0] ?? startedAt;
3888
+ const minutes = Math.max(0, (Date.parse(last) - Date.parse(first)) / 6e4);
3889
+ return {
3890
+ toolCalls: scan.facts.toolCalls,
3891
+ errors: scan.facts.toolFailures,
3892
+ corrections,
3893
+ userTurns: scan.facts.userMessages,
3894
+ minutes: Number.isFinite(minutes) ? Math.round(minutes * 10) / 10 : 0,
3895
+ lastActivityAt: last
3896
+ };
3897
+ }
3898
+ function errorStreakOvercome(facts, cursor = 0) {
3899
+ const results = facts.turns.filter((t) => t.role === "tool" && t.tool === "result" && !t.pending);
3900
+ const fresh = results.slice(Math.min(cursor, results.length));
3901
+ let failures = 0;
3902
+ let nudge = false;
3903
+ for (const r of fresh) {
3904
+ if (r.isError) failures++;
3905
+ else if (failures >= 3) nudge = true;
3906
+ }
3907
+ return { nudge, cursor: nudge ? results.length : Math.min(cursor, results.length) };
3908
+ }
3909
+ var CORRECTION_RE;
3910
+ var init_signals = __esm({
3911
+ "src/v2/signals.ts"() {
3912
+ "use strict";
3913
+ init_transcript();
3914
+ CORRECTION_RE = /^(?:no\b|nope\b|wrong\b|stop\b|wait\b|not quite\b)|\b(?:that'?s (?:not|wrong|incorrect)|that is (?:not|wrong)|not what i|i (?:said|meant|asked for)|you (?:should(?:n'?t)?|need to|forgot|missed|broke)|(?:please )?don'?t|do not|instead of|use \S+ instead|actually,|revert (?:that|this|it)|undo (?:that|this|it)|we (?:always|never|don'?t|do not))\b/i;
3915
+ }
3916
+ });
3917
+
3793
3918
  // src/v2/sync.ts
3794
3919
  var sync_exports = {};
3795
3920
  __export(sync_exports, {
@@ -3798,7 +3923,7 @@ __export(sync_exports, {
3798
3923
  syncV2: () => syncV2,
3799
3924
  uploadIndex: () => uploadIndex
3800
3925
  });
3801
- import fs9 from "node:fs";
3926
+ import fs10 from "node:fs";
3802
3927
  import path8 from "node:path";
3803
3928
  function episodePayload(e) {
3804
3929
  const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, ...rest } = e;
@@ -3839,7 +3964,13 @@ async function syncV2(store, opts = {}) {
3839
3964
  for (const e of v2.listEpisodes().filter((x) => !x.uploaded)) {
3840
3965
  if (Date.now() >= deadline) break;
3841
3966
  try {
3842
- await call("POST", "/episodes", { episode: episodePayload(e) });
3967
+ const res = await call("POST", "/episodes", { episode: episodePayload(e) });
3968
+ if (res && (typeof res.reflectedTurns === "number" || Array.isArray(res.lessons)) && v2.readSession(e.sessionId)) {
3969
+ v2.updateSession(e.sessionId, "", (s) => {
3970
+ if (typeof res.reflectedTurns === "number") s.serverReflectedTurns = Math.max(s.serverReflectedTurns ?? 0, res.reflectedTurns);
3971
+ if (Array.isArray(res.lessons)) s.serverLessons = res.lessons.filter((l) => typeof l === "string").slice(0, 40);
3972
+ });
3973
+ }
3843
3974
  const fresh = v2.readEpisode(e.id) ?? e;
3844
3975
  const next = { ...fresh, uploaded: JSON.stringify(episodePayload(fresh)) === JSON.stringify(episodePayload(e)) ? true : fresh.uploaded };
3845
3976
  const legacy = fresh.servedReported && !fresh.reportedServed;
@@ -3948,7 +4079,7 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
3948
4079
  const { root, index } = indexWorkingTree(v2, config, cwd, { force: true });
3949
4080
  const docs = (index.paths ?? Object.keys(index.files)).flatMap((rel) => {
3950
4081
  try {
3951
- return [{ path: rel, text: fs9.readFileSync(path8.join(root, rel), "utf8") }];
4082
+ return [{ path: rel, text: fs10.readFileSync(path8.join(root, rel), "utf8") }];
3952
4083
  } catch {
3953
4084
  return [];
3954
4085
  }
@@ -3974,7 +4105,7 @@ var init_sync = __esm({
3974
4105
  // src/v2/reflect.ts
3975
4106
  import { spawn as spawn3 } from "node:child_process";
3976
4107
  import { createHash as createHash2 } from "node:crypto";
3977
- import fs10 from "node:fs";
4108
+ import fs11 from "node:fs";
3978
4109
  function episodeIdFor(contextId, sessionId) {
3979
4110
  return `e_${createHash2("sha256").update(`${contextId}
3980
4111
  ${sessionId}`).digest("hex").slice(0, 16)}`;
@@ -3994,7 +4125,7 @@ function runClaudeFork(sessionId, prompt, { cwd, timeoutMs = FORK_TIMEOUT_MS } =
3994
4125
  };
3995
4126
  let child;
3996
4127
  try {
3997
- child = spawn3(bin, args, { cwd: cwd && fs10.existsSync(cwd) ? cwd : void 0, env: childEnv(), stdio: ["ignore", "pipe", "pipe"] });
4128
+ child = spawn3(bin, args, { cwd: cwd && fs11.existsSync(cwd) ? cwd : void 0, env: childEnv(), stdio: ["ignore", "pipe", "pipe"] });
3998
4129
  } catch (e) {
3999
4130
  resolve({ ok: false, error: e instanceof Error ? e.message : String(e) });
4000
4131
  return;
@@ -4039,13 +4170,28 @@ function reflectionIndex(view, ids, task, focus, humanTurns = []) {
4039
4170
  return [...chosen.values()].slice(0, 30).map(({ id, kind, layer, title, when, outline }) => ({ id, kind, layer, title, when, ...outline?.length ? { outline } : {} }));
4040
4171
  }
4041
4172
  function humanTurnsOf(transcriptPath) {
4042
- if (!transcriptPath || !fs10.existsSync(transcriptPath)) return [];
4173
+ if (!transcriptPath || !fs11.existsSync(transcriptPath)) return [];
4043
4174
  try {
4044
4175
  return parseTranscript(transcriptPath).turns.filter((t) => t.role === "user" && t.text.trim()).map((t) => t.text.slice(0, 1e3));
4045
4176
  } catch {
4046
4177
  return [];
4047
4178
  }
4048
4179
  }
4180
+ function readFacts(transcriptPath) {
4181
+ if (!transcriptPath || !fs11.existsSync(transcriptPath)) return null;
4182
+ try {
4183
+ return parseTranscript(transcriptPath);
4184
+ } catch {
4185
+ return null;
4186
+ }
4187
+ }
4188
+ function anchorAt(facts, from) {
4189
+ const quote = (t) => `"${t.replace(/\s+/g, " ").trim().slice(0, 160)}"`;
4190
+ const user = facts.turns.slice(from).find((t) => t.role === "user" && t.text.trim());
4191
+ if (user) return `the user message that begins ${quote(user.text)}`;
4192
+ const reply = facts.turns.slice(0, from).reverse().find((t) => t.role === "assistant" && t.text.trim());
4193
+ return reply ? `just after your reply that begins ${quote(reply.text)}` : void 0;
4194
+ }
4049
4195
  async function runReflectionV2(store, opts) {
4050
4196
  const v2 = new V2Store(store);
4051
4197
  const config = store.config();
@@ -4066,7 +4212,12 @@ async function runReflectionV2(store, opts) {
4066
4212
  const captured = [.../* @__PURE__ */ new Set([...session.captured, ...prior.map((s) => s.target.sectionId ?? localSectionId(s.id))])];
4067
4213
  const index = reflectionIndex(view, [...session.loaded, ...session.pushed, ...session.served], session.task, opts.focus, humanTurnsOf(session.transcriptPath));
4068
4214
  const harness = session.harness ?? config.harness;
4069
- const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, ...opts.focus ? { focus: opts.focus } : {} };
4215
+ const facts = readFacts(session.transcriptPath);
4216
+ const turns = facts?.turns.length ?? 0;
4217
+ const from = reflectCursor(session, turns);
4218
+ const lessons = [.../* @__PURE__ */ new Set([...prior.map(lessonLine), ...session.serverLessons ?? []])].slice(0, 40);
4219
+ const earlier = from > 0 || lessons.length ? { ...from > 0 && facts ? { from: anchorAt(facts, from) } : {}, lessons } : void 0;
4220
+ const promptBase = { contextKind: view.ctx.kind, layers: view.layers, index, captured, ...opts.focus ? { focus: opts.focus } : {}, ...earlier ? { earlier } : {} };
4070
4221
  let raw = null;
4071
4222
  if (harness === "claude-code") {
4072
4223
  const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd });
@@ -4084,8 +4235,8 @@ async function runReflectionV2(store, opts) {
4084
4235
  result.method = "fork";
4085
4236
  } else result.error = fork.error;
4086
4237
  }
4087
- if (result.method === "none" && session.transcriptPath && fs10.existsSync(session.transcriptPath)) {
4088
- const transcriptDigest = digest(parseTranscript(session.transcriptPath), { maxChars: 4e4 });
4238
+ if (result.method === "none" && session.transcriptPath && fs11.existsSync(session.transcriptPath)) {
4239
+ const transcriptDigest = digest(facts ?? parseTranscript(session.transcriptPath), { from, maxChars: 4e4 });
4089
4240
  const res = await askJson(config, {
4090
4241
  system: "You are Bunshin's reflection pass. Follow the instructions in the user message and reply with one JSON object only.",
4091
4242
  user: buildReflectPrompt({ ...promptBase, transcriptDigest }),
@@ -4121,7 +4272,10 @@ async function runReflectionV2(store, opts) {
4121
4272
  const ids = result.suggestions.map((s) => s.id);
4122
4273
  v2.updateSession(opts.sessionId, session.cwd, (s) => {
4123
4274
  addUnique(s.suggestionIds, ...ids);
4124
- if (result.method !== "none") s.reflectedAt = createdAt;
4275
+ if (result.method !== "none") {
4276
+ s.reflectedAt = createdAt;
4277
+ s.reflectedTurns = Math.max(s.reflectedTurns ?? 0, turns);
4278
+ }
4125
4279
  if (origin === "learn") {
4126
4280
  for (const sug of result.suggestions) if (sug.status !== "proposed") addUnique(s.captured, sug.target.sectionId ?? localSectionId(sug.id));
4127
4281
  }
@@ -4134,6 +4288,7 @@ async function runReflectionV2(store, opts) {
4134
4288
  suggestionIds: [.../* @__PURE__ */ new Set([...episode.suggestionIds, ...ids])],
4135
4289
  captured: fresh?.captured ?? episode.captured,
4136
4290
  reflected: result.method !== "none" ? "edge" : episode.reflected === "edge" ? "none" : episode.reflected,
4291
+ ...result.method !== "none" ? { reflectedTurns: Math.max(episode.reflectedTurns ?? 0, turns), turns: Math.max(episode.turns ?? 0, turns) } : {},
4137
4292
  uploaded: false
4138
4293
  });
4139
4294
  }
@@ -4170,6 +4325,7 @@ var init_reflect2 = __esm({
4170
4325
  init_api();
4171
4326
  init_apply();
4172
4327
  init_store2();
4328
+ init_signals();
4173
4329
  init_view();
4174
4330
  FORK_TIMEOUT_MS = 12e4;
4175
4331
  }
@@ -7955,91 +8111,7 @@ function refreshInBackground(v2, store, detach2, cwd, now = Date.now()) {
7955
8111
  // src/v2/hooks.ts
7956
8112
  init_reflect2();
7957
8113
  init_repo();
7958
-
7959
- // src/v2/signals.ts
7960
- init_transcript();
7961
- import fs11 from "node:fs";
7962
- var CORRECTION_RE = /^(?:no\b|nope\b|wrong\b|stop\b|wait\b|not quite\b)|\b(?:that'?s (?:not|wrong|incorrect)|that is (?:not|wrong)|not what i|i (?:said|meant|asked for)|you (?:should(?:n'?t)?|need to|forgot|missed|broke)|(?:please )?don'?t|do not|instead of|use \S+ instead|actually,|revert (?:that|this|it)|undo (?:that|this|it)|we (?:always|never|don'?t|do not))\b/i;
7963
- function isCorrection(text2) {
7964
- const t = text2.trim().slice(0, 600);
7965
- return !!t && CORRECTION_RE.test(t);
7966
- }
7967
- function codeOf(input) {
7968
- let v = input;
7969
- if (typeof v === "string") {
7970
- try {
7971
- v = JSON.parse(v);
7972
- } catch {
7973
- return null;
7974
- }
7975
- }
7976
- if (v && typeof v === "object" && typeof v.code === "string") return v.code;
7977
- return null;
7978
- }
7979
- function scanTranscript(file) {
7980
- const empty = { facts: { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 }, snippets: [] };
7981
- if (!file) return empty;
7982
- let raw;
7983
- try {
7984
- raw = fs11.readFileSync(file, "utf8");
7985
- } catch {
7986
- return empty;
7987
- }
7988
- const out = { facts: parseTranscript(file), snippets: [] };
7989
- for (const text2 of raw.split("\n")) {
7990
- if (!text2.trim()) continue;
7991
- let line;
7992
- try {
7993
- line = JSON.parse(text2);
7994
- } catch {
7995
- continue;
7996
- }
7997
- const at = typeof line.timestamp === "string" && !Number.isNaN(Date.parse(line.timestamp)) ? line.timestamp : void 0;
7998
- if (at && !out.firstAt) out.firstAt = at;
7999
- const isModel = line.type === "assistant" || line.type === "response_item" && (line.payload?.type === "message" || line.payload?.type === "function_call");
8000
- if (at && isModel) out.lastModelAt = at;
8001
- if (line.type === "assistant" && Array.isArray(line.message?.content)) {
8002
- for (const b of line.message.content) {
8003
- if (b?.type === "tool_use") {
8004
- const code2 = codeOf(b.input);
8005
- if (code2) out.snippets.push(code2);
8006
- }
8007
- }
8008
- } else if (line.type === "response_item" && (line.payload?.type === "function_call" || line.payload?.type === "custom_tool_call")) {
8009
- const code2 = codeOf(line.payload.input ?? line.payload.arguments);
8010
- if (code2) out.snippets.push(code2);
8011
- }
8012
- }
8013
- return out;
8014
- }
8015
- function sessionSignals({ scan, startedAt, lastActivityAt }) {
8016
- const users = scan.facts.turns.filter((t) => t.role === "user");
8017
- const corrections = users.slice(1).filter((t) => isCorrection(t.text)).length;
8018
- const last = [scan.lastModelAt, lastActivityAt].filter((x) => !!x).sort().at(-1) ?? lastActivityAt;
8019
- const first = [scan.firstAt, startedAt].filter((x) => !!x).sort()[0] ?? startedAt;
8020
- const minutes = Math.max(0, (Date.parse(last) - Date.parse(first)) / 6e4);
8021
- return {
8022
- toolCalls: scan.facts.toolCalls,
8023
- errors: scan.facts.toolFailures,
8024
- corrections,
8025
- userTurns: scan.facts.userMessages,
8026
- minutes: Number.isFinite(minutes) ? Math.round(minutes * 10) / 10 : 0,
8027
- lastActivityAt: last
8028
- };
8029
- }
8030
- function errorStreakOvercome(facts, cursor = 0) {
8031
- const results = facts.turns.filter((t) => t.role === "tool" && t.tool === "result" && !t.pending);
8032
- const fresh = results.slice(Math.min(cursor, results.length));
8033
- let failures = 0;
8034
- let nudge = false;
8035
- for (const r of fresh) {
8036
- if (r.isError) failures++;
8037
- else if (failures >= 3) nudge = true;
8038
- }
8039
- return { nudge, cursor: nudge ? results.length : Math.min(cursor, results.length) };
8040
- }
8041
-
8042
- // src/v2/hooks.ts
8114
+ init_signals();
8043
8115
  init_store2();
8044
8116
  init_view();
8045
8117
  var NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
@@ -8324,7 +8396,10 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
8324
8396
  const view = localView(v2, config, session.cwd, { index: false });
8325
8397
  const scan = opts.scan ?? scanTranscript(transcriptPath ?? session.transcriptPath);
8326
8398
  const outcome = v2.readOutcome(session.sessionId);
8327
- const signals = { ...sessionSignals({ scan, startedAt: session.startedAt, lastActivityAt: session.lastActivityAt }), ...outcome ? { outcome: outcome.status } : {} };
8399
+ const turns = scan.facts.turns.length;
8400
+ const from = reflectCursor(session, turns);
8401
+ const fresh = sinceTurn(scan, from);
8402
+ const signals = { ...sessionSignals({ scan: fresh, startedAt: session.startedAt, lastActivityAt: session.lastActivityAt, partial: from > 0 }), ...outcome ? { outcome: outcome.status } : {} };
8328
8403
  const gate = sessionGate(signals, { now: now.toISOString(), cacheTtlMs: config.reflect.cacheTtlMs });
8329
8404
  const id = episodeIdFor(view.ctx.id, session.sessionId);
8330
8405
  const previous = v2.readEpisode(id);
@@ -8344,10 +8419,13 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
8344
8419
  sectionsLoaded: session.loaded,
8345
8420
  captured: session.captured,
8346
8421
  suggestionIds: [.../* @__PURE__ */ new Set([...previous?.suggestionIds ?? [], ...session.suggestionIds])],
8347
- reflected: previous?.reflected === "edge" ? "edge" : "none",
8422
+ // "edge" only while this machine's reflections cover every turn; new work is open again.
8423
+ reflected: previous?.reflected === "edge" && from >= turns ? "edge" : "none",
8348
8424
  gate,
8349
8425
  upload,
8350
- ...upload !== "header-only" && scan.facts.turns.length ? { transcript: digest(scan.facts, { maxChars: upload === "full" ? 2e5 : 6e4 }) } : {},
8426
+ turns,
8427
+ reflectedTurns: from,
8428
+ ...upload !== "header-only" && turns > from ? { transcript: digest(scan.facts, { from, maxChars: upload === "full" ? 2e5 : 6e4 }), transcriptFrom: from } : {},
8351
8429
  ...calls.length ? { calls } : {},
8352
8430
  ...snippets.length ? { snippets } : {},
8353
8431
  ...outcome ? { outcome } : {},
@@ -8450,7 +8528,8 @@ function onPreCompact(ctx) {
8450
8528
  const session = touch(v2, ctx);
8451
8529
  if (!config.reflect.enabled) return {};
8452
8530
  const scan = scanTranscript(ctx.input.transcript_path ?? session.transcriptPath);
8453
- const gate = sessionGate(sessionSignals({ scan, startedAt: session.startedAt, lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() }), { cacheTtlMs: config.reflect.cacheTtlMs });
8531
+ const from = reflectCursor(session, scan.facts.turns.length);
8532
+ const gate = sessionGate(sessionSignals({ scan: sinceTurn(scan, from), startedAt: session.startedAt, lastActivityAt: (/* @__PURE__ */ new Date()).toISOString(), partial: from > 0 }), { cacheTtlMs: config.reflect.cacheTtlMs });
8454
8533
  if (gate.worth) ctx.detach(reflectArgs(ctx, session.sessionId), { cwd: session.cwd || ctx.cwd });
8455
8534
  v2.log("v2-pre-compact", { session: session.sessionId, gate: gate.reason, fork: gate.worth });
8456
8535
  return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunshin-cc/bunshin",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Shared memory for company AI agents. Connect Codex, Claude Code, or Pi to Bunshin.",
5
5
  "type": "module",
6
6
  "bin": {