agent-relay-server 0.122.0 → 0.123.0
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/docs/openapi.json +1 -1
- package/package.json +7 -8
- package/public/assets/{MessageBubble-DRdUD-kW.js → MessageBubble-BTeFExjj.js} +2 -2
- package/public/assets/{MessageBubble-DRdUD-kW.js.map → MessageBubble-BTeFExjj.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-siKMPCln.js → PlanGraphPanel-jU1DXg7w.js} +2 -2
- package/public/assets/{PlanGraphPanel-siKMPCln.js.map → PlanGraphPanel-jU1DXg7w.js.map} +1 -1
- package/public/assets/{activity-D3MNcSaV.js → activity-DUig2iYW.js} +2 -2
- package/public/assets/{activity-D3MNcSaV.js.map → activity-DUig2iYW.js.map} +1 -1
- package/public/assets/{agent-profiles-CKzttq6G.js → agent-profiles-D-oFmb6S.js} +2 -2
- package/public/assets/{agent-profiles-CKzttq6G.js.map → agent-profiles-D-oFmb6S.js.map} +1 -1
- package/public/assets/{agent-type-icon-0ZJRP1Vy.js → agent-type-icon-Dan2l3Tm.js} +2 -2
- package/public/assets/{agent-type-icon-0ZJRP1Vy.js.map → agent-type-icon-Dan2l3Tm.js.map} +1 -1
- package/public/assets/{agents-CuVj_rfG.js → agents-BkC6mcTh.js} +2 -2
- package/public/assets/{agents-CuVj_rfG.js.map → agents-BkC6mcTh.js.map} +1 -1
- package/public/assets/{analytics-iob6C27d.js → analytics-C3Dlmqe9.js} +2 -2
- package/public/assets/{analytics-iob6C27d.js.map → analytics-C3Dlmqe9.js.map} +1 -1
- package/public/assets/{automation-B2ixxs4q.js → automation-AeMZ70WQ.js} +2 -2
- package/public/assets/{automation-B2ixxs4q.js.map → automation-AeMZ70WQ.js.map} +1 -1
- package/public/assets/{branch-state-badge-CafBJWKo.js → branch-state-badge-CNW10MTC.js} +2 -2
- package/public/assets/{branch-state-badge-CafBJWKo.js.map → branch-state-badge-CNW10MTC.js.map} +1 -1
- package/public/assets/{channels-C-WsbL2H.js → channels-Bc_mEd56.js} +2 -2
- package/public/assets/{channels-C-WsbL2H.js.map → channels-Bc_mEd56.js.map} +1 -1
- package/public/assets/{chat-DeoVTVob.js → chat-BCNCuTgw.js} +2 -2
- package/public/assets/{chat-DeoVTVob.js.map → chat-BCNCuTgw.js.map} +1 -1
- package/public/assets/{connectors-C2Ac03LJ.js → connectors-h0d5Mc6e.js} +2 -2
- package/public/assets/{connectors-C2Ac03LJ.js.map → connectors-h0d5Mc6e.js.map} +1 -1
- package/public/assets/{display-CJzSoTMq.js → display-GFbGUG0O.js} +2 -2
- package/public/assets/{display-CJzSoTMq.js.map → display-GFbGUG0O.js.map} +1 -1
- package/public/assets/{formatted-body-35XBOY5L.js → formatted-body-CVr3Iev9.js} +3 -3
- package/public/assets/{formatted-body-35XBOY5L.js.map → formatted-body-CVr3Iev9.js.map} +1 -1
- package/public/assets/{formatted-body-impl-RmDxI5Ux.js → formatted-body-impl-Cq5SRGpm.js} +2 -2
- package/public/assets/{formatted-body-impl-RmDxI5Ux.js.map → formatted-body-impl-Cq5SRGpm.js.map} +1 -1
- package/public/assets/{index-D0xMQQb_.js → index--9UMzCFf.js} +6 -6
- package/public/assets/{index-D0xMQQb_.js.map → index--9UMzCFf.js.map} +1 -1
- package/public/assets/{insights-B643SFvm.js → insights-C8lIBXg3.js} +2 -2
- package/public/assets/{insights-B643SFvm.js.map → insights-C8lIBXg3.js.map} +1 -1
- package/public/assets/{integrations-hFIHeCF4.js → integrations-7OFsJtY7.js} +2 -2
- package/public/assets/{integrations-hFIHeCF4.js.map → integrations-7OFsJtY7.js.map} +1 -1
- package/public/assets/{maintenance-CURvdp_Z.js → maintenance-HDpg20OY.js} +2 -2
- package/public/assets/{maintenance-CURvdp_Z.js.map → maintenance-HDpg20OY.js.map} +1 -1
- package/public/assets/{managed-agents-DrZtdlSn.js → managed-agents-muCIMxng.js} +2 -2
- package/public/assets/{managed-agents-DrZtdlSn.js.map → managed-agents-muCIMxng.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CVDO--ek.js → markdown-preview-impl-Cx7pCfA6.js} +2 -2
- package/public/assets/{markdown-preview-impl-CVDO--ek.js.map → markdown-preview-impl-Cx7pCfA6.js.map} +1 -1
- package/public/assets/{memory-Bm7dt_Qf.js → memory-CTO1FfJ-.js} +2 -2
- package/public/assets/{memory-Bm7dt_Qf.js.map → memory-CTO1FfJ-.js.map} +1 -1
- package/public/assets/{messages-DTq4i2KZ.js → messages-RWSXG2UM.js} +2 -2
- package/public/assets/{messages-DTq4i2KZ.js.map → messages-RWSXG2UM.js.map} +1 -1
- package/public/assets/{orchestrators-Dra318Ap.js → orchestrators-5aiYw3yx.js} +2 -2
- package/public/assets/{orchestrators-Dra318Ap.js.map → orchestrators-5aiYw3yx.js.map} +1 -1
- package/public/assets/{overview-yr1Vmk64.js → overview-Du6XLt5l.js} +2 -2
- package/public/assets/{overview-yr1Vmk64.js.map → overview-Du6XLt5l.js.map} +1 -1
- package/public/assets/{pairs-mnlKDBGp.js → pairs-BDRnlEta.js} +2 -2
- package/public/assets/{pairs-mnlKDBGp.js.map → pairs-BDRnlEta.js.map} +1 -1
- package/public/assets/{projects-Dzdf-gkK.js → projects-DwsWE9es.js} +2 -2
- package/public/assets/{projects-Dzdf-gkK.js.map → projects-DwsWE9es.js.map} +1 -1
- package/public/assets/{prompt-settings-ByL8SNCi.js → prompt-settings-CaJX_LbA.js} +2 -2
- package/public/assets/{prompt-settings-ByL8SNCi.js.map → prompt-settings-CaJX_LbA.js.map} +1 -1
- package/public/assets/{registry-CXWyOtpr.js → registry-C-uIL0Nl.js} +2 -2
- package/public/assets/{registry-CXWyOtpr.js.map → registry-C-uIL0Nl.js.map} +1 -1
- package/public/assets/{security-0SBsc_4W.js → security-BpVrdYhJ.js} +2 -2
- package/public/assets/{security-0SBsc_4W.js.map → security-BpVrdYhJ.js.map} +1 -1
- package/public/assets/{select-Bw5z2KJ_.js → select-DallbU93.js} +2 -2
- package/public/assets/{select-Bw5z2KJ_.js.map → select-DallbU93.js.map} +1 -1
- package/public/assets/{settings-78Cyq_tA.js → settings-BUQirdyI.js} +2 -2
- package/public/assets/{settings-78Cyq_tA.js.map → settings-BUQirdyI.js.map} +1 -1
- package/public/assets/{store-BPrVuKiv.js → store-DMZSTozC.js} +2 -2
- package/public/assets/{store-BPrVuKiv.js.map → store-DMZSTozC.js.map} +1 -1
- package/public/assets/{tasks-Cg2MMeri.js → tasks-k8mQDfKy.js} +2 -2
- package/public/assets/{tasks-Cg2MMeri.js.map → tasks-k8mQDfKy.js.map} +1 -1
- package/public/assets/{teams-DWv6UwFV.js → teams-DI-Lhhbl.js} +2 -2
- package/public/assets/{teams-DWv6UwFV.js.map → teams-DI-Lhhbl.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DBLDjbIg.js → terminal-viewer-impl-BL853Fpi.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DBLDjbIg.js.map → terminal-viewer-impl-BL853Fpi.js.map} +1 -1
- package/public/assets/types-CRptbSYh.js +2 -0
- package/public/assets/types-CRptbSYh.js.map +1 -0
- package/public/assets/{user-avatar-BsOLyVJj.js → user-avatar-D9xyGi3u.js} +2 -2
- package/public/assets/{user-avatar-BsOLyVJj.js.map → user-avatar-D9xyGi3u.js.map} +1 -1
- package/public/assets/{work-queue-DmwvnF6F.js → work-queue-DrLIdvxB.js} +2 -2
- package/public/assets/{work-queue-DmwvnF6F.js.map → work-queue-DrLIdvxB.js.map} +1 -1
- package/public/assets/{workspaces-dIBivJN8.js → workspaces-CnNL6T8Q.js} +2 -2
- package/public/assets/{workspaces-dIBivJN8.js.map → workspaces-CnNL6T8Q.js.map} +1 -1
- package/public/index.html +6 -6
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/adapters/claude-delivery.ts +127 -0
- package/runner/src/adapters/claude-quota-harvest.ts +92 -0
- package/runner/src/adapters/claude-session-probe.ts +225 -0
- package/runner/src/adapters/claude-status-detectors.ts +147 -0
- package/runner/src/adapters/claude-transcript-tail.ts +128 -0
- package/runner/src/adapters/claude-transcript.ts +414 -0
- package/runner/src/adapters/claude.ts +675 -0
- package/runner/src/adapters/codex-agent-message-capture.ts +139 -0
- package/runner/src/adapters/codex-client.ts +279 -0
- package/runner/src/adapters/codex.ts +1345 -0
- package/runner/src/attachment-cache.ts +189 -0
- package/runner/src/busy-reconciler.ts +102 -0
- package/runner/src/claim-tracker.ts +140 -0
- package/runner/src/claude-prompt-gates.ts +268 -0
- package/runner/src/codex-version.ts +63 -0
- package/runner/src/config.ts +7 -0
- package/runner/src/context-probe-state.ts +6 -0
- package/runner/src/continuation-archive.ts +179 -0
- package/runner/src/control-server.ts +639 -0
- package/runner/src/index.ts +396 -0
- package/runner/src/launch-assembly.ts +674 -0
- package/runner/src/liveness.ts +50 -0
- package/runner/src/logger.ts +99 -0
- package/runner/src/mcp-outbox.ts +120 -0
- package/runner/src/message-body-config/config.ts +3 -0
- package/runner/src/message-body-config.ts +1 -0
- package/runner/src/native-self-resume.ts +202 -0
- package/runner/src/outbox.ts +342 -0
- package/runner/src/process-meta.ts +9 -0
- package/runner/src/profile-home.ts +260 -0
- package/runner/src/profile-projection.ts +324 -0
- package/runner/src/providers.ts +20 -0
- package/runner/src/provisioning.ts +368 -0
- package/runner/src/quota.ts +40 -0
- package/runner/src/rate-limit.ts +189 -0
- package/runner/src/relay-injection-events.ts +105 -0
- package/runner/src/relay-instructions.ts +60 -0
- package/runner/src/relay-mcp-proxy.ts +383 -0
- package/runner/src/relay-mcp.ts +156 -0
- package/runner/src/reply-obligation-cache.ts +136 -0
- package/runner/src/response-capture-report.ts +63 -0
- package/runner/src/runner-core.ts +2409 -0
- package/runner/src/runner-helpers.ts +435 -0
- package/runner/src/runner-insights.ts +105 -0
- package/runner/src/runner.ts +14 -0
- package/runner/src/session-destroy.ts +22 -0
- package/runner/src/session-insights.ts +118 -0
- package/runner/src/session-scratch.ts +271 -0
- package/runner/src/template-resolver.ts +29 -0
- package/runner/src/version.ts +44 -0
- package/src/config-store.ts +2 -0
- package/src/db/provisioning-registry.ts +40 -9
- package/src/db/provisioning-resolve.ts +66 -5
- package/src/gate-resolver.ts +7 -12
- package/src/routes/provisioning-registry.ts +1 -1
- package/src/services/spawn-agent.ts +1 -0
- package/public/assets/types-BeJUSfDj.js +0 -2
- package/public/assets/types-BeJUSfDj.js.map +0 -1
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// Phase 1 live-session lane: extract the user-visible assistant turn from a
|
|
2
|
+
// Claude Code transcript JSONL so it can be surfaced in the dashboard chat
|
|
3
|
+
// without the agent re-emitting it via /reply (zero agent tokens).
|
|
4
|
+
//
|
|
5
|
+
// Transcript shape (one JSON object per line):
|
|
6
|
+
// { type: "user", message: { content: "..." | [{type:"text"|"tool_result", ...}] } }
|
|
7
|
+
// { type: "assistant", message: { content: [{type:"thinking"|"text"|"tool_use", ...}], stop_reason } }
|
|
8
|
+
//
|
|
9
|
+
// The "current turn" is everything after the last *real* user prompt (a user
|
|
10
|
+
// entry carrying text, not just tool_result blocks). We collect the assistant
|
|
11
|
+
// `text` blocks from that turn — thinking and tool_use are dropped.
|
|
12
|
+
|
|
13
|
+
import { computeContextRatio, type SessionAnalysis, type SessionEvent } from "../session-insights";
|
|
14
|
+
|
|
15
|
+
interface TranscriptBlock {
|
|
16
|
+
type?: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
thinking?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
input?: Record<string, unknown>;
|
|
21
|
+
is_error?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TurnStep {
|
|
25
|
+
type: "narration" | "reasoning" | "tool";
|
|
26
|
+
text: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TranscriptMessage {
|
|
31
|
+
role?: string;
|
|
32
|
+
content?: string | TranscriptBlock[];
|
|
33
|
+
stop_reason?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface TranscriptEntry {
|
|
37
|
+
type?: string;
|
|
38
|
+
message?: TranscriptMessage;
|
|
39
|
+
// Claude Code stamps every transcript entry with `isSidechain`: true for
|
|
40
|
+
// entries belonging to a Task (subagent) run, false for the root session.
|
|
41
|
+
// Current CC writes sidechains to a separate subagents/*.jsonl so they don't
|
|
42
|
+
// reach the root transcript the runner tails — but older CC inlined them, and
|
|
43
|
+
// the behavior can revert, so the chat-mirror parsers below defensively skip
|
|
44
|
+
// sidechain entries to keep a subagent's reasoning/tools/responses from
|
|
45
|
+
// leaking into the parent agent's chat. Insights parsers (collectClaudeSession-
|
|
46
|
+
// Events/countSubstantiveTurns) intentionally do NOT filter — changing them
|
|
47
|
+
// would shift the #184/#185 baselines, a separate concern.
|
|
48
|
+
isSidechain?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function blocks(message: TranscriptMessage | undefined): TranscriptBlock[] {
|
|
52
|
+
if (!message || !Array.isArray(message.content)) return [];
|
|
53
|
+
return message.content.filter((b): b is TranscriptBlock => Boolean(b) && typeof b === "object");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** True for a subagent (Task) transcript entry — see the note on TranscriptEntry.isSidechain. */
|
|
57
|
+
function isSidechainEntry(entry: TranscriptEntry): boolean {
|
|
58
|
+
return entry.isSidechain === true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isRealUserPrompt(entry: TranscriptEntry): boolean {
|
|
62
|
+
if (entry.type !== "user") return false;
|
|
63
|
+
const content = entry.message?.content;
|
|
64
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
65
|
+
// An array user entry is a real prompt only if it carries a text block;
|
|
66
|
+
// pure tool_result entries are mid-turn plumbing, not a new prompt.
|
|
67
|
+
return blocks(entry.message).some((b) => b.type === "text" && Boolean(b.text?.trim()));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assistantText(entry: TranscriptEntry): string {
|
|
71
|
+
if (entry.type !== "assistant") return "";
|
|
72
|
+
return blocks(entry.message)
|
|
73
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
74
|
+
.map((b) => b.text!.trim())
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.join("\n\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns true when the transcript's final parseable entry is an assistant
|
|
81
|
+
* message with stop_reason "end_turn", meaning the full turn has been flushed.
|
|
82
|
+
* Used to detect a race where the Stop hook reads the file before the last
|
|
83
|
+
* assistant block is written to disk.
|
|
84
|
+
*/
|
|
85
|
+
export function transcriptLooksComplete(jsonl: string): boolean {
|
|
86
|
+
const state = new LatestTurnStepAccumulator();
|
|
87
|
+
state.appendJsonl(jsonl);
|
|
88
|
+
return state.transcriptLooksComplete;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns the concatenated assistant `text` for the most recent turn (since the
|
|
93
|
+
* last real user prompt), or "" if there is nothing user-visible to surface.
|
|
94
|
+
*/
|
|
95
|
+
export function extractLastAssistantTurn(jsonl: string): string {
|
|
96
|
+
const lines = jsonl.split("\n");
|
|
97
|
+
let collected: string[] = [];
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const trimmed = line.trim();
|
|
100
|
+
if (!trimmed) continue;
|
|
101
|
+
let entry: TranscriptEntry;
|
|
102
|
+
try {
|
|
103
|
+
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
104
|
+
} catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (isSidechainEntry(entry)) continue;
|
|
108
|
+
if (isRealUserPrompt(entry)) {
|
|
109
|
+
collected = [];
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const text = assistantText(entry);
|
|
113
|
+
if (text) collected.push(text);
|
|
114
|
+
}
|
|
115
|
+
return collected.join("\n\n").trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Like extractLastAssistantTurn but skips the first `afterEntry` valid JSON entries.
|
|
120
|
+
* Used when a partial transcript flush already emitted entries 1..afterEntry so the
|
|
121
|
+
* Stop-hook full-mode capture doesn't re-emit the already-published text.
|
|
122
|
+
*/
|
|
123
|
+
export function extractLastAssistantTurnAfterEntry(jsonl: string, afterEntry: number): string {
|
|
124
|
+
const lines = jsonl.split("\n");
|
|
125
|
+
const collected: string[] = [];
|
|
126
|
+
let entryIndex = 0;
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
const trimmed = line.trim();
|
|
129
|
+
if (!trimmed) continue;
|
|
130
|
+
let entry: TranscriptEntry;
|
|
131
|
+
try {
|
|
132
|
+
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
133
|
+
} catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
entryIndex++;
|
|
137
|
+
if (isSidechainEntry(entry)) continue;
|
|
138
|
+
if (isRealUserPrompt(entry)) {
|
|
139
|
+
collected.length = 0;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (entryIndex <= afterEntry) continue;
|
|
143
|
+
const text = assistantText(entry);
|
|
144
|
+
if (text) collected.push(text);
|
|
145
|
+
}
|
|
146
|
+
return collected.join("\n\n").trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Count valid JSON entries in a JSONL string. Used to record the high-water-mark
|
|
151
|
+
* after a pre-flush so the Stop hook knows where to resume in full-capture mode.
|
|
152
|
+
*/
|
|
153
|
+
export function countTranscriptEntries(jsonl: string): number {
|
|
154
|
+
let count = 0;
|
|
155
|
+
for (const line of jsonl.split("\n")) {
|
|
156
|
+
if (!line.trim()) continue;
|
|
157
|
+
try { JSON.parse(line); count++; } catch {}
|
|
158
|
+
}
|
|
159
|
+
return count;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Returns only the text from the LAST assistant entry in the current turn.
|
|
164
|
+
* Unlike extractLastAssistantTurn which collects all intermediate text between
|
|
165
|
+
* tool calls, this returns just the final response — what the user sees as the
|
|
166
|
+
* concluding message.
|
|
167
|
+
*/
|
|
168
|
+
export function extractFinalAssistantMessage(jsonl: string): string {
|
|
169
|
+
const lines = jsonl.split("\n");
|
|
170
|
+
let lastText = "";
|
|
171
|
+
let pastLastUserPrompt = false;
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const trimmed = line.trim();
|
|
174
|
+
if (!trimmed) continue;
|
|
175
|
+
let entry: TranscriptEntry;
|
|
176
|
+
try {
|
|
177
|
+
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
178
|
+
} catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (isSidechainEntry(entry)) continue;
|
|
182
|
+
if (isRealUserPrompt(entry)) {
|
|
183
|
+
pastLastUserPrompt = true;
|
|
184
|
+
lastText = "";
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!pastLastUserPrompt) continue;
|
|
188
|
+
const text = assistantText(entry);
|
|
189
|
+
if (text) lastText = text;
|
|
190
|
+
}
|
|
191
|
+
return lastText.trim();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Like extractFinalAssistantMessage but skips the first `afterEntry` valid JSON
|
|
196
|
+
* entries. Used when a PreToolUse pre-flush already emitted the text before a
|
|
197
|
+
* blocking control, so final-mode Stop capture only sees later assistant text.
|
|
198
|
+
*/
|
|
199
|
+
export function extractFinalAssistantMessageAfterEntry(jsonl: string, afterEntry: number): string {
|
|
200
|
+
const lines = jsonl.split("\n");
|
|
201
|
+
let lastText = "";
|
|
202
|
+
let pastLastUserPrompt = afterEntry > 0;
|
|
203
|
+
let entryIndex = 0;
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
const trimmed = line.trim();
|
|
206
|
+
if (!trimmed) continue;
|
|
207
|
+
let entry: TranscriptEntry;
|
|
208
|
+
try {
|
|
209
|
+
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
210
|
+
} catch {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
entryIndex++;
|
|
214
|
+
if (isSidechainEntry(entry)) continue;
|
|
215
|
+
if (entryIndex <= afterEntry) continue;
|
|
216
|
+
if (isRealUserPrompt(entry)) {
|
|
217
|
+
pastLastUserPrompt = true;
|
|
218
|
+
lastText = "";
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!pastLastUserPrompt) continue;
|
|
222
|
+
const text = assistantText(entry);
|
|
223
|
+
if (text) lastText = text;
|
|
224
|
+
}
|
|
225
|
+
return lastText.trim();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extract text from the `last_assistant_message` field in the Stop hook
|
|
230
|
+
* payload. This is the content of the final assistant message — either a plain
|
|
231
|
+
* string or an array of content blocks (same shape as transcript entries).
|
|
232
|
+
* Thinking and tool_use blocks are dropped, matching extractLastAssistantTurn.
|
|
233
|
+
*/
|
|
234
|
+
/**
|
|
235
|
+
* Extract the ordered narration, reasoning, and tool steps for the most recent
|
|
236
|
+
* turn (since the last real user prompt). Used by the reasoning tailer to stream
|
|
237
|
+
* progress into chat while a turn is in flight. `narration` is the assistant's
|
|
238
|
+
* intermediate `text` between tool calls (the terminal's `●` lines); it is the
|
|
239
|
+
* primary, default-visible turn content. Returns steps in transcript order so the
|
|
240
|
+
* tailer can emit only the ones it hasn't seen yet.
|
|
241
|
+
*/
|
|
242
|
+
export function extractLatestTurnSteps(jsonl: string): TurnStep[] {
|
|
243
|
+
const state = new LatestTurnStepAccumulator();
|
|
244
|
+
state.appendJsonl(jsonl);
|
|
245
|
+
return state.steps;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export class LatestTurnStepAccumulator {
|
|
249
|
+
private latestSteps: TurnStep[] = [];
|
|
250
|
+
private lastAssistantStopReason: string | undefined;
|
|
251
|
+
private parsedEntries = 0;
|
|
252
|
+
|
|
253
|
+
get steps(): TurnStep[] {
|
|
254
|
+
return [...this.latestSteps];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
get transcriptLooksComplete(): boolean {
|
|
258
|
+
return this.lastAssistantStopReason === "end_turn";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
get entryCount(): number {
|
|
262
|
+
return this.parsedEntries;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
reset(): void {
|
|
266
|
+
this.latestSteps = [];
|
|
267
|
+
this.lastAssistantStopReason = undefined;
|
|
268
|
+
this.parsedEntries = 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
appendJsonl(jsonl: string): void {
|
|
272
|
+
for (const line of jsonl.split("\n")) this.appendLine(line);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
appendLine(line: string): void {
|
|
276
|
+
const trimmed = line.trim();
|
|
277
|
+
if (!trimmed) return;
|
|
278
|
+
let entry: TranscriptEntry;
|
|
279
|
+
try {
|
|
280
|
+
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
281
|
+
} catch {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
this.parsedEntries += 1;
|
|
285
|
+
if (isSidechainEntry(entry)) return;
|
|
286
|
+
if (entry.type === "assistant") this.lastAssistantStopReason = entry.message?.stop_reason;
|
|
287
|
+
if (isRealUserPrompt(entry)) {
|
|
288
|
+
this.latestSteps = [];
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (entry.type !== "assistant") return;
|
|
292
|
+
for (const b of blocks(entry.message)) {
|
|
293
|
+
if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
|
|
294
|
+
this.latestSteps.push({ type: "narration", text: b.text.trim() });
|
|
295
|
+
} else if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.trim()) {
|
|
296
|
+
this.latestSteps.push({ type: "reasoning", text: b.thinking.trim() });
|
|
297
|
+
} else if (b.type === "tool_use" && typeof b.name === "string" && b.name) {
|
|
298
|
+
this.latestSteps.push({ type: "tool", label: b.name, text: summarizeToolUse(b.name, b.input) });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Stable dedup keys for a turn's steps, in order. Each key is salted with how many
|
|
306
|
+
* identical (type,label,text) steps preceded it in the same window — so running the
|
|
307
|
+
* same tool twice with identical input within a turn yields two distinct keys and
|
|
308
|
+
* both show in the activity trace (#265). Keying on occurrence-within-window rather
|
|
309
|
+
* than raw transcript index keeps the reasoning tailer idempotent when the "latest
|
|
310
|
+
* turn" window shrinks/resets mid-poll: a surviving step recomputes to the same or a
|
|
311
|
+
* lower occurrence, so an already-emitted step never re-fires.
|
|
312
|
+
*/
|
|
313
|
+
export function stepDedupKeys(steps: TurnStep[]): string[] {
|
|
314
|
+
const counts = new Map<string, number>();
|
|
315
|
+
return steps.map((step) => {
|
|
316
|
+
const base = JSON.stringify([step.type, step.label ?? "", step.text]);
|
|
317
|
+
const occ = counts.get(base) ?? 0;
|
|
318
|
+
counts.set(base, occ + 1);
|
|
319
|
+
return JSON.stringify([step.type, step.label ?? "", step.text, occ]);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Compact one-line summary of a tool invocation for the discreet activity row. */
|
|
324
|
+
export function summarizeToolUse(name: string, input: Record<string, unknown> | undefined): string {
|
|
325
|
+
const str = (key: string): string | undefined => (input && typeof input[key] === "string" ? (input[key] as string) : undefined);
|
|
326
|
+
const candidate = str("command") ?? str("file_path") ?? str("path") ?? str("pattern") ?? str("query") ?? str("url") ?? str("description") ?? str("prompt");
|
|
327
|
+
const summary = candidate ? candidate.replace(/\s+/g, " ").trim() : "";
|
|
328
|
+
if (!summary) return name;
|
|
329
|
+
return summary.length > 200 ? `${summary.slice(0, 197)}…` : summary;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// --- Insights #184: context-gathering ratio (epic #183, docs/self-improvement.md) ---
|
|
333
|
+
//
|
|
334
|
+
// Computed mechanically from the whole-session transcript at session end — no model
|
|
335
|
+
// involvement, so it costs zero agent tokens and the agent can't game it. The ratio is
|
|
336
|
+
// paired with cheap outcome proxies (user re-prompts, tool errors) so it's never read
|
|
337
|
+
// alone — see the anti-Goodhart constraint in the epic.
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Normalize a full Claude transcript into the provider-agnostic `SessionEvent` stream
|
|
341
|
+
* (#183/#184). The classifier and ratio math live in `session-insights.ts` and are shared
|
|
342
|
+
* across providers; this only knows the Claude JSONL shape. Events are emitted in
|
|
343
|
+
* transcript order so `leadingGather` is meaningful.
|
|
344
|
+
*/
|
|
345
|
+
export function collectClaudeSessionEvents(jsonl: string): SessionEvent[] {
|
|
346
|
+
const events: SessionEvent[] = [];
|
|
347
|
+
for (const line of jsonl.split("\n")) {
|
|
348
|
+
const trimmed = line.trim();
|
|
349
|
+
if (!trimmed) continue;
|
|
350
|
+
let entry: TranscriptEntry;
|
|
351
|
+
try {
|
|
352
|
+
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
353
|
+
} catch {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (isRealUserPrompt(entry)) events.push({ type: "user_prompt" });
|
|
357
|
+
if (entry.type === "user") {
|
|
358
|
+
for (const b of blocks(entry.message)) {
|
|
359
|
+
if (b.type === "tool_result" && b.is_error === true) events.push({ type: "tool_error" });
|
|
360
|
+
}
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (entry.type !== "assistant") continue;
|
|
364
|
+
let producedSomething = false;
|
|
365
|
+
for (const b of blocks(entry.message)) {
|
|
366
|
+
if (b.type === "text" && b.text?.trim()) producedSomething = true;
|
|
367
|
+
if (b.type !== "tool_use" || typeof b.name !== "string" || !b.name) continue;
|
|
368
|
+
producedSomething = true;
|
|
369
|
+
events.push({ type: "tool", name: b.name });
|
|
370
|
+
}
|
|
371
|
+
if (producedSomething) events.push({ type: "turn" });
|
|
372
|
+
}
|
|
373
|
+
return events;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Walk the full transcript and compute the context-gathering ratio plus paired outcome
|
|
378
|
+
* proxies. Returns null when there's nothing substantive to measure (no tool calls) —
|
|
379
|
+
* trivial sessions have nothing to learn from and shouldn't pollute the baselines.
|
|
380
|
+
*/
|
|
381
|
+
export function analyzeSession(jsonl: string): SessionAnalysis | null {
|
|
382
|
+
return computeContextRatio(collectClaudeSessionEvents(jsonl));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Count substantive assistant turns — used by the #185 introspection gate. */
|
|
386
|
+
export function countSubstantiveTurns(jsonl: string): number {
|
|
387
|
+
let turns = 0;
|
|
388
|
+
for (const line of jsonl.split("\n")) {
|
|
389
|
+
const trimmed = line.trim();
|
|
390
|
+
if (!trimmed) continue;
|
|
391
|
+
let entry: TranscriptEntry;
|
|
392
|
+
try {
|
|
393
|
+
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
394
|
+
} catch {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (entry.type !== "assistant") continue;
|
|
398
|
+
const hasContent = blocks(entry.message).some(
|
|
399
|
+
(b) => (b.type === "text" && b.text?.trim()) || (b.type === "tool_use" && b.name),
|
|
400
|
+
);
|
|
401
|
+
if (hasContent) turns++;
|
|
402
|
+
}
|
|
403
|
+
return turns;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function extractHookAssistantMessage(content: unknown): string {
|
|
407
|
+
if (typeof content === "string") return content.trim();
|
|
408
|
+
if (!Array.isArray(content)) return "";
|
|
409
|
+
return (content as TranscriptBlock[])
|
|
410
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
411
|
+
.map((b) => b.text!.trim())
|
|
412
|
+
.filter(Boolean)
|
|
413
|
+
.join("\n\n");
|
|
414
|
+
}
|