-
Notifications
You must be signed in to change notification settings - Fork 83
Expand file tree
/
Copy pathinject_transcript.mjs
More file actions
102 lines (89 loc) · 3.92 KB
/
Copy pathinject_transcript.mjs
File metadata and controls
102 lines (89 loc) · 3.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/usr/bin/env node
// SubagentStart hook: feeds the fable-advisor subagent the recent conversation
// (it otherwise sees only the caller's prompt). Node-only, exits 0 on any failure.
import { randomUUID } from "node:crypto";
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const emit = (ctx) =>
process.stdout.write(
JSON.stringify({ hookSpecificOutput: { hookEventName: "SubagentStart", additionalContext: ctx } })
);
const clip = (s, n) => (s.length > n ? s.slice(0, n) + " …[truncated]" : s);
const renderBlock = (role, b) => {
switch (b.type) {
case "text":
return `${role}: ${clip(b.text ?? "", 4000)}`;
case "thinking":
return `assistant (thinking): ${clip(b.thinking ?? "", 4000)}`;
case "tool_use":
return `assistant → ${b.name ?? "?"}(${clip(JSON.stringify(b.input ?? {}), 8000)})`;
case "tool_result": {
const c = b.content;
const text =
typeof c === "string" ? c : Array.isArray(c) ? c.map((x) => x.text ?? "").join(" ") : JSON.stringify(c ?? "");
return ` ↳ result: ${clip(text, 8000)}`;
}
default:
return "";
}
};
// On failure lines stays empty, so the reviewer is told it has only the caller's prompt.
let transcriptPath;
let lines = [];
try {
transcriptPath = JSON.parse(readFileSync(0, "utf8")).transcript_path;
lines = readFileSync(transcriptPath, "utf8").split("\n");
} catch {}
// Scan newest-first and stop at 80 records, so a multi-MB transcript never parses the head it would discard.
const records = [];
let i = lines.length - 1;
for (; i >= 0 && records.length < 80; i--) {
if (!lines[i]) continue;
let r;
try {
r = JSON.parse(lines[i]); // the transcript is appended to while this runs
} catch {
continue;
}
if (r.isSidechain === true || r.isMeta === true) continue;
if (r.type !== "user" && r.type !== "assistant") continue;
const content = r.message?.content;
let rendered;
if (typeof content === "string") rendered = `${r.type}: ${clip(content, 4000)}`;
else if (Array.isArray(content)) rendered = content.map((b) => renderBlock(r.type, b)).filter(Boolean).join("\n");
else continue;
if (rendered) records.push(rendered);
}
records.reverse();
const source = transcriptPath
? ` Full session: ${transcriptPath}, one JSON record per line, newest last. Grep it for a term, number, or phrase you are chasing; never read it whole.`
: "";
let recent = records.join("\n\n---\n\n");
const MAX = 160000;
if (recent.length > MAX) recent = "…[older turns truncated for length]\n" + recent.slice(-MAX);
// Without this the record cap is silent, and the tail of a session reads as the whole of it.
if (i >= 0) recent = `…[this is only the newest ${records.length} turns of a longer session]\n` + recent;
// Staged, not inlined: the harness previews a large additionalContext instead of passing it.
// The token exists only in the file, so echoing it proves the reviewer read it.
let contextFile;
try {
if (records.length) {
const staged = join(mkdtempSync(join(tmpdir(), "fable-advisor-")), "conversation.md");
writeFileSync(
staged,
`You are reviewing an in-progress Claude Code session. Below is the recent conversation, most recent last, the same history the built-in advisor tool would see. Weigh the specific question in the caller's prompt against this actual context, not just the caller's summary of it.
Confirmation token: ${randomUUID().slice(0, 8)}
Long turns are shortened and marked \`…[truncated]\`.${source}
<recent-conversation>
${recent}
</recent-conversation>`
);
contextFile = staged;
}
} catch {}
emit(
contextFile
? `Read ${contextFile} before answering. It holds the recent conversation your question is about and the caller's prompt is only a summary of it.`
: `The recent conversation could not be reconstructed, so you see only the caller's prompt. Name the context you need.${source}`
);