@bahulam/code 0.1.7 → 0.1.9

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 (49) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +1 -1
  4. package/src/commands/agent.mjs +3 -3
  5. package/src/commands/device.mjs +2 -2
  6. package/src/commands/pair.mjs +2 -2
  7. package/src/commands/remote.mjs +3 -3
  8. package/src/commands/workflow.mjs +5 -5
  9. package/src/config/env.mjs +9 -2
  10. package/src/config/memory-loader.mjs +1 -1
  11. package/src/config/settings.mjs +2 -2
  12. package/src/core/agent-loop.mjs +17 -0
  13. package/src/core/approval.mjs +1 -1
  14. package/src/core/attachments.mjs +1 -1
  15. package/src/core/backend-url.mjs +11 -12
  16. package/src/core/callback-client.mjs +1 -1
  17. package/src/core/headless.mjs +4 -4
  18. package/src/core/jsonl-writer.mjs +15 -15
  19. package/src/core/local-agent.mjs +26 -5
  20. package/src/core/local-store.mjs +172 -24
  21. package/src/core/mode-selector.mjs +1 -1
  22. package/src/core/output-filter.mjs +1 -1
  23. package/src/core/project-artifacts.mjs +3 -3
  24. package/src/core/project-context-loader.mjs +9 -9
  25. package/src/core/settings-sync.mjs +1 -1
  26. package/src/core/stagnation.mjs +61 -1
  27. package/src/core/stream-client.mjs +4 -4
  28. package/src/core/system-prompt.mjs +2 -2
  29. package/src/core/tool-executor.mjs +79 -61
  30. package/src/local-service/agent-relay.mjs +8 -8
  31. package/src/local-service/server.mjs +2 -2
  32. package/src/mcp/client.mjs +4 -4
  33. package/src/onboarding/preflight.mjs +1 -1
  34. package/src/terminal/agents.mjs +2 -2
  35. package/src/terminal/analytics.mjs +4 -1
  36. package/src/terminal/main.mjs +6 -6
  37. package/src/terminal/repl-render.mjs +26 -4
  38. package/src/terminal/repl-resume.mjs +3 -3
  39. package/src/terminal/repl.mjs +18 -18
  40. package/src/terminal/tool-display.mjs +1 -1
  41. package/src/tools/agent.mjs +2 -2
  42. package/src/tools/analyze-image.mjs +2 -2
  43. package/src/tools/generate-image.mjs +4 -4
  44. package/src/tools/project-overview.mjs +12 -12
  45. package/src/ui/formatter.mjs +2 -2
  46. package/src/ui/input-dock.mjs +7 -7
  47. package/src/ui/spinner.mjs +1 -1
  48. package/src/ui/term.mjs +2 -6
  49. package/src/ui/tool-card.mjs +74 -3
