zelari-code 1.46.1 → 1.47.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/cli/app.js +4 -1
  2. package/dist/cli/app.js.map +1 -1
  3. package/dist/cli/budget/tokenBudget.js +14 -12
  4. package/dist/cli/budget/tokenBudget.js.map +1 -1
  5. package/dist/cli/hooks/conversationContext.js +7 -4
  6. package/dist/cli/hooks/conversationContext.js.map +1 -1
  7. package/dist/cli/hooks/observationStore.js +219 -0
  8. package/dist/cli/hooks/observationStore.js.map +1 -0
  9. package/dist/cli/hooks/observationStore.test.js +135 -0
  10. package/dist/cli/hooks/observationStore.test.js.map +1 -0
  11. package/dist/cli/hooks/sessionSurface.js +171 -0
  12. package/dist/cli/hooks/sessionSurface.js.map +1 -0
  13. package/dist/cli/hooks/sessionSurface.test.js +112 -0
  14. package/dist/cli/hooks/sessionSurface.test.js.map +1 -0
  15. package/dist/cli/hooks/useChatTurn.js +25 -0
  16. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  17. package/dist/cli/hooks/useSession.js +5 -3
  18. package/dist/cli/hooks/useSession.js.map +1 -1
  19. package/dist/cli/main.bundled.js +1783 -812
  20. package/dist/cli/main.bundled.js.map +4 -4
  21. package/dist/cli/metrics.js +4 -0
  22. package/dist/cli/metrics.js.map +1 -1
  23. package/dist/cli/provider/capabilities.js +53 -0
  24. package/dist/cli/provider/capabilities.js.map +1 -0
  25. package/dist/cli/provider/capabilities.test.js +46 -0
  26. package/dist/cli/provider/capabilities.test.js.map +1 -0
  27. package/dist/cli/provider/openai-compatible.js +2 -1
  28. package/dist/cli/provider/openai-compatible.js.map +1 -1
  29. package/dist/cli/toolRegistry.js +28 -0
  30. package/dist/cli/toolRegistry.js.map +1 -1
  31. package/dist/cli/tools/inspectCommand.js +36 -0
  32. package/dist/cli/tools/inspectCommand.js.map +1 -1
  33. package/dist/cli/tools/inspectCommand.schema.test.js +82 -0
  34. package/dist/cli/tools/inspectCommand.schema.test.js.map +1 -0
  35. package/dist/cli/tools/observeBatch.js +254 -0
  36. package/dist/cli/tools/observeBatch.js.map +1 -0
  37. package/dist/cli/tools/observeBatch.test.js +329 -0
  38. package/dist/cli/tools/observeBatch.test.js.map +1 -0
  39. package/dist/cli/tools/retrieveObservation.js +82 -0
  40. package/dist/cli/tools/retrieveObservation.js.map +1 -0
  41. package/dist/cli/utils/contextGrowthSummary.js +55 -0
  42. package/dist/cli/utils/contextGrowthSummary.js.map +1 -0
  43. package/dist/cli/utils/contextGrowthSummary.test.js +74 -0
  44. package/dist/cli/utils/contextGrowthSummary.test.js.map +1 -0
  45. package/dist/cli/utils/doctor.js +26 -0
  46. package/dist/cli/utils/doctor.js.map +1 -1
  47. package/package.json +2 -2
