@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.
- package/dist/bin.js +203 -124
- 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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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 &&
|
|
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 || !
|
|
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
|
|
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 &&
|
|
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")
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {};
|