@bahulam/code 0.1.8 → 0.1.10
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/package.json +2 -2
- package/src/core/approval.mjs +1 -1
- package/src/core/local-store.mjs +171 -23
- package/src/core/tool-executor.mjs +7 -3
- package/src/terminal/analytics.mjs +4 -1
- package/src/terminal/repl-render.mjs +24 -2
- package/src/terminal/repl-resume.mjs +1 -1
- package/src/terminal/repl.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bahulam/code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Bahulam Code — abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"gemini"
|
|
41
41
|
],
|
|
42
42
|
"license": "Apache-2.0",
|
|
43
|
-
"author": "Bahulam <
|
|
43
|
+
"author": "Bahulam <support@bahulam.ai>",
|
|
44
44
|
"homepage": "https://bahulam.ai/code",
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
package/src/core/approval.mjs
CHANGED
|
@@ -113,7 +113,7 @@ function shellHardBlockReason(tool, args = {}) {
|
|
|
113
113
|
|
|
114
114
|
function withShellRetryHint(reason) {
|
|
115
115
|
const text = String(reason || '').trim();
|
|
116
|
-
if (/command substitution|backticks|\$\(
|
|
116
|
+
if (/command substitution|backticks|\$\(/i.test(text)) {
|
|
117
117
|
return `${text}. Retry with separate simple shell commands instead of backticks or $().`;
|
|
118
118
|
}
|
|
119
119
|
return text;
|
package/src/core/local-store.mjs
CHANGED
|
@@ -12,6 +12,100 @@ import { bahulamHome } from './paths.mjs';
|
|
|
12
12
|
|
|
13
13
|
const KEPLER_DIR = bahulamHome();
|
|
14
14
|
const PROJECTS_DIR = path.join(KEPLER_DIR, 'projects');
|
|
15
|
+
const REPLAY_EVENT_RECORD_TYPES = new Set(['bahulam_event', 'kepler_event']);
|
|
16
|
+
|
|
17
|
+
function finiteNumber(value) {
|
|
18
|
+
const n = Number(value);
|
|
19
|
+
return Number.isFinite(n) ? n : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function firstFiniteNumber(...values) {
|
|
23
|
+
for (const value of values) {
|
|
24
|
+
const n = finiteNumber(value);
|
|
25
|
+
if (n !== null) return n;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function replayEventFromRecord(record) {
|
|
31
|
+
if (!record || !REPLAY_EVENT_RECORD_TYPES.has(record.type) || !record.event) return null;
|
|
32
|
+
const event = record.event;
|
|
33
|
+
if (!event || typeof event !== 'object' || !event.type) return null;
|
|
34
|
+
return {
|
|
35
|
+
...event,
|
|
36
|
+
data: event.data && typeof event.data === 'object' ? event.data : {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function eventUsage(event) {
|
|
41
|
+
const data = event?.data && typeof event.data === 'object' ? event.data : {};
|
|
42
|
+
const usage = data.usage && typeof data.usage === 'object' ? data.usage : null;
|
|
43
|
+
if (!usage) return null;
|
|
44
|
+
return usage;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function usageTotals(usage = {}) {
|
|
48
|
+
return {
|
|
49
|
+
inputTokens: firstFiniteNumber(usage.total_input_tokens, usage.input_tokens, usage.prompt_tokens),
|
|
50
|
+
outputTokens: firstFiniteNumber(usage.total_output_tokens, usage.output_tokens, usage.completion_tokens),
|
|
51
|
+
cacheReadTokens: firstFiniteNumber(usage.cache_read_input_tokens, usage.cache_read_tokens, usage.cache_read),
|
|
52
|
+
cacheCreationTokens: firstFiniteNumber(usage.cache_creation_input_tokens, usage.cache_creation_tokens, usage.cache_creation),
|
|
53
|
+
reasoningTokens: firstFiniteNumber(usage.reasoning_tokens),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function addModelUsage(meta, modelSet, usage = {}) {
|
|
58
|
+
if (!Array.isArray(usage.models)) return;
|
|
59
|
+
for (const item of usage.models) {
|
|
60
|
+
const model = typeof item === 'string' ? item : item?.model;
|
|
61
|
+
if (typeof model === 'string' && model) modelSet.add(model);
|
|
62
|
+
if (!model || typeof item !== 'object') continue;
|
|
63
|
+
if (!meta.modelUsage[model]) {
|
|
64
|
+
meta.modelUsage[model] = {
|
|
65
|
+
inputTokens: 0,
|
|
66
|
+
outputTokens: 0,
|
|
67
|
+
cacheReadTokens: 0,
|
|
68
|
+
cacheCreationTokens: 0,
|
|
69
|
+
reasoningTokens: 0,
|
|
70
|
+
costUsd: 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const totals = usageTotals(item);
|
|
74
|
+
meta.modelUsage[model].inputTokens += totals.inputTokens;
|
|
75
|
+
meta.modelUsage[model].outputTokens += totals.outputTokens;
|
|
76
|
+
meta.modelUsage[model].cacheReadTokens += totals.cacheReadTokens;
|
|
77
|
+
meta.modelUsage[model].cacheCreationTokens += totals.cacheCreationTokens;
|
|
78
|
+
meta.modelUsage[model].reasoningTokens += totals.reasoningTokens;
|
|
79
|
+
meta.modelUsage[model].costUsd += firstFiniteNumber(item.cost_usd, item.cost);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function addUsageTotals(meta, modelSet, usage = {}) {
|
|
84
|
+
const totals = usageTotals(usage);
|
|
85
|
+
meta.inputTokens += totals.inputTokens;
|
|
86
|
+
meta.outputTokens += totals.outputTokens;
|
|
87
|
+
meta.cacheReadTokens += totals.cacheReadTokens;
|
|
88
|
+
meta.cacheCreationTokens += totals.cacheCreationTokens;
|
|
89
|
+
meta.reasoningTokens += totals.reasoningTokens;
|
|
90
|
+
addModelUsage(meta, modelSet, usage);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assistantHasMatchingComplete(assistantRecord, completeRecords) {
|
|
94
|
+
return completeRecords.some((record) => {
|
|
95
|
+
const distance = Math.abs(Number(assistantRecord.order) - Number(record.order));
|
|
96
|
+
return distance > 0 && distance <= 3;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function applyUsageRecords(meta, modelSet, records) {
|
|
101
|
+
const completeRecords = records.filter(record => record.source === 'complete');
|
|
102
|
+
for (const record of completeRecords) addUsageTotals(meta, modelSet, record.usage);
|
|
103
|
+
for (const record of records) {
|
|
104
|
+
if (record.source !== 'assistant') continue;
|
|
105
|
+
if (assistantHasMatchingComplete(record, completeRecords)) continue;
|
|
106
|
+
addUsageTotals(meta, modelSet, record.usage);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
15
109
|
|
|
16
110
|
function normalizeBlock(block) {
|
|
17
111
|
if (!block || typeof block !== 'object') {
|
|
@@ -188,8 +282,10 @@ async function parseSessionMeta(filePath) {
|
|
|
188
282
|
outputTokens: 0,
|
|
189
283
|
cacheReadTokens: 0,
|
|
190
284
|
cacheCreationTokens: 0,
|
|
285
|
+
reasoningTokens: 0,
|
|
191
286
|
toolCalls: [], // [{name, count}]
|
|
192
287
|
models: [], // [model strings]
|
|
288
|
+
modelUsage: {}, // model -> token/cost totals from complete events
|
|
193
289
|
modelLimits: {}, // role -> {model, context_length, max_output, source}
|
|
194
290
|
subAgentModels: {}, // role -> model from backend session_info
|
|
195
291
|
startTime: null,
|
|
@@ -207,6 +303,7 @@ async function parseSessionMeta(filePath) {
|
|
|
207
303
|
|
|
208
304
|
const toolCounts = {};
|
|
209
305
|
const modelSet = new Set();
|
|
306
|
+
const usageRecords = [];
|
|
210
307
|
|
|
211
308
|
// endStatus tracking
|
|
212
309
|
let lastMessageRole = null;
|
|
@@ -218,8 +315,10 @@ async function parseSessionMeta(filePath) {
|
|
|
218
315
|
|
|
219
316
|
const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
220
317
|
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
318
|
+
let lineOrder = 0;
|
|
221
319
|
|
|
222
320
|
for await (const line of rl) {
|
|
321
|
+
const recordOrder = lineOrder++;
|
|
223
322
|
if (!line.trim()) continue;
|
|
224
323
|
let obj;
|
|
225
324
|
try { obj = JSON.parse(line); }
|
|
@@ -235,13 +334,30 @@ async function parseSessionMeta(filePath) {
|
|
|
235
334
|
if (!meta.endTime || ts > meta.endTime) meta.endTime = ts;
|
|
236
335
|
}
|
|
237
336
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
337
|
+
// Bahulam replay events may carry cost / error markers. Older local
|
|
338
|
+
// transcripts used the same payload under the legacy kepler_event type.
|
|
339
|
+
const ev = replayEventFromRecord(obj);
|
|
340
|
+
if (ev) {
|
|
341
|
+
const data = ev.data || {};
|
|
342
|
+
if (ev.type === 'complete') {
|
|
343
|
+
const usage = eventUsage(ev);
|
|
344
|
+
if (usage) {
|
|
345
|
+
usageRecords.push({ source: 'complete', order: recordOrder, usage });
|
|
346
|
+
}
|
|
347
|
+
const eventCost = firstFiniteNumber(
|
|
348
|
+
data.cost_usd,
|
|
349
|
+
data.total_cost_usd,
|
|
350
|
+
data.total_cost,
|
|
351
|
+
data.usage?.total_cost_usd,
|
|
352
|
+
data.usage?.total_cost,
|
|
353
|
+
data.usage?.cost,
|
|
354
|
+
ev.cost_usd,
|
|
355
|
+
);
|
|
356
|
+
if (eventCost) meta.costUsd += eventCost;
|
|
357
|
+
}
|
|
242
358
|
if (ev.type === 'session_info') {
|
|
243
|
-
if (typeof
|
|
244
|
-
const info =
|
|
359
|
+
if (typeof data.total_cost_usd === 'number') meta.costUsd = data.total_cost_usd;
|
|
360
|
+
const info = data;
|
|
245
361
|
if (info.model_limits && typeof info.model_limits === 'object') {
|
|
246
362
|
meta.modelLimits = info.model_limits;
|
|
247
363
|
}
|
|
@@ -255,15 +371,15 @@ async function parseSessionMeta(filePath) {
|
|
|
255
371
|
}
|
|
256
372
|
}
|
|
257
373
|
if (ev.type === 'error' || ev.error === true) hadError = true;
|
|
258
|
-
if (ev.type === 'resume_summary' && typeof
|
|
374
|
+
if (ev.type === 'resume_summary' && typeof data.summary === 'string') {
|
|
259
375
|
meta.resumeSummary = {
|
|
260
|
-
sourceMessageCount: Number(
|
|
261
|
-
previousSourceMessageCount: Number(
|
|
262
|
-
fullMessageCount: Number(
|
|
263
|
-
summaryChars:
|
|
264
|
-
summarySource:
|
|
265
|
-
mode:
|
|
266
|
-
modeLabel:
|
|
376
|
+
sourceMessageCount: Number(data.source_message_count) || 0,
|
|
377
|
+
previousSourceMessageCount: Number(data.previous_source_message_count) || 0,
|
|
378
|
+
fullMessageCount: Number(data.full_message_count) || 0,
|
|
379
|
+
summaryChars: data.summary.length,
|
|
380
|
+
summarySource: data.summary_source || '',
|
|
381
|
+
mode: data.mode || '',
|
|
382
|
+
modeLabel: data.mode_label || '',
|
|
267
383
|
timestamp: obj.timestamp || null,
|
|
268
384
|
};
|
|
269
385
|
}
|
|
@@ -296,10 +412,7 @@ async function parseSessionMeta(filePath) {
|
|
|
296
412
|
lastMessageRole = 'assistant';
|
|
297
413
|
const usage = obj.message?.usage;
|
|
298
414
|
if (usage) {
|
|
299
|
-
|
|
300
|
-
meta.outputTokens += usage.output_tokens || 0;
|
|
301
|
-
meta.cacheReadTokens += usage.cache_read_input_tokens || 0;
|
|
302
|
-
meta.cacheCreationTokens += usage.cache_creation_input_tokens || 0;
|
|
415
|
+
usageRecords.push({ source: 'assistant', order: recordOrder, usage });
|
|
303
416
|
}
|
|
304
417
|
const model = obj.message?.model;
|
|
305
418
|
if (model) modelSet.add(model);
|
|
@@ -322,6 +435,8 @@ async function parseSessionMeta(filePath) {
|
|
|
322
435
|
.map(([name, count]) => ({ name, count }))
|
|
323
436
|
.sort((a, b) => b.count - a.count);
|
|
324
437
|
meta.models = [...modelSet];
|
|
438
|
+
applyUsageRecords(meta, modelSet, usageRecords);
|
|
439
|
+
meta.models = [...modelSet];
|
|
325
440
|
|
|
326
441
|
// Projected context size for resume should estimate the serialized payload,
|
|
327
442
|
// not cumulative provider usage. Provider input tokens are charged per turn
|
|
@@ -393,11 +508,12 @@ export async function getSessionDetail(sessionId, options = {}) {
|
|
|
393
508
|
}
|
|
394
509
|
const entryOrder = order++;
|
|
395
510
|
|
|
396
|
-
|
|
511
|
+
const replayEvent = replayEventFromRecord(obj);
|
|
512
|
+
if (replayEvent) {
|
|
397
513
|
replayEvents.push({
|
|
398
514
|
order: entryOrder,
|
|
399
515
|
timestamp: obj.timestamp || null,
|
|
400
|
-
event:
|
|
516
|
+
event: replayEvent,
|
|
401
517
|
});
|
|
402
518
|
continue;
|
|
403
519
|
}
|
|
@@ -765,6 +881,7 @@ export async function getSessionStats(days = 30) {
|
|
|
765
881
|
totalInputTokens: 0,
|
|
766
882
|
totalOutputTokens: 0,
|
|
767
883
|
totalCacheReadTokens: 0,
|
|
884
|
+
totalReasoningTokens: 0,
|
|
768
885
|
totalToolCalls: 0,
|
|
769
886
|
toolBreakdown: {},
|
|
770
887
|
modelBreakdown: {},
|
|
@@ -777,13 +894,44 @@ export async function getSessionStats(days = 30) {
|
|
|
777
894
|
stats.totalInputTokens += meta.inputTokens;
|
|
778
895
|
stats.totalOutputTokens += meta.outputTokens;
|
|
779
896
|
stats.totalCacheReadTokens += meta.cacheReadTokens;
|
|
897
|
+
stats.totalReasoningTokens += meta.reasoningTokens;
|
|
780
898
|
|
|
781
899
|
for (const tc of meta.toolCalls) {
|
|
782
900
|
stats.toolBreakdown[tc.name] = (stats.toolBreakdown[tc.name] || 0) + tc.count;
|
|
783
901
|
stats.totalToolCalls += tc.count;
|
|
784
902
|
}
|
|
785
903
|
for (const model of meta.models) {
|
|
786
|
-
|
|
904
|
+
if (!stats.modelBreakdown[model]) {
|
|
905
|
+
stats.modelBreakdown[model] = {
|
|
906
|
+
sessions: 0,
|
|
907
|
+
inputTokens: 0,
|
|
908
|
+
outputTokens: 0,
|
|
909
|
+
cacheReadTokens: 0,
|
|
910
|
+
cacheCreationTokens: 0,
|
|
911
|
+
reasoningTokens: 0,
|
|
912
|
+
costUsd: 0,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
stats.modelBreakdown[model].sessions += 1;
|
|
916
|
+
}
|
|
917
|
+
for (const [model, usage] of Object.entries(meta.modelUsage || {})) {
|
|
918
|
+
if (!stats.modelBreakdown[model]) {
|
|
919
|
+
stats.modelBreakdown[model] = {
|
|
920
|
+
sessions: 0,
|
|
921
|
+
inputTokens: 0,
|
|
922
|
+
outputTokens: 0,
|
|
923
|
+
cacheReadTokens: 0,
|
|
924
|
+
cacheCreationTokens: 0,
|
|
925
|
+
reasoningTokens: 0,
|
|
926
|
+
costUsd: 0,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
stats.modelBreakdown[model].inputTokens += usage.inputTokens || 0;
|
|
930
|
+
stats.modelBreakdown[model].outputTokens += usage.outputTokens || 0;
|
|
931
|
+
stats.modelBreakdown[model].cacheReadTokens += usage.cacheReadTokens || 0;
|
|
932
|
+
stats.modelBreakdown[model].cacheCreationTokens += usage.cacheCreationTokens || 0;
|
|
933
|
+
stats.modelBreakdown[model].reasoningTokens += usage.reasoningTokens || 0;
|
|
934
|
+
stats.modelBreakdown[model].costUsd += usage.costUsd || 0;
|
|
787
935
|
}
|
|
788
936
|
}
|
|
789
937
|
|
|
@@ -808,8 +956,8 @@ export async function getToolBreakdown(days = 30) {
|
|
|
808
956
|
export async function getModelBreakdown(days = 30) {
|
|
809
957
|
const stats = await getSessionStats(days);
|
|
810
958
|
return Object.entries(stats.modelBreakdown)
|
|
811
|
-
.map(([model,
|
|
812
|
-
.sort((a, b) => b.sessions - a.sessions);
|
|
959
|
+
.map(([model, usage]) => ({ model, ...usage }))
|
|
960
|
+
.sort((a, b) => b.sessions - a.sessions || (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens));
|
|
813
961
|
}
|
|
814
962
|
|
|
815
963
|
/**
|
|
@@ -139,7 +139,7 @@ export function createToolExecutor({
|
|
|
139
139
|
|
|
140
140
|
function blockedShellOutput(reason) {
|
|
141
141
|
const text = String(reason || 'Blocked by shell safety policy').trim();
|
|
142
|
-
const hint = /command substitution|backticks|\$\(
|
|
142
|
+
const hint = /command substitution|backticks|\$\(/i.test(text)
|
|
143
143
|
? 'Retry with separate simple shell commands instead of backticks or $().'
|
|
144
144
|
: 'Work only inside a registered project root.';
|
|
145
145
|
return `BLOCKED: ${text}. ${hint}`;
|
|
@@ -1296,9 +1296,13 @@ export function createToolExecutor({
|
|
|
1296
1296
|
const after = readTextIfExists(filePath);
|
|
1297
1297
|
if (wrapped.success !== false && before === after) {
|
|
1298
1298
|
const relativePath = path.relative(projectRootFor(filePath), filePath) || path.basename(filePath);
|
|
1299
|
+
// File content is unchanged — the desired state is already in place.
|
|
1300
|
+
// Return success so the agent doesn't treat this as a failure and
|
|
1301
|
+
// loop into repeated read_file calls trying to diagnose why it failed.
|
|
1302
|
+
// _no_change flags this for stagnation detection on repeated no-op edits.
|
|
1299
1303
|
return {
|
|
1300
|
-
success:
|
|
1301
|
-
output: `edit_file
|
|
1304
|
+
success: true,
|
|
1305
|
+
output: `edit_file: no changes made to ${relativePath} — content already matches or replacement is identical to the original.`,
|
|
1302
1306
|
_tool: 'edit_file',
|
|
1303
1307
|
_no_change: true,
|
|
1304
1308
|
no_change: true,
|
|
@@ -103,6 +103,7 @@ export function formatStatsReport(stats, tools, models, days, paths) {
|
|
|
103
103
|
lines.push(`Messages ${formatNumber(stats.totalUserMessages + stats.totalAssistantMessages)} (${formatNumber(stats.totalUserMessages)} user, ${formatNumber(stats.totalAssistantMessages)} assistant)`);
|
|
104
104
|
lines.push(`Tokens ${formatNumber(stats.totalInputTokens + stats.totalOutputTokens)} (${formatNumber(stats.totalInputTokens)} in, ${formatNumber(stats.totalOutputTokens)} out)`);
|
|
105
105
|
lines.push(`Cache Read ${formatNumber(stats.totalCacheReadTokens)}`);
|
|
106
|
+
lines.push(`Reasoning ${formatNumber(stats.totalReasoningTokens)}`);
|
|
106
107
|
lines.push(`Tool Calls ${formatNumber(stats.totalToolCalls)}`);
|
|
107
108
|
lines.push('');
|
|
108
109
|
|
|
@@ -121,7 +122,9 @@ export function formatStatsReport(stats, tools, models, days, paths) {
|
|
|
121
122
|
lines.push(' none');
|
|
122
123
|
} else {
|
|
123
124
|
for (const model of models.slice(0, 8)) {
|
|
124
|
-
|
|
125
|
+
const tokens = (model.inputTokens || 0) + (model.outputTokens || 0);
|
|
126
|
+
const tokenText = tokens ? ` ${formatNumber(tokens)} tok` : '';
|
|
127
|
+
lines.push(` ${truncate(model.model, 42).padEnd(42)} ${formatNumber(model.sessions)} sessions${tokenText}`);
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
lines.push('');
|
|
@@ -840,16 +840,38 @@ export function transcriptRenderableLines(rendered) {
|
|
|
840
840
|
return lines;
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
function positiveInteger(value) {
|
|
844
|
+
const n = Number(value);
|
|
845
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
export function stagnationDisplayCount(data = {}, reason = '') {
|
|
849
|
+
const text = String(reason || data?.reason || data?.message || '');
|
|
850
|
+
const patterns = [
|
|
851
|
+
/\bcalled\s+(\d+)\s+times\b/i,
|
|
852
|
+
/[×x]\s*(\d+)\b/i,
|
|
853
|
+
/\brepeated\s+(\d+)x\b/i,
|
|
854
|
+
/\b(\d+)\s+times\s+without\s+mutation\b/i,
|
|
855
|
+
/\b(\d+)\s+tool\s+calls\s+without\s+mutating\s+state\b/i,
|
|
856
|
+
];
|
|
857
|
+
for (const pattern of patterns) {
|
|
858
|
+
const match = text.match(pattern);
|
|
859
|
+
const count = positiveInteger(match?.[1]);
|
|
860
|
+
if (count) return count;
|
|
861
|
+
}
|
|
862
|
+
return positiveInteger(data?.count) || positiveInteger(data?.repeat_count);
|
|
863
|
+
}
|
|
864
|
+
|
|
843
865
|
export function renderStagnation(data = {}) {
|
|
844
866
|
const rawMessage = data?.message || '';
|
|
845
867
|
const reason = data?.reason || rawMessage.replace(/^Stagnation:\s*/i, '').trim();
|
|
846
868
|
const tool = data?.tool || data?.tool_name || '';
|
|
847
|
-
const count = data
|
|
869
|
+
const count = stagnationDisplayCount(data, reason);
|
|
848
870
|
// Try to extract a target/path from the reason so we can show a
|
|
849
871
|
// compact one-liner. Reason shapes we know about from the framework:
|
|
850
872
|
// "Repeated overlapping <tool> inspections of '<target>' N times without mutation"
|
|
851
873
|
// "..." (fallback: use reason as-is, trimmed to ~80 chars)
|
|
852
|
-
const targetMatch = reason.match(/of\s+['"]([^'"]+)['"]/);
|
|
874
|
+
const targetMatch = reason.match(/(?:of|on)\s+['"]([^'"]+)['"]/);
|
|
853
875
|
const target = targetMatch ? targetMatch[1] : '';
|
|
854
876
|
|
|
855
877
|
// Compose a compact single-line message:
|
|
@@ -605,7 +605,7 @@ export async function compactCurrentSession(ctx, rest = '') {
|
|
|
605
605
|
|
|
606
606
|
if (ctx.jsonlWriter) {
|
|
607
607
|
progress.update('writing summary checkpoint', 88);
|
|
608
|
-
ctx.jsonlWriter.
|
|
608
|
+
ctx.jsonlWriter.writeBahulamEvent({
|
|
609
609
|
type: 'resume_summary',
|
|
610
610
|
data: {
|
|
611
611
|
session_id: session.id || null,
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -3684,7 +3684,7 @@ async function handleCommand(input, ctx) {
|
|
|
3684
3684
|
}
|
|
3685
3685
|
|
|
3686
3686
|
// 5. Show continuity context. Non-summary modes use the captured
|
|
3687
|
-
//
|
|
3687
|
+
// bahulam_event stream when available so the terminal replay matches
|
|
3688
3688
|
// the original styled interaction; older sessions fall back to
|
|
3689
3689
|
// reconstructed text.
|
|
3690
3690
|
if (mode === 'summary' && resumed.summary) {
|