@bunshin-cc/bunshin 0.4.2 → 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.
- package/dist/bin.js +294 -139
- 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}
|
|
@@ -2387,6 +2426,17 @@ var init_store2 = __esm({
|
|
|
2387
2426
|
this.writeSession(s);
|
|
2388
2427
|
return s;
|
|
2389
2428
|
}
|
|
2429
|
+
listSessions() {
|
|
2430
|
+
const dir = this.p("sessions");
|
|
2431
|
+
if (!fileExists(dir)) return [];
|
|
2432
|
+
const out = [];
|
|
2433
|
+
for (const f of fs4.readdirSync(dir)) {
|
|
2434
|
+
if (!f.endsWith(".json") || f === "current.json") continue;
|
|
2435
|
+
const raw = readJson(path3.join(dir, f), null);
|
|
2436
|
+
if (raw?.sessionId) out.push(normalizeSession(raw.sessionId, raw));
|
|
2437
|
+
}
|
|
2438
|
+
return out;
|
|
2439
|
+
}
|
|
2390
2440
|
readCurrent() {
|
|
2391
2441
|
return readJson(this.p("sessions", "current.json"), null);
|
|
2392
2442
|
}
|
|
@@ -2819,7 +2869,7 @@ function captureNewSkills(c, state, roots, owned, incoming, out, layer, only) {
|
|
|
2819
2869
|
} catch {
|
|
2820
2870
|
continue;
|
|
2821
2871
|
}
|
|
2822
|
-
if (!(born >= since) && !only) continue;
|
|
2872
|
+
if (!(born >= since - BORN_SLACK_MS) && !only) continue;
|
|
2823
2873
|
const files = {};
|
|
2824
2874
|
for (const rel of listFiles(dir)) {
|
|
2825
2875
|
if (rel === "SKILL.md" || rel === MANIFEST) continue;
|
|
@@ -3090,7 +3140,7 @@ function materialize(sections, dir) {
|
|
|
3090
3140
|
}
|
|
3091
3141
|
return { files };
|
|
3092
3142
|
}
|
|
3093
|
-
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;
|
|
3094
3144
|
var init_deploy = __esm({
|
|
3095
3145
|
"src/v2/deploy.ts"() {
|
|
3096
3146
|
"use strict";
|
|
@@ -3112,6 +3162,7 @@ var init_deploy = __esm({
|
|
|
3112
3162
|
};
|
|
3113
3163
|
LAYER_RANK = { org: 0, dept: 1, team: 2, user: 3, fleet: 0, arm: 1, mount: 2, robot: 3 };
|
|
3114
3164
|
docMarker = (id, version2) => `<!-- bunshin:doc id=${id} v=${version2} -->`;
|
|
3165
|
+
BORN_SLACK_MS = 2e3;
|
|
3115
3166
|
}
|
|
3116
3167
|
});
|
|
3117
3168
|
|
|
@@ -3592,28 +3643,7 @@ function appendedText(op) {
|
|
|
3592
3643
|
return op.condition ? `- **${op.condition}:** ${op.text.trim()}` : op.text.trim();
|
|
3593
3644
|
}
|
|
3594
3645
|
function summarize(op) {
|
|
3595
|
-
|
|
3596
|
-
const one = s.replace(/\s+/g, " ").trim();
|
|
3597
|
-
return one.length > 100 ? `${one.slice(0, 97)}\u2026` : one;
|
|
3598
|
-
};
|
|
3599
|
-
switch (op.op) {
|
|
3600
|
-
case "append":
|
|
3601
|
-
return clip(op.condition ? `${op.condition}: ${op.text}` : op.text);
|
|
3602
|
-
case "replace":
|
|
3603
|
-
return clip(`${op.find} \u2192 ${op.text}`);
|
|
3604
|
-
case "create":
|
|
3605
|
-
return clip(op.section.title);
|
|
3606
|
-
case "param":
|
|
3607
|
-
return `${op.key} = ${String(op.value)}`;
|
|
3608
|
-
case "rewrite":
|
|
3609
|
-
return clip(`rewrite (${op.text.length} chars)`);
|
|
3610
|
-
case "file":
|
|
3611
|
-
return clip(op.content === null ? `delete ${op.path}` : `file ${op.path}`);
|
|
3612
|
-
case "retire":
|
|
3613
|
-
return clip(`retire: ${op.reason}`);
|
|
3614
|
-
case "situation":
|
|
3615
|
-
return clip(`also applies to: ${op.text}`);
|
|
3616
|
-
}
|
|
3646
|
+
return summarizeOp(op);
|
|
3617
3647
|
}
|
|
3618
3648
|
function describeTarget(s, view) {
|
|
3619
3649
|
if (s.op.op === "param") return view.ctx.kind === "robot" ? "params.json" : "private note";
|
|
@@ -3779,6 +3809,112 @@ var init_apply = __esm({
|
|
|
3779
3809
|
}
|
|
3780
3810
|
});
|
|
3781
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
|
+
|
|
3782
3918
|
// src/v2/sync.ts
|
|
3783
3919
|
var sync_exports = {};
|
|
3784
3920
|
__export(sync_exports, {
|
|
@@ -3787,11 +3923,11 @@ __export(sync_exports, {
|
|
|
3787
3923
|
syncV2: () => syncV2,
|
|
3788
3924
|
uploadIndex: () => uploadIndex
|
|
3789
3925
|
});
|
|
3790
|
-
import
|
|
3926
|
+
import fs10 from "node:fs";
|
|
3791
3927
|
import path8 from "node:path";
|
|
3792
3928
|
function episodePayload(e) {
|
|
3793
|
-
const { orgId: _o, uploaded: _u, servedReported: _s, ...rest } = e;
|
|
3794
|
-
void _o, _u, _s;
|
|
3929
|
+
const { orgId: _o, uploaded: _u, servedReported: _s, reportedServed: _rs, reportedLoaded: _rl, ...rest } = e;
|
|
3930
|
+
void _o, _u, _s, _rs, _rl;
|
|
3795
3931
|
return rest;
|
|
3796
3932
|
}
|
|
3797
3933
|
function suggestionPayload(s) {
|
|
@@ -3828,18 +3964,27 @@ async function syncV2(store, opts = {}) {
|
|
|
3828
3964
|
for (const e of v2.listEpisodes().filter((x) => !x.uploaded)) {
|
|
3829
3965
|
if (Date.now() >= deadline) break;
|
|
3830
3966
|
try {
|
|
3831
|
-
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
|
+
}
|
|
3832
3974
|
const fresh = v2.readEpisode(e.id) ?? e;
|
|
3833
3975
|
const next = { ...fresh, uploaded: JSON.stringify(episodePayload(fresh)) === JSON.stringify(episodePayload(e)) ? true : fresh.uploaded };
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3976
|
+
const legacy = fresh.servedReported && !fresh.reportedServed;
|
|
3977
|
+
const doneServed = new Set(legacy ? e.sectionsServed : fresh.reportedServed ?? []);
|
|
3978
|
+
const doneLoaded = new Set(legacy ? e.sectionsLoaded : fresh.reportedLoaded ?? []);
|
|
3979
|
+
const served = e.sectionsServed.filter((id) => !isLocalId(id) && !doneServed.has(id));
|
|
3980
|
+
const loaded = e.sectionsLoaded.filter((id) => !isLocalId(id) && !doneLoaded.has(id));
|
|
3981
|
+
try {
|
|
3982
|
+
if (served.length) await call("POST", "/recall/served", { sectionIds: served });
|
|
3983
|
+
if (loaded.length) await call("POST", "/recall/served", { sectionIds: loaded, loaded: true });
|
|
3984
|
+
next.reportedServed = [...doneServed, ...served];
|
|
3985
|
+
next.reportedLoaded = [...doneLoaded, ...loaded];
|
|
3986
|
+
delete next.servedReported;
|
|
3987
|
+
} catch {
|
|
3843
3988
|
}
|
|
3844
3989
|
v2.writeEpisode(next);
|
|
3845
3990
|
report.uploadedEpisodes++;
|
|
@@ -3934,7 +4079,7 @@ async function uploadIndex(store, cwd, deadline = Date.now() + 3e4) {
|
|
|
3934
4079
|
const { root, index } = indexWorkingTree(v2, config, cwd, { force: true });
|
|
3935
4080
|
const docs = (index.paths ?? Object.keys(index.files)).flatMap((rel) => {
|
|
3936
4081
|
try {
|
|
3937
|
-
return [{ path: rel, text:
|
|
4082
|
+
return [{ path: rel, text: fs10.readFileSync(path8.join(root, rel), "utf8") }];
|
|
3938
4083
|
} catch {
|
|
3939
4084
|
return [];
|
|
3940
4085
|
}
|
|
@@ -3960,7 +4105,7 @@ var init_sync = __esm({
|
|
|
3960
4105
|
// src/v2/reflect.ts
|
|
3961
4106
|
import { spawn as spawn3 } from "node:child_process";
|
|
3962
4107
|
import { createHash as createHash2 } from "node:crypto";
|
|
3963
|
-
import
|
|
4108
|
+
import fs11 from "node:fs";
|
|
3964
4109
|
function episodeIdFor(contextId, sessionId) {
|
|
3965
4110
|
return `e_${createHash2("sha256").update(`${contextId}
|
|
3966
4111
|
${sessionId}`).digest("hex").slice(0, 16)}`;
|
|
@@ -3980,7 +4125,7 @@ function runClaudeFork(sessionId, prompt, { cwd, timeoutMs = FORK_TIMEOUT_MS } =
|
|
|
3980
4125
|
};
|
|
3981
4126
|
let child;
|
|
3982
4127
|
try {
|
|
3983
|
-
child = spawn3(bin, args, { cwd: cwd &&
|
|
4128
|
+
child = spawn3(bin, args, { cwd: cwd && fs11.existsSync(cwd) ? cwd : void 0, env: childEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
3984
4129
|
} catch (e) {
|
|
3985
4130
|
resolve({ ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
3986
4131
|
return;
|
|
@@ -4025,13 +4170,28 @@ function reflectionIndex(view, ids, task, focus, humanTurns = []) {
|
|
|
4025
4170
|
return [...chosen.values()].slice(0, 30).map(({ id, kind, layer, title, when, outline }) => ({ id, kind, layer, title, when, ...outline?.length ? { outline } : {} }));
|
|
4026
4171
|
}
|
|
4027
4172
|
function humanTurnsOf(transcriptPath) {
|
|
4028
|
-
if (!transcriptPath || !
|
|
4173
|
+
if (!transcriptPath || !fs11.existsSync(transcriptPath)) return [];
|
|
4029
4174
|
try {
|
|
4030
4175
|
return parseTranscript(transcriptPath).turns.filter((t) => t.role === "user" && t.text.trim()).map((t) => t.text.slice(0, 1e3));
|
|
4031
4176
|
} catch {
|
|
4032
4177
|
return [];
|
|
4033
4178
|
}
|
|
4034
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
|
+
}
|
|
4035
4195
|
async function runReflectionV2(store, opts) {
|
|
4036
4196
|
const v2 = new V2Store(store);
|
|
4037
4197
|
const config = store.config();
|
|
@@ -4052,7 +4212,12 @@ async function runReflectionV2(store, opts) {
|
|
|
4052
4212
|
const captured = [.../* @__PURE__ */ new Set([...session.captured, ...prior.map((s) => s.target.sectionId ?? localSectionId(s.id))])];
|
|
4053
4213
|
const index = reflectionIndex(view, [...session.loaded, ...session.pushed, ...session.served], session.task, opts.focus, humanTurnsOf(session.transcriptPath));
|
|
4054
4214
|
const harness = session.harness ?? config.harness;
|
|
4055
|
-
const
|
|
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 } : {} };
|
|
4056
4221
|
let raw = null;
|
|
4057
4222
|
if (harness === "claude-code") {
|
|
4058
4223
|
const fork = await runClaudeFork(opts.sessionId, buildReflectPrompt(promptBase), { cwd: session.cwd });
|
|
@@ -4070,8 +4235,8 @@ async function runReflectionV2(store, opts) {
|
|
|
4070
4235
|
result.method = "fork";
|
|
4071
4236
|
} else result.error = fork.error;
|
|
4072
4237
|
}
|
|
4073
|
-
if (result.method === "none" && session.transcriptPath &&
|
|
4074
|
-
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 });
|
|
4075
4240
|
const res = await askJson(config, {
|
|
4076
4241
|
system: "You are Bunshin's reflection pass. Follow the instructions in the user message and reply with one JSON object only.",
|
|
4077
4242
|
user: buildReflectPrompt({ ...promptBase, transcriptDigest }),
|
|
@@ -4107,7 +4272,10 @@ async function runReflectionV2(store, opts) {
|
|
|
4107
4272
|
const ids = result.suggestions.map((s) => s.id);
|
|
4108
4273
|
v2.updateSession(opts.sessionId, session.cwd, (s) => {
|
|
4109
4274
|
addUnique(s.suggestionIds, ...ids);
|
|
4110
|
-
if (result.method !== "none")
|
|
4275
|
+
if (result.method !== "none") {
|
|
4276
|
+
s.reflectedAt = createdAt;
|
|
4277
|
+
s.reflectedTurns = Math.max(s.reflectedTurns ?? 0, turns);
|
|
4278
|
+
}
|
|
4111
4279
|
if (origin === "learn") {
|
|
4112
4280
|
for (const sug of result.suggestions) if (sug.status !== "proposed") addUnique(s.captured, sug.target.sectionId ?? localSectionId(sug.id));
|
|
4113
4281
|
}
|
|
@@ -4120,6 +4288,7 @@ async function runReflectionV2(store, opts) {
|
|
|
4120
4288
|
suggestionIds: [.../* @__PURE__ */ new Set([...episode.suggestionIds, ...ids])],
|
|
4121
4289
|
captured: fresh?.captured ?? episode.captured,
|
|
4122
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) } : {},
|
|
4123
4292
|
uploaded: false
|
|
4124
4293
|
});
|
|
4125
4294
|
}
|
|
@@ -4156,6 +4325,7 @@ var init_reflect2 = __esm({
|
|
|
4156
4325
|
init_api();
|
|
4157
4326
|
init_apply();
|
|
4158
4327
|
init_store2();
|
|
4328
|
+
init_signals();
|
|
4159
4329
|
init_view();
|
|
4160
4330
|
FORK_TIMEOUT_MS = 12e4;
|
|
4161
4331
|
}
|
|
@@ -7941,95 +8111,15 @@ function refreshInBackground(v2, store, detach2, cwd, now = Date.now()) {
|
|
|
7941
8111
|
// src/v2/hooks.ts
|
|
7942
8112
|
init_reflect2();
|
|
7943
8113
|
init_repo();
|
|
7944
|
-
|
|
7945
|
-
// src/v2/signals.ts
|
|
7946
|
-
init_transcript();
|
|
7947
|
-
import fs11 from "node:fs";
|
|
7948
|
-
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;
|
|
7949
|
-
function isCorrection(text2) {
|
|
7950
|
-
const t = text2.trim().slice(0, 600);
|
|
7951
|
-
return !!t && CORRECTION_RE.test(t);
|
|
7952
|
-
}
|
|
7953
|
-
function codeOf(input) {
|
|
7954
|
-
let v = input;
|
|
7955
|
-
if (typeof v === "string") {
|
|
7956
|
-
try {
|
|
7957
|
-
v = JSON.parse(v);
|
|
7958
|
-
} catch {
|
|
7959
|
-
return null;
|
|
7960
|
-
}
|
|
7961
|
-
}
|
|
7962
|
-
if (v && typeof v === "object" && typeof v.code === "string") return v.code;
|
|
7963
|
-
return null;
|
|
7964
|
-
}
|
|
7965
|
-
function scanTranscript(file) {
|
|
7966
|
-
const empty = { facts: { turns: [], userMessages: 0, toolCalls: 0, toolFailures: 0 }, snippets: [] };
|
|
7967
|
-
if (!file) return empty;
|
|
7968
|
-
let raw;
|
|
7969
|
-
try {
|
|
7970
|
-
raw = fs11.readFileSync(file, "utf8");
|
|
7971
|
-
} catch {
|
|
7972
|
-
return empty;
|
|
7973
|
-
}
|
|
7974
|
-
const out = { facts: parseTranscript(file), snippets: [] };
|
|
7975
|
-
for (const text2 of raw.split("\n")) {
|
|
7976
|
-
if (!text2.trim()) continue;
|
|
7977
|
-
let line;
|
|
7978
|
-
try {
|
|
7979
|
-
line = JSON.parse(text2);
|
|
7980
|
-
} catch {
|
|
7981
|
-
continue;
|
|
7982
|
-
}
|
|
7983
|
-
const at = typeof line.timestamp === "string" && !Number.isNaN(Date.parse(line.timestamp)) ? line.timestamp : void 0;
|
|
7984
|
-
if (at && !out.firstAt) out.firstAt = at;
|
|
7985
|
-
const isModel = line.type === "assistant" || line.type === "response_item" && (line.payload?.type === "message" || line.payload?.type === "function_call");
|
|
7986
|
-
if (at && isModel) out.lastModelAt = at;
|
|
7987
|
-
if (line.type === "assistant" && Array.isArray(line.message?.content)) {
|
|
7988
|
-
for (const b of line.message.content) {
|
|
7989
|
-
if (b?.type === "tool_use") {
|
|
7990
|
-
const code2 = codeOf(b.input);
|
|
7991
|
-
if (code2) out.snippets.push(code2);
|
|
7992
|
-
}
|
|
7993
|
-
}
|
|
7994
|
-
} else if (line.type === "response_item" && (line.payload?.type === "function_call" || line.payload?.type === "custom_tool_call")) {
|
|
7995
|
-
const code2 = codeOf(line.payload.input ?? line.payload.arguments);
|
|
7996
|
-
if (code2) out.snippets.push(code2);
|
|
7997
|
-
}
|
|
7998
|
-
}
|
|
7999
|
-
return out;
|
|
8000
|
-
}
|
|
8001
|
-
function sessionSignals({ scan, startedAt, lastActivityAt }) {
|
|
8002
|
-
const users = scan.facts.turns.filter((t) => t.role === "user");
|
|
8003
|
-
const corrections = users.slice(1).filter((t) => isCorrection(t.text)).length;
|
|
8004
|
-
const last = [scan.lastModelAt, lastActivityAt].filter((x) => !!x).sort().at(-1) ?? lastActivityAt;
|
|
8005
|
-
const first = [scan.firstAt, startedAt].filter((x) => !!x).sort()[0] ?? startedAt;
|
|
8006
|
-
const minutes = Math.max(0, (Date.parse(last) - Date.parse(first)) / 6e4);
|
|
8007
|
-
return {
|
|
8008
|
-
toolCalls: scan.facts.toolCalls,
|
|
8009
|
-
errors: scan.facts.toolFailures,
|
|
8010
|
-
corrections,
|
|
8011
|
-
userTurns: scan.facts.userMessages,
|
|
8012
|
-
minutes: Number.isFinite(minutes) ? Math.round(minutes * 10) / 10 : 0,
|
|
8013
|
-
lastActivityAt: last
|
|
8014
|
-
};
|
|
8015
|
-
}
|
|
8016
|
-
function errorStreakOvercome(facts, cursor = 0) {
|
|
8017
|
-
const results = facts.turns.filter((t) => t.role === "tool" && t.tool === "result" && !t.pending);
|
|
8018
|
-
const fresh = results.slice(Math.min(cursor, results.length));
|
|
8019
|
-
let failures = 0;
|
|
8020
|
-
let nudge = false;
|
|
8021
|
-
for (const r of fresh) {
|
|
8022
|
-
if (r.isError) failures++;
|
|
8023
|
-
else if (failures >= 3) nudge = true;
|
|
8024
|
-
}
|
|
8025
|
-
return { nudge, cursor: nudge ? results.length : Math.min(cursor, results.length) };
|
|
8026
|
-
}
|
|
8027
|
-
|
|
8028
|
-
// src/v2/hooks.ts
|
|
8114
|
+
init_signals();
|
|
8029
8115
|
init_store2();
|
|
8030
8116
|
init_view();
|
|
8031
8117
|
var NUDGE_TEXT = "This looks worth capturing. Consider /learn.";
|
|
8032
8118
|
var NUDGE_INTERVAL_MS = 60 * 6e4;
|
|
8119
|
+
var CHECKPOINT_MS = 30 * 6e4;
|
|
8120
|
+
var LEFTOVER_IDLE_MS = 60 * 6e4;
|
|
8121
|
+
var LEFTOVER_MAX_AGE_MS = 14 * 24 * 60 * 6e4;
|
|
8122
|
+
var LEFTOVERS_PER_START = 5;
|
|
8033
8123
|
var MAX_PUSH = 3;
|
|
8034
8124
|
var additional = (hookEventName, text2) => text2.trim() ? { hookSpecificOutput: { hookEventName, additionalContext: text2.trim() } } : {};
|
|
8035
8125
|
function touch(v2, ctx, patch = () => {
|
|
@@ -8072,6 +8162,11 @@ async function onSessionStart(ctx) {
|
|
|
8072
8162
|
v2.writeCurrent({ sessionId: id, cwd: ctx.cwd, at: now });
|
|
8073
8163
|
const config = ctx.store.config();
|
|
8074
8164
|
if (canCall(config)) {
|
|
8165
|
+
try {
|
|
8166
|
+
const swept = checkpointLeftovers(ctx.store, id);
|
|
8167
|
+
if (swept.length) v2.log("v2-leftovers", { session: id, episodes: swept });
|
|
8168
|
+
} catch {
|
|
8169
|
+
}
|
|
8075
8170
|
const budget = Math.min(3e3, ctx.deadline - Date.now() - 400);
|
|
8076
8171
|
if (budget > 100) {
|
|
8077
8172
|
try {
|
|
@@ -8254,6 +8349,16 @@ function onStop(ctx) {
|
|
|
8254
8349
|
const config = ctx.store.config();
|
|
8255
8350
|
if (ctx.input.stop_hook_active) return {};
|
|
8256
8351
|
const scan = scanTranscript(ctx.input.transcript_path ?? session.transcriptPath);
|
|
8352
|
+
if (canCall(config) && checkpointDue(session, scan, now)) {
|
|
8353
|
+
try {
|
|
8354
|
+
const { episode } = buildEpisode(ctx.store, session, void 0, now, { scan });
|
|
8355
|
+
v2.writeEpisode(episode);
|
|
8356
|
+
markCheckpoint(v2, session, scan, now);
|
|
8357
|
+
ctx.detach(["--root", ctx.store.root, "sync", "--quiet"], { cwd: ctx.cwd });
|
|
8358
|
+
v2.log("v2-checkpoint", { session: session.sessionId, episode: episode.id });
|
|
8359
|
+
} catch {
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8257
8362
|
const check = learningCheck(v2, session, scan.facts.toolCalls, now);
|
|
8258
8363
|
if (check) return check;
|
|
8259
8364
|
if (!config.reflect.nudge) return {};
|
|
@@ -8285,13 +8390,16 @@ function learningCheck(v2, session, toolCalls, now) {
|
|
|
8285
8390
|
const reason = doc.body.replace(/^\s*<!--\s*bunshin:[\s\S]*?-->\s*/, "").trim().replace(/\{delta\}/g, String(toolCalls - last));
|
|
8286
8391
|
return { decision: "block", reason };
|
|
8287
8392
|
}
|
|
8288
|
-
function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new Date()) {
|
|
8393
|
+
function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new Date(), opts = {}) {
|
|
8289
8394
|
const v2 = new V2Store(store);
|
|
8290
8395
|
const config = store.config();
|
|
8291
8396
|
const view = localView(v2, config, session.cwd, { index: false });
|
|
8292
|
-
const scan = scanTranscript(transcriptPath);
|
|
8397
|
+
const scan = opts.scan ?? scanTranscript(transcriptPath ?? session.transcriptPath);
|
|
8293
8398
|
const outcome = v2.readOutcome(session.sessionId);
|
|
8294
|
-
const
|
|
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 } : {} };
|
|
8295
8403
|
const gate = sessionGate(signals, { now: now.toISOString(), cacheTtlMs: config.reflect.cacheTtlMs });
|
|
8296
8404
|
const id = episodeIdFor(view.ctx.id, session.sessionId);
|
|
8297
8405
|
const previous = v2.readEpisode(id);
|
|
@@ -8306,23 +8414,69 @@ function buildEpisode(store, session, transcriptPath, now = /* @__PURE__ */ new
|
|
|
8306
8414
|
harness: ["claude-code", "codex", "pi", "other"].includes(session.harness) ? session.harness : config.harness,
|
|
8307
8415
|
task: session.task,
|
|
8308
8416
|
startedAt: session.startedAt,
|
|
8309
|
-
endedAt: now.toISOString(),
|
|
8417
|
+
endedAt: opts.endedAt ?? now.toISOString(),
|
|
8310
8418
|
sectionsServed: session.served,
|
|
8311
8419
|
sectionsLoaded: session.loaded,
|
|
8312
8420
|
captured: session.captured,
|
|
8313
8421
|
suggestionIds: [.../* @__PURE__ */ new Set([...previous?.suggestionIds ?? [], ...session.suggestionIds])],
|
|
8314
|
-
|
|
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",
|
|
8315
8424
|
gate,
|
|
8316
8425
|
upload,
|
|
8317
|
-
|
|
8426
|
+
turns,
|
|
8427
|
+
reflectedTurns: from,
|
|
8428
|
+
...upload !== "header-only" && turns > from ? { transcript: digest(scan.facts, { from, maxChars: upload === "full" ? 2e5 : 6e4 }), transcriptFrom: from } : {},
|
|
8318
8429
|
...calls.length ? { calls } : {},
|
|
8319
8430
|
...snippets.length ? { snippets } : {},
|
|
8320
8431
|
...outcome ? { outcome } : {},
|
|
8321
8432
|
...view.bundle?.release?.version ? { harnessVersion: view.bundle.release.version } : {},
|
|
8322
|
-
uploaded: false
|
|
8433
|
+
uploaded: false,
|
|
8434
|
+
...previous?.reportedServed ? { reportedServed: previous.reportedServed } : {},
|
|
8435
|
+
...previous?.reportedLoaded ? { reportedLoaded: previous.reportedLoaded } : {},
|
|
8436
|
+
...previous?.servedReported ? { servedReported: true } : {}
|
|
8323
8437
|
};
|
|
8324
8438
|
return { episode, gate, scan };
|
|
8325
8439
|
}
|
|
8440
|
+
var progressOf = (scan) => scan.facts.toolCalls + scan.facts.turns.length;
|
|
8441
|
+
function checkpointDue(session, scan, now) {
|
|
8442
|
+
const since = Date.parse(session.checkpointAt ?? session.startedAt);
|
|
8443
|
+
if (!Number.isFinite(since) || now.getTime() - since < CHECKPOINT_MS) return false;
|
|
8444
|
+
return progressOf(scan) > (session.checkpointProgress ?? 0);
|
|
8445
|
+
}
|
|
8446
|
+
function markCheckpoint(v2, session, scan, now) {
|
|
8447
|
+
v2.updateSession(session.sessionId, session.cwd, (s) => {
|
|
8448
|
+
s.checkpointAt = now.toISOString();
|
|
8449
|
+
s.checkpointProgress = progressOf(scan);
|
|
8450
|
+
s.checkpointActivity = s.lastActivityAt;
|
|
8451
|
+
});
|
|
8452
|
+
}
|
|
8453
|
+
function checkpointLeftovers(store, currentSessionId, now = /* @__PURE__ */ new Date()) {
|
|
8454
|
+
const v2 = new V2Store(store);
|
|
8455
|
+
const out = [];
|
|
8456
|
+
const candidates = v2.listSessions().filter((s) => {
|
|
8457
|
+
if (s.sessionId === currentSessionId || !s.transcriptPath) return false;
|
|
8458
|
+
const idle = now.getTime() - Date.parse(s.lastActivityAt);
|
|
8459
|
+
if (!(idle >= LEFTOVER_IDLE_MS && idle <= LEFTOVER_MAX_AGE_MS)) return false;
|
|
8460
|
+
return s.checkpointActivity !== s.lastActivityAt;
|
|
8461
|
+
}).sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt));
|
|
8462
|
+
for (const s of candidates) {
|
|
8463
|
+
if (out.length >= LEFTOVERS_PER_START) break;
|
|
8464
|
+
const config = store.config();
|
|
8465
|
+
const id = episodeIdFor(localView(v2, config, s.cwd, { index: false }).ctx.id, s.sessionId);
|
|
8466
|
+
const prev = v2.readEpisode(id);
|
|
8467
|
+
if (prev && prev.endedAt >= s.lastActivityAt) {
|
|
8468
|
+
v2.updateSession(s.sessionId, s.cwd, (x) => {
|
|
8469
|
+
x.checkpointActivity = x.lastActivityAt;
|
|
8470
|
+
});
|
|
8471
|
+
continue;
|
|
8472
|
+
}
|
|
8473
|
+
const { episode, scan } = buildEpisode(store, s, void 0, now, { endedAt: s.lastActivityAt });
|
|
8474
|
+
v2.writeEpisode(episode);
|
|
8475
|
+
markCheckpoint(v2, s, scan, now);
|
|
8476
|
+
out.push(episode.id);
|
|
8477
|
+
}
|
|
8478
|
+
return out;
|
|
8479
|
+
}
|
|
8326
8480
|
function recordTrialEvidence(v2, session, outcome) {
|
|
8327
8481
|
if (!outcome || outcome.status === "unknown") return 0;
|
|
8328
8482
|
const used = /* @__PURE__ */ new Set([...session.loaded, ...session.pushed]);
|
|
@@ -8374,7 +8528,8 @@ function onPreCompact(ctx) {
|
|
|
8374
8528
|
const session = touch(v2, ctx);
|
|
8375
8529
|
if (!config.reflect.enabled) return {};
|
|
8376
8530
|
const scan = scanTranscript(ctx.input.transcript_path ?? session.transcriptPath);
|
|
8377
|
-
const
|
|
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 });
|
|
8378
8533
|
if (gate.worth) ctx.detach(reflectArgs(ctx, session.sessionId), { cwd: session.cwd || ctx.cwd });
|
|
8379
8534
|
v2.log("v2-pre-compact", { session: session.sessionId, gate: gate.reason, fork: gate.worth });
|
|
8380
8535
|
return {};
|