@@ -0,0 +1,135 @@
1
+ /**
2
+ * observationStore + retrieve_observation — Fase 2 acceptance.
3
+ */
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { promises as fs } from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { SessionJsonlWriter } from '@zelari/core/harness';
9
+ import { createBrainEvent } from '@zelari/core/events';
10
+ import { getObservationBySeq, getSeqForToolCallId, indexEventsForTests, ingestLiveEvent, invalidateObservationIndex, loadObservationIndex, } from './observationStore.js';
11
+ import { formatObservationStub } from './sessionSurface.js';
12
+ import { createRetrieveObservationTool, parseSeqRef } from '../tools/retrieveObservation.js';
13
+ const abort = new AbortController().signal;
14
+ function ctx(sessionId) {
15
+ return {
16
+ signal: abort,
17
+ cwd: process.cwd(),
18
+ sessionId,
19
+ audit: () => undefined,
20
+ };
21
+ }
22
+ describe('parseSeqRef', () => {
23
+ it('accepts number, #N and ref=#N', () => {
24
+ expect(parseSeqRef(12)).toBe(12);
25
+ expect(parseSeqRef('#12')).toBe(12);
26
+ expect(parseSeqRef('ref=#12')).toBe(12);
27
+ expect(parseSeqRef('OBSERVATION ref=#3')).toBe(3);
28
+ expect(parseSeqRef('nope')).toBeNull();
29
+ expect(parseSeqRef(0)).toBeNull();
30
+ });
31
+ });
32
+ describe('observationStore from JSONL', () => {
33
+ let dir;
34
+ afterEach(async () => {
35
+ invalidateObservationIndex();
36
+ if (dir)
37
+ await fs.rm(dir, { recursive: true, force: true });
38
+ });
39
+ it('assigns monotonic 1-based seq in file order and rematerializes', async () => {
40
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), 'zelari-obs-'));
41
+ const sid = 'sess-a';
42
+ const w = new SessionJsonlWriter(sid, { baseDir: dir, flushIntervalMs: 60_000 });
43
+ const start1 = createBrainEvent('tool_execution_start', sid, {
44
+ toolCallId: 'c1',
45
+ toolName: 'read_file',
46
+ args: { path: 'a.ts' },
47
+ });
48
+ const end1 = createBrainEvent('tool_execution_end', sid, {
49
+ toolCallId: 'c1',
50
+ result: 'FILE-ONE-BODY',
51
+ isError: false,
52
+ durationMs: 11,
53
+ });
54
+ const start2 = createBrainEvent('tool_execution_start', sid, {
55
+ toolCallId: 'c2',
56
+ toolName: 'grep_content',
57
+ args: { pattern: 'x' },
58
+ });
59
+ const end2 = createBrainEvent('tool_execution_end', sid, {
60
+ toolCallId: 'c2',
61
+ result: 'FILE-TWO-BODY',
62
+ isError: false,
63
+ durationMs: 22,
64
+ });
65
+ void w.append(start1);
66
+ void w.append(end1);
67
+ void w.append(start2);
68
+ void w.append(end2);
69
+ await w.flush();
70
+ const index = await loadObservationIndex(sid, dir);
71
+ expect(index.bySeq.size).toBe(2);
72
+ expect(await getSeqForToolCallId(sid, 'c2', dir)).toBe(2);
73
+ const rec = await getObservationBySeq(sid, 1, dir);
74
+ expect(rec?.result).toBe('FILE-ONE-BODY');
75
+ expect(rec?.toolName).toBe('read_file');
76
+ const tool = createRetrieveObservationTool({ sessionId: sid, baseDir: dir });
77
+ const hit = await tool.execute({ seq: '#2' }, ctx(sid));
78
+ expect(hit.ok).toBe(true);
79
+ if (hit.ok) {
80
+ expect(hit.value.result).toBe('FILE-TWO-BODY');
81
+ expect(hit.value.tool).toBe('grep_content');
82
+ expect(hit.value.seq).toBe(2);
83
+ }
84
+ const miss = await tool.execute({ seq: 99 }, ctx(sid));
85
+ expect(miss.ok).toBe(false);
86
+ await w.close();
87
+ });
88
+ it('ingestLiveEvent is idempotent and retrieve works before flush', async () => {
89
+ const sid = 'live-sess';
90
+ const start = createBrainEvent('tool_execution_start', sid, {
91
+ toolCallId: 'live-1',
92
+ toolName: 'list_files',
93
+ args: {},
94
+ });
95
+ const end = createBrainEvent('tool_execution_end', sid, {
96
+ toolCallId: 'live-1',
97
+ result: 'LIVE-BODY',
98
+ isError: false,
99
+ durationMs: 5,
100
+ });
101
+ ingestLiveEvent(sid, start);
102
+ ingestLiveEvent(sid, end);
103
+ ingestLiveEvent(sid, end); // idempotent
104
+ const rec = await getObservationBySeq(sid, 1);
105
+ expect(rec?.result).toBe('LIVE-BODY');
106
+ expect(await getSeqForToolCallId(sid, 'live-1')).toBe(1);
107
+ });
108
+ it('refuses to rematerialize a stub that leaked into the log', async () => {
109
+ const sid = 'stub-sess';
110
+ const stub = formatObservationStub({ seq: 1, tool: 'read_file', bytes: 10 });
111
+ indexEventsForTests(sid, [
112
+ createBrainEvent('tool_execution_end', sid, {
113
+ toolCallId: 's1',
114
+ result: stub,
115
+ isError: false,
116
+ durationMs: 1,
117
+ }),
118
+ ]);
119
+ const tool = createRetrieveObservationTool({ sessionId: sid, baseDir: os.tmpdir() });
120
+ const res = await tool.execute({ seq: 1 }, ctx(sid));
121
+ expect(res.ok).toBe(false);
122
+ if (!res.ok)
123
+ expect(res.meta?.warnings).toContain('STUB_IN_LOG');
124
+ });
125
+ });
126
+ describe('retrieve_observation session guard', () => {
127
+ it('fails clearly without a real session id', async () => {
128
+ const tool = createRetrieveObservationTool();
129
+ const res = await tool.execute({ seq: 1 }, ctx('cli'));
130
+ expect(res.ok).toBe(false);
131
+ if (!res.ok)
132
+ expect(res.meta?.warnings).toContain('NO_SESSION');
133
+ });
134
+ });
135
+ //# sourceMappingURL=observationStore.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observationStore.test.js","sourceRoot":"","sources":["../../../src/cli/hooks/observationStore.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ import { envNumber } from '../utils/envNumber.js';
2
+ /** Marker prefix — also used by retrieve_observation to refuse rematerializing a stub. */
3
+ export const OBSERVATION_STUB_PREFIX = 'OBSERVATION ref=#';
4
+ export const DEFAULT_HOT_TAIL = 4;
5
+ export const DEFAULT_MAX_BODY_CHARS = 4_000;
6
+ /** Kill switch: ZELARI_SESSION_SURFACE=0 restores the pre-Fase-2 pruner. */
7
+ export function sessionSurfaceEnabled() {
8
+ return process.env.ZELARI_SESSION_SURFACE !== '0';
9
+ }
10
+ /** True when a tool-result body is already a Fase-2 stub (never un-prune). */
11
+ export function isObservationStub(content) {
12
+ return content.startsWith(OBSERVATION_STUB_PREFIX);
13
+ }
14
+ /**
15
+ * Parse `OBSERVATION ref=#N …` into fields. Returns null if the body is not a stub.
16
+ * Extra key=value pairs after the known ones are preserved as `extra`.
17
+ */
18
+ export function parseObservationStub(content) {
19
+ if (!isObservationStub(content))
20
+ return null;
21
+ const first = content.split('\n', 1)[0] ?? content;
22
+ const seqMatch = first.match(/^OBSERVATION ref=#(\d+)\b/);
23
+ if (!seqMatch)
24
+ return null;
25
+ const fields = { seq: Number(seqMatch[1]) };
26
+ const tool = first.match(/\btool=([A-Za-z0-9._-]+)/);
27
+ if (tool)
28
+ fields.tool = tool[1];
29
+ const status = first.match(/\bstatus=([A-Za-z0-9._-]+)/);
30
+ if (status)
31
+ fields.status = status[1];
32
+ const bytes = first.match(/\bbytes=(\d+)/);
33
+ if (bytes)
34
+ fields.bytes = Number(bytes[1]);
35
+ const known = /^(ref|#\d+|tool|status|bytes)$/;
36
+ const extras = [];
37
+ for (const m of first.matchAll(/\b([A-Za-z_][A-Za-z0-9._-]*)=([^\s]+)/g)) {
38
+ if (!known.test(m[1]))
39
+ extras.push(`${m[1]}=${m[2]}`);
40
+ }
41
+ if (extras.length)
42
+ fields.extra = extras.join(' ');
43
+ return fields;
44
+ }
45
+ /**
46
+ * Deterministic stub. Same fields → byte-identical string (cache-safe).
47
+ * `seq` is the 1-based index of this tool_execution_end in the session log.
48
+ */
49
+ export function formatObservationStub(fields) {
50
+ const parts = [`${OBSERVATION_STUB_PREFIX}${fields.seq}`];
51
+ if (fields.tool)
52
+ parts.push(`tool=${fields.tool}`);
53
+ if (fields.status)
54
+ parts.push(`status=${fields.status}`);
55
+ if (fields.bytes !== undefined)
56
+ parts.push(`bytes=${fields.bytes}`);
57
+ if (fields.extra)
58
+ parts.push(fields.extra);
59
+ return `${parts.join(' ')}\n[retrieve_observation #${fields.seq}]`;
60
+ }
61
+ /**
62
+ * Extract Ground-Truth status from a tool-result body (Fase 0 footer), if any.
63
+ * Footer shape: `\n[observation status=complete filesWalked=12 …]`
64
+ */
65
+ export function statusFromToolBody(body) {
66
+ const m = body.match(/\[observation\s+([^\]]+)\]\s*$/);
67
+ if (!m)
68
+ return undefined;
69
+ const status = m[1].match(/\bstatus=([A-Za-z0-9._-]+)/);
70
+ return status?.[1];
71
+ }
72
+ function resolveSurfaceLimits(opts) {
73
+ return {
74
+ hotTail: opts?.hotTail ??
75
+ envNumber(process.env.ZELARI_SURFACE_HOT_TAIL, { default: DEFAULT_HOT_TAIL, min: 0 }),
76
+ maxBodyChars: opts?.maxBodyChars ??
77
+ envNumber(process.env.ZELARI_SURFACE_MAX_BODY_CHARS, {
78
+ default: DEFAULT_MAX_BODY_CHARS,
79
+ min: 64,
80
+ }),
81
+ };
82
+ }
83
+ /**
84
+ * Resolve the stable observation seq for a tool message.
85
+ *
86
+ * Preference order (all deterministic):
87
+ * 1. already a stub → keep its seq (monotone — never rewrite the number)
88
+ * 2. `lookup(toolCallId)` from the session log
89
+ * 3. 1-based index among role:'tool' messages in THIS history (offline / tests)
90
+ */
91
+ export function resolveObservationSeq(message, indexAmongTools, lookup) {
92
+ const existing = parseObservationStub(message.content ?? '');
93
+ if (existing)
94
+ return existing.seq;
95
+ const id = message.toolCallId;
96
+ if (id && lookup) {
97
+ const fromLog = lookup(id);
98
+ if (typeof fromLog === 'number' && fromLog > 0)
99
+ return fromLog;
100
+ }
101
+ return indexAmongTools + 1;
102
+ }
103
+ /**
104
+ * Infer a tool name from the preceding assistant(tool_calls) that declared
105
+ * this toolCallId. Deterministic walk — no side tables.
106
+ */
107
+ export function inferToolName(messages, toolIndex, toolCallId) {
108
+ if (!toolCallId)
109
+ return undefined;
110
+ for (let i = toolIndex - 1; i >= 0; i--) {
111
+ const m = messages[i];
112
+ if (m.role !== 'assistant' || !m.toolCalls)
113
+ continue;
114
+ const hit = m.toolCalls.find((c) => c.id === toolCallId);
115
+ if (hit)
116
+ return hit.name;
117
+ }
118
+ return undefined;
119
+ }
120
+ /**
121
+ * Project oversized / cold tool-result bodies onto addressable stubs.
122
+ *
123
+ * Invariants:
124
+ * - never mutates the input array or its messages
125
+ * - already-stubbed bodies are left byte-identical
126
+ * - same (messages, opts, lookup) → byte-identical output (cache-safe)
127
+ * - returns the SAME array reference when nothing changed
128
+ */
129
+ export function projectSessionSurface(messages, opts, lookup) {
130
+ const { hotTail, maxBodyChars } = resolveSurfaceLimits(opts);
131
+ const stats = { projected: 0, charsOmitted: 0, alreadyStubbed: 0 };
132
+ const toolPositions = [];
133
+ for (let i = 0; i < messages.length; i++) {
134
+ if (messages[i].role === 'tool')
135
+ toolPositions.push(i);
136
+ }
137
+ if (toolPositions.length === 0) {
138
+ return { messages: messages, stats };
139
+ }
140
+ const hotStart = Math.max(0, toolPositions.length - hotTail);
141
+ const protect = new Set(toolPositions.slice(hotStart));
142
+ let changed = false;
143
+ const out = messages.map((m, i) => {
144
+ if (m.role !== 'tool')
145
+ return m;
146
+ const body = m.content ?? '';
147
+ if (isObservationStub(body)) {
148
+ stats.alreadyStubbed += 1;
149
+ return m;
150
+ }
151
+ if (protect.has(i) || body.length <= maxBodyChars)
152
+ return m;
153
+ const toolOrdinal = toolPositions.indexOf(i);
154
+ const seq = resolveObservationSeq(m, toolOrdinal, lookup);
155
+ const stub = formatObservationStub({
156
+ seq,
157
+ tool: inferToolName(messages, i, m.toolCallId),
158
+ status: statusFromToolBody(body),
159
+ bytes: body.length,
160
+ });
161
+ changed = true;
162
+ stats.projected += 1;
163
+ stats.charsOmitted += Math.max(0, body.length - stub.length);
164
+ return { ...m, content: stub };
165
+ });
166
+ return {
167
+ messages: changed ? out : messages,
168
+ stats,
169
+ };
170
+ }
171
+ //# sourceMappingURL=sessionSurface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionSurface.js","sourceRoot":"","sources":["../../../src/cli/hooks/sessionSurface.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * sessionSurface — Fase 2 acceptance: deterministic projection, monotone
3
+ * stubs, byte-identical replay, no un-pruning.
4
+ */
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+ import { formatObservationStub, isObservationStub, parseObservationStub, projectSessionSurface, resolveObservationSeq, statusFromToolBody, } from './sessionSurface.js';
7
+ function tool(id, content) {
8
+ return { role: 'tool', toolCallId: id, content };
9
+ }
10
+ function assistantCall(id, name) {
11
+ return { role: 'assistant', content: '', toolCalls: [{ id, name, args: {} }] };
12
+ }
13
+ describe('observation stub codec', () => {
14
+ it('round-trips fields byte-identically', () => {
15
+ const stub = formatObservationStub({
16
+ seq: 12,
17
+ tool: 'grep_content',
18
+ status: 'complete',
19
+ bytes: 4096,
20
+ });
21
+ expect(stub).toBe('OBSERVATION ref=#12 tool=grep_content status=complete bytes=4096\n[retrieve_observation #12]');
22
+ expect(isObservationStub(stub)).toBe(true);
23
+ expect(parseObservationStub(stub)).toEqual({
24
+ seq: 12,
25
+ tool: 'grep_content',
26
+ status: 'complete',
27
+ bytes: 4096,
28
+ });
29
+ expect(formatObservationStub(parseObservationStub(stub))).toBe(stub);
30
+ });
31
+ it('rejects non-stubs', () => {
32
+ expect(isObservationStub('hello')).toBe(false);
33
+ expect(parseObservationStub('hello')).toBeNull();
34
+ });
35
+ it('reads Fase-0 footer status', () => {
36
+ expect(statusFromToolBody('body\n[observation status=partial filesWalked=3]')).toBe('partial');
37
+ expect(statusFromToolBody('plain')).toBeUndefined();
38
+ });
39
+ });
40
+ describe('projectSessionSurface', () => {
41
+ afterEach(() => {
42
+ delete process.env.ZELARI_SURFACE_HOT_TAIL;
43
+ delete process.env.ZELARI_SURFACE_MAX_BODY_CHARS;
44
+ delete process.env.ZELARI_SESSION_SURFACE;
45
+ });
46
+ it('returns the same reference when nothing needs projecting', () => {
47
+ const msgs = [
48
+ { role: 'user', content: 'hi' },
49
+ tool('c1', 'tiny'),
50
+ ];
51
+ const r = projectSessionSurface(msgs, { hotTail: 4, maxBodyChars: 4000 });
52
+ expect(r.messages).toBe(msgs);
53
+ expect(r.stats.projected).toBe(0);
54
+ });
55
+ it('keeps the hot tail full-fidelity and stubs older oversized bodies', () => {
56
+ const big = 'B'.repeat(500);
57
+ const msgs = [];
58
+ for (let i = 1; i <= 6; i++) {
59
+ msgs.push(assistantCall(`c${i}`, 'read_file'));
60
+ msgs.push(tool(`c${i}`, big + `\n[observation status=complete bytes=${big.length}]`));
61
+ }
62
+ const r = projectSessionSurface(msgs, { hotTail: 2, maxBodyChars: 100 });
63
+ expect(r.stats.projected).toBe(4);
64
+ const tools = r.messages.filter((m) => m.role === 'tool');
65
+ expect(tools.slice(0, 4).every((m) => isObservationStub(m.content))).toBe(true);
66
+ expect(tools.slice(4).every((m) => !isObservationStub(m.content))).toBe(true);
67
+ expect(tools[0].content).toContain('tool=read_file');
68
+ expect(tools[0].content).toContain('status=complete');
69
+ expect(tools[0].content).toContain('[retrieve_observation #1]');
70
+ });
71
+ it('is deterministic: same input → byte-identical surface', () => {
72
+ const big = 'X'.repeat(800);
73
+ const msgs = [
74
+ assistantCall('a', 'grep_content'),
75
+ tool('a', big),
76
+ assistantCall('b', 'grep_content'),
77
+ tool('b', big),
78
+ assistantCall('c', 'list_files'),
79
+ tool('c', big),
80
+ ];
81
+ const a = projectSessionSurface(msgs, { hotTail: 1, maxBodyChars: 50 });
82
+ const b = projectSessionSurface(msgs, { hotTail: 1, maxBodyChars: 50 });
83
+ expect(JSON.stringify(a.messages)).toBe(JSON.stringify(b.messages));
84
+ expect(a.stats).toEqual(b.stats);
85
+ });
86
+ it('never un-prunes an existing stub (monotone / cache-safe)', () => {
87
+ const stub = formatObservationStub({ seq: 7, tool: 'read_file', bytes: 9999 });
88
+ const msgs = [tool('old', stub), tool('hot', 'H'.repeat(50))];
89
+ const r = projectSessionSurface(msgs, { hotTail: 4, maxBodyChars: 10 });
90
+ expect(r.messages[0].content).toBe(stub);
91
+ expect(r.stats.alreadyStubbed).toBe(1);
92
+ expect(r.stats.projected).toBe(0);
93
+ });
94
+ it('uses lookup(toolCallId) for stable seq when provided', () => {
95
+ const big = 'Z'.repeat(200);
96
+ const msgs = [
97
+ assistantCall('call-z', 'bash'),
98
+ tool('call-z', big),
99
+ assistantCall('keep', 'bash'),
100
+ tool('keep', big),
101
+ ];
102
+ const lookup = (id) => (id === 'call-z' ? 42 : undefined);
103
+ const r = projectSessionSurface(msgs, { hotTail: 1, maxBodyChars: 10 }, lookup);
104
+ expect(r.messages[1].content).toContain('ref=#42');
105
+ expect(r.messages[1].content).toContain('[retrieve_observation #42]');
106
+ });
107
+ it('falls back to 1-based ordinal when lookup misses', () => {
108
+ const seq = resolveObservationSeq(tool('x', 'body'), 3);
109
+ expect(seq).toBe(4);
110
+ });
111
+ });
112
+ //# sourceMappingURL=sessionSurface.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionSurface.test.js","sourceRoot":"","sources":["../../../src/cli/hooks/sessionSurface.test.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEzD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAE7B,SAAS,IAAI,CAAC,EAAU,EAAE,OAAe;IACvC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC;AACnD,CAAC;AACD,SAAS,aAAa,CAAC,EAAU,EAAE,IAAY;IAC7C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,IAAI,GAAG,qBAAqB,CAAC;YACjC,GAAG,EAAE,EAAE;YACP,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,UAAU;YAClB,KAAK,EAAE,IAAI;SACZ,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CACf,8FAA8F,CAC/F,CAAC;QACF,MAAM,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;YACzC,GAAG,EAAE,EAAE;YACP,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,UAAU;YAClB,KAAK,EAAE,IAAI;SACZ,CAAC,CAAC;QACH,MAAM,CAAC,qBAAqB,CAAC,oBAAoB,CAAC,IAAI,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,MAAM,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/C,MAAM,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,kBAAkB,CAAC,kDAAkD,CAAC,CAAC,CAAC,IAAI,CACjF,SAAS,CACV,CAAC;QACF,MAAM,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;QAC3C,OAAO,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC;QACjD,OAAO,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,IAAI,GAAmB;YAC3B,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;YAC/B,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;SACnB,CAAC;QACF,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1E,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAmB,EAAE,CAAC;QAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC;YAC/C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,GAAG,wCAAwC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACxF,CAAC;QACD,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;QACzE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,KAAK,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChF,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9E,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QACrD,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QACtD,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAmB;YAC3B,aAAa,CAAC,GAAG,EAAE,cAAc,CAAC;YAClC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC;YACd,aAAa,CAAC,GAAG,EAAE,cAAc,CAAC;YAClC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC;YACd,aAAa,CAAC,GAAG,EAAE,YAAY,CAAC;YAChC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC;SACf,CAAC;QACF,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,IAAI,GAAG,qBAAqB,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/E,MAAM,IAAI,GAAmB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9E,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAmB;YAC3B,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC;YAC/B,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC;YACnB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC;YAC7B,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;SAClB,CAAC;QACF,MAAM,MAAM,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAClE,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;QAChF,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACnD,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,GAAG,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QACxD,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -2,6 +2,7 @@
2
2
  // from app.tsx. Runtime is correct; tighten signatures in a follow-up.
3
3
  import { useState, useRef, useCallback } from "react";
4
4
  import { AgentHarness } from "@zelari/core/harness";
5
+ import { ingestLiveEvent } from "./observationStore.js";
5
6
  import { getMetricsLogger } from "../metrics.js";
6
7
  import { calculateCost } from "../modelPricing.js";
7
8
  import { providerFromEnv, providerConfigFor, resolveActiveProvider, } from "../provider/openai-compatible.js";
@@ -538,6 +539,8 @@ export function useChatTurn(params) {
538
539
  // matching start event (keyed by toolCallId) for metrics.
539
540
  const toolNameById = new Map();
540
541
  const metrics = getMetricsLogger();
542
+ // Fase M: last log-only context_metrics event (arrives before agent_end).
543
+ let lastGrowth = null;
541
544
  let realUsage = null;
542
545
  try {
543
546
  for await (const event of harness.run()) {
@@ -580,6 +583,11 @@ export function useChatTurn(params) {
580
583
  // to serialize the render loop with mkdir+appendFile per delta.
581
584
  // Turn boundaries below call flush() so the tail is durable.
582
585
  void writerRef.current.append(event);
586
+ if (sessionId)
587
+ ingestLiveEvent(sessionId, event);
588
+ }
589
+ if (event.type === "context_metrics") {
590
+ lastGrowth = event;
583
591
  }
584
592
  if (event.type === "agent_end") {
585
593
  metrics.record({
@@ -589,6 +597,17 @@ export function useChatTurn(params) {
589
597
  model: envConfig.model,
590
598
  latencyMs: event.durationMs,
591
599
  ok: event.reason === "stop",
600
+ // Fase M: context-growth counters (log-only event, folded here).
601
+ ...(lastGrowth
602
+ ? {
603
+ toolRoundTrips: lastGrowth.toolRoundTrips,
604
+ intermediateToolBytes: lastGrowth.intermediateToolBytes,
605
+ requests: lastGrowth.requests,
606
+ historyBytesAtRequest: lastGrowth.historyBytesLast,
607
+ historyBytesPeak: lastGrowth.historyBytesPeak,
608
+ cacheHitTokens: lastGrowth.cacheHitTokens,
609
+ }
610
+ : {}),
592
611
  // v1.35: real usage landed on message_end (which precedes
593
612
  // agent_end), so historical spend can be aggregated from
594
613
  // metrics.jsonl — previously these fields were always absent.
@@ -1173,12 +1192,16 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
1173
1192
  // Drain remaining events silently after the abort decision.
1174
1193
  if (writerRef.current)
1175
1194
  void writerRef.current.append(event);
1195
+ if (sessionId)
1196
+ ingestLiveEvent(sessionId, event);
1176
1197
  continue;
1177
1198
  }
1178
1199
  if (writerRef.current) {
1179
1200
  // Same batching as the single-agent path: fire-and-forget per event,
1180
1201
  // flush at the turn boundary in `finally`.
1181
1202
  void writerRef.current.append(event);
1203
+ if (sessionId)
1204
+ ingestLiveEvent(sessionId, event);
1182
1205
  }
1183
1206
  if (event.type === "council_mode") {
1184
1207
  councilRunMode = event.runMode;
@@ -1682,6 +1705,8 @@ async function runZelariMissionInTui(userMessage, deps, emit) {
1682
1705
  onEvent: async (event) => {
1683
1706
  if (writerRef.current)
1684
1707
  await writerRef.current.append(event);
1708
+ if (sessionId)
1709
+ ingestLiveEvent(sessionId, event);
1685
1710
  if (event.type === "tool_execution_start") {
1686
1711
  const name = event.toolName ?? "tool";
1687
1712
  appendSystem(setMessages, `[build@kraken] → ${name}`, Date.now());