@@ -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
- // Backend kepler_event payloads may carry cost / error markers.
239
- if (obj.type === 'kepler_event' && obj.event) {
240
- const ev = obj.event;
241
- if (ev.type === 'complete' && typeof ev.cost_usd === 'number') meta.costUsd += ev.cost_usd;
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 ev.total_cost_usd === 'number') meta.costUsd = ev.total_cost_usd;
244
- const info = ev.data || ev;
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 ev.data?.summary === 'string') {
374
+ if (ev.type === 'resume_summary' && typeof data.summary === 'string') {
259
375
  meta.resumeSummary = {
260
- sourceMessageCount: Number(ev.data.source_message_count) || 0,
261
- previousSourceMessageCount: Number(ev.data.previous_source_message_count) || 0,
262
- fullMessageCount: Number(ev.data.full_message_count) || 0,
263
- summaryChars: ev.data.summary.length,
264
- summarySource: ev.data.summary_source || '',
265
- mode: ev.data.mode || '',
266
- modeLabel: ev.data.mode_label || '',
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
- meta.inputTokens += usage.input_tokens || 0;
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
- if (obj.type === 'kepler_event' && obj.event?.type) {
511
+ const replayEvent = replayEventFromRecord(obj);
512
+ if (replayEvent) {
397
513
  replayEvents.push({
398
514
  order: entryOrder,
399
515
  timestamp: obj.timestamp || null,
400
- event: obj.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
- stats.modelBreakdown[model] = (stats.modelBreakdown[model] || 0) + 1;
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, sessions]) => ({ model, sessions }))
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
  /**
@@ -833,7 +981,7 @@ export function getHistory(n = 50) {
833
981
 
834
982
  export function getStorePaths() {
835
983
  return {
836
- keplerDir: KEPLER_DIR,
984
+ bahulamDir: KEPLER_DIR,
837
985
  projectsDir: PROJECTS_DIR,
838
986
  historyPath: path.join(KEPLER_DIR, 'history.jsonl'),
839
987
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Mode Selector
3
3
  *
4
- * remote (default): All requests go to Tarang backend.
4
+ * remote (default): All requests go to Bahulam backend.
5
5
  * Backend handles orchestration, model selection, tool routing.
6
6
  * User's provider and models configured via web Settings page.
7
7
  *
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Output Filter — Smart shell output filtering + auto-lint.
3
- * Ported from tarang-cli (Python) ws/executor.py with enhanced patterns.
3
+ * Ported from tarang-cli (Python, legacy) ws/executor.py with enhanced patterns.
4
4
  */
5
5
 
6
6
  import * as fs from 'node:fs';
@@ -23,9 +23,9 @@ export function persistProjectArtifacts(data, resources, log = () => {}) {
23
23
  for (const [field, filename] of artifacts) {
24
24
  try {
25
25
  // Resolver honors legacy .kepler/ when it's the only dir that exists.
26
- const keplerDir = projectConfigDir(resource.root);
27
- fs.mkdirSync(keplerDir, { recursive: true });
28
- const artifactPath = path.join(keplerDir, filename);
26
+ const bahulamDir = projectConfigDir(resource.root);
27
+ fs.mkdirSync(bahulamDir, { recursive: true });
28
+ const artifactPath = path.join(bahulamDir, filename);
29
29
  fs.writeFileSync(artifactPath, data[field], 'utf-8');
30
30
  resource[field] = data[field];
31
31
  written.push(artifactPath);
@@ -1,7 +1,7 @@
1
1
  import * as crypto from 'node:crypto';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
- import { loadKeplerMemory } from '../config/memory-loader.mjs';
4
+ import { loadBahulamMemory } from '../config/memory-loader.mjs';
5
5
  import { bahulamHome, projectConfigDir } from './paths.mjs';
6
6
 
7
7
  function sha(content) {
@@ -27,8 +27,8 @@ function readFile(filePath, label, maxChars = 12000) {
27
27
  }
28
28
  }
29
29
 
30
- function readTasks(keplerDir) {
31
- const tasksDir = path.join(keplerDir, 'tasks');
30
+ function readTasks(bahulamDir) {
31
+ const tasksDir = path.join(bahulamDir, 'tasks');
32
32
  const files = [];
33
33
  try {
34
34
  if (!fs.existsSync(tasksDir)) return files;
@@ -66,9 +66,9 @@ function scanSkills(dir, scope) {
66
66
  }
67
67
 
68
68
  export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}) {
69
- const keplerDir = projectConfigDir(cwd);
69
+ const bahulamDir = projectConfigDir(cwd);
70
70
  const files = [];
71
- for (const file of loadKeplerMemory({ cwd })) {
71
+ for (const file of loadBahulamMemory({ cwd })) {
72
72
  const label = file.path.endsWith(path.join('.bahulam', 'KEPLER.md'))
73
73
  ? 'KEPLER.md'
74
74
  : path.basename(file.path);
@@ -83,10 +83,10 @@ export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}
83
83
  }
84
84
 
85
85
  for (const name of ['config.json', 'project.md', 'style.md', 'goal.md', 'plan.md', 'hitl.md']) {
86
- const file = readFile(path.join(keplerDir, name), name, name.endsWith('.json') ? 4000 : 12000);
86
+ const file = readFile(path.join(bahulamDir, name), name, name.endsWith('.json') ? 4000 : 12000);
87
87
  if (file) files.push(file);
88
88
  }
89
- files.push(...readTasks(keplerDir));
89
+ files.push(...readTasks(bahulamDir));
90
90
 
91
91
  const previousHashes = new Map((previous?.files || []).map(f => [f.path, f.hash]));
92
92
  const changed = files.filter(f => f.hash && previousHashes.get(f.path) && previousHashes.get(f.path) !== f.hash);
@@ -102,14 +102,14 @@ export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}
102
102
 
103
103
  const skills = [
104
104
  ...scanSkills(bahulamHome(), 'global'),
105
- ...scanSkills(keplerDir, 'project'),
105
+ ...scanSkills(bahulamDir, 'project'),
106
106
  ];
107
107
  const byName = new Map();
108
108
  for (const skill of skills) byName.set(skill.name, skill);
109
109
 
110
110
  return {
111
111
  root: cwd,
112
- kepler_dir: keplerDir,
112
+ kepler_dir: bahulamDir,
113
113
  files,
114
114
  loaded,
115
115
  changed,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Settings Sync — fetch user settings from Tarang web and cache locally.
2
+ * Settings Sync — fetch user settings from Bahulam web and cache locally.
3
3
  *
4
4
  * Syncs: gateway_type, model preferences, configured providers.
5
5
  * Cached in ~/.bahulam/config.json alongside auth token.
@@ -1,4 +1,5 @@
1
1
  export const DEFAULT_STAGNATION_THRESHOLD = 3;
2
+ const NO_CHANGE_EDIT_TOOLS = new Set(['edit_file']);
2
3
 
3
4
  function canonicalize(value) {
4
5
  if (Array.isArray(value)) return value.map(canonicalize);
@@ -12,6 +13,26 @@ function canonicalize(value) {
12
13
  return value;
13
14
  }
14
15
 
16
+ function editTarget(input = {}, result = {}) {
17
+ return String(
18
+ input.file_path || input.path || result.file_path || result.path || '',
19
+ ).trim();
20
+ }
21
+
22
+ function isNoChangeEdit(tool, result = {}) {
23
+ if (!NO_CHANGE_EDIT_TOOLS.has(tool)) return false;
24
+ if (!result || typeof result !== 'object') return false;
25
+ if (result._no_change || result.no_change) return true;
26
+ if (
27
+ result.success !== false
28
+ && result.lines_added === 0
29
+ && result.lines_removed === 0
30
+ ) {
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+
15
36
  export function createStagnationTracker({
16
37
  enabled = true,
17
38
  threshold = DEFAULT_STAGNATION_THRESHOLD,
@@ -21,6 +42,8 @@ export function createStagnationTracker({
21
42
  : DEFAULT_STAGNATION_THRESHOLD;
22
43
  let previousSignature = null;
23
44
  let consecutiveCount = 0;
45
+ let previousResultSignature = null;
46
+ let consecutiveResultCount = 0;
24
47
 
25
48
  return {
26
49
  record(tool, input) {
@@ -43,14 +66,51 @@ export function createStagnationTracker({
43
66
  };
44
67
  },
45
68
 
69
+ recordResult(tool, input, result) {
70
+ if (!enabled) return { detected: false, count: 0 };
71
+ if (!isNoChangeEdit(tool, result)) {
72
+ previousResultSignature = null;
73
+ consecutiveResultCount = 0;
74
+ return { detected: false, count: 0 };
75
+ }
76
+
77
+ const target = editTarget(input, result);
78
+ const signature = JSON.stringify({
79
+ kind: 'no_change_edit',
80
+ tool,
81
+ target,
82
+ });
83
+ if (signature === previousResultSignature) {
84
+ consecutiveResultCount++;
85
+ } else {
86
+ previousResultSignature = signature;
87
+ consecutiveResultCount = 1;
88
+ }
89
+
90
+ return {
91
+ detected: consecutiveResultCount >= effectiveThreshold,
92
+ count: consecutiveResultCount,
93
+ kind: 'no_change_edit',
94
+ target,
95
+ };
96
+ },
97
+
46
98
  reset() {
47
99
  previousSignature = null;
48
100
  consecutiveCount = 0;
101
+ previousResultSignature = null;
102
+ consecutiveResultCount = 0;
49
103
  },
50
104
  };
51
105
  }
52
106
 
53
- export function stagnationMessage(tool, count) {
107
+ export function stagnationMessage(tool, count, details = {}) {
108
+ if (details.kind === 'no_change_edit') {
109
+ const target = details.target ? ` for "${details.target}"` : '';
110
+ return `STAGNATION WARNING: Tool "${tool}" made no file changes ${count} consecutive times${target}. ` +
111
+ `The duplicate edit loop was stopped. Re-read the target range, compare the current file content, ` +
112
+ `change the edit strategy, or finish the task.`;
113
+ }
54
114
  return `STAGNATION WARNING: Tool "${tool}" was called ${count} consecutive times ` +
55
115
  `with identical arguments. The duplicate call was skipped. Review the previous ` +
56
116
  `result, change the arguments, try another tool, or finish the task.`;
@@ -1,8 +1,8 @@
1
1
  /**
2
- * TarangStreamClient — SSE consumer for Tarang backend.
2
+ * BahulamStreamClient — SSE consumer for Bahulam backend.
3
3
  *
4
4
  * Replaces OCC's agent-loop.mjs. Instead of calling the LLM API directly,
5
- * this client POSTs to the Tarang backend, parses the SSE stream, intercepts
5
+ * this client POSTs to the Bahulam backend, parses the SSE stream, intercepts
6
6
  * tool_request/tool_call events (executes locally, POSTs callback), and
7
7
  * yields all other events to the caller for rendering.
8
8
  *
@@ -117,10 +117,10 @@ function isOfflineLikelyError(err) {
117
117
  );
118
118
  }
119
119
 
120
- export class TarangStreamClient {
120
+ export class BahulamStreamClient {
121
121
  /**
122
122
  * @param {Object} opts
123
- * @param {string} opts.baseUrl - Tarang backend URL (ignored when mode='bundled')
123
+ * @param {string} opts.baseUrl - Bahulam backend URL (ignored when mode='bundled')
124
124
  * @param {string} opts.token - CLI auth token
125
125
  * @param {Object} opts.toolExecutor - { execute(name, args) }
126
126
  * @param {boolean} [opts.verbose=false]
@@ -10,7 +10,7 @@
10
10
  import fs from 'fs';
11
11
  import path from 'path';
12
12
  import os from 'os';
13
- import { loadKeplerMemory } from '../config/memory-loader.mjs';
13
+ import { loadBahulamMemory } from '../config/memory-loader.mjs';
14
14
 
15
15
  /**
16
16
  * Load all CLAUDE.md files and merge them in order.
@@ -89,7 +89,7 @@ export function buildSystemPrompt({ cwd, tools, override, addDirs } = {}) {
89
89
  parts.push(f.content);
90
90
  }
91
91
 
92
- for (const f of loadKeplerMemory({ cwd })) {
92
+ for (const f of loadBahulamMemory({ cwd })) {
93
93
  parts.push(f.content);
94
94
  }
95
95