muonroi-cli 1.8.0 → 1.8.1

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 (101) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/src/agent-harness/mock-model.js +112 -4
  4. package/dist/src/cli/cost-forensics.js +17 -14
  5. package/dist/src/council/prompts.js +20 -20
  6. package/dist/src/flow/compaction/index.d.ts +1 -0
  7. package/dist/src/flow/compaction/index.js +4 -0
  8. package/dist/src/generated/version.d.ts +1 -1
  9. package/dist/src/generated/version.js +1 -1
  10. package/dist/src/index.js +4 -2
  11. package/dist/src/lsp/manager.js +11 -3
  12. package/dist/src/lsp/manager.test.js +39 -0
  13. package/dist/src/mcp/auto-setup.js +0 -8
  14. package/dist/src/mcp/oauth-callback.js +2 -2
  15. package/dist/src/mcp/parse-headers.test.js +14 -14
  16. package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
  17. package/dist/src/mcp/self-verify-runner.js +1 -1
  18. package/dist/src/mcp/setup-guide-text.js +74 -74
  19. package/dist/src/mcp/smoke.test.js +43 -43
  20. package/dist/src/ops/doctor.js +7 -7
  21. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +1 -0
  22. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  23. package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
  24. package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
  25. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
  26. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
  27. package/dist/src/orchestrator/auto-commit.js +9 -7
  28. package/dist/src/orchestrator/message-processor.d.ts +2 -0
  29. package/dist/src/orchestrator/message-processor.js +7 -0
  30. package/dist/src/orchestrator/orchestrator.d.ts +3 -0
  31. package/dist/src/orchestrator/orchestrator.js +362 -28
  32. package/dist/src/orchestrator/pending-calls.js +2 -1
  33. package/dist/src/orchestrator/preprocessor.js +3 -2
  34. package/dist/src/orchestrator/prompts.d.ts +8 -0
  35. package/dist/src/orchestrator/prompts.js +65 -18
  36. package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
  37. package/dist/src/orchestrator/sub-agent-cap.js +12 -4
  38. package/dist/src/orchestrator/subagent-compactor.d.ts +7 -0
  39. package/dist/src/orchestrator/subagent-compactor.js +65 -14
  40. package/dist/src/orchestrator/subagent-compactor.spec.js +125 -9
  41. package/dist/src/orchestrator/tool-engine.d.ts +2 -0
  42. package/dist/src/orchestrator/tool-engine.js +80 -21
  43. package/dist/src/orchestrator/turn-runner-deps.d.ts +2 -0
  44. package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
  45. package/dist/src/pil/layer1-intent.js +37 -37
  46. package/dist/src/pil/layer2_5-ponytail.js +8 -8
  47. package/dist/src/pil/llm-classify.d.ts +11 -0
  48. package/dist/src/pil/llm-classify.js +193 -0
  49. package/dist/src/product-loop/done-gate.js +3 -3
  50. package/dist/src/product-loop/loop-driver.js +18 -18
  51. package/dist/src/product-loop/progress-snapshot.js +4 -4
  52. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  53. package/dist/src/reporter/index.js +1 -1
  54. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  55. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  56. package/dist/src/scaffold/continuation-prompt.js +60 -60
  57. package/dist/src/scaffold/init-new.js +453 -453
  58. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  59. package/dist/src/self-qa/agentic-loop.js +11 -2
  60. package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
  61. package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
  62. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  63. package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
  64. package/dist/src/storage/index.d.ts +1 -1
  65. package/dist/src/storage/index.js +1 -1
  66. package/dist/src/storage/interaction-log.js +5 -5
  67. package/dist/src/storage/migrations.js +135 -123
  68. package/dist/src/storage/session-experience-store.js +4 -4
  69. package/dist/src/storage/sessions.js +43 -43
  70. package/dist/src/storage/transcript-response-entry.test.js +18 -5
  71. package/dist/src/storage/transcript.d.ts +2 -0
  72. package/dist/src/storage/transcript.js +223 -97
  73. package/dist/src/storage/usage.js +34 -17
  74. package/dist/src/storage/workspaces.js +12 -12
  75. package/dist/src/tools/native-tools.js +10 -1
  76. package/dist/src/tools/registry.d.ts +1 -0
  77. package/dist/src/tools/registry.js +30 -2
  78. package/dist/src/types/index.d.ts +1 -1
  79. package/dist/src/ui/app.js +0 -0
  80. package/dist/src/ui/slash/council-inspect.js +4 -4
  81. package/dist/src/ui/slash/expand.js +14 -1
  82. package/dist/src/ui/use-app-logic.js +0 -0
  83. package/dist/src/utils/__tests__/compaction-caps.test.js +9 -9
  84. package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
  85. package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
  86. package/dist/src/utils/__tests__/logger.test.js +115 -0
  87. package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
  88. package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
  89. package/dist/src/utils/clipboard-image.js +23 -23
  90. package/dist/src/utils/install-manager.d.ts +1 -0
  91. package/dist/src/utils/install-manager.js +76 -7
  92. package/dist/src/utils/install-manager.test.js +43 -1
  93. package/dist/src/utils/logger.d.ts +28 -0
  94. package/dist/src/utils/logger.js +171 -0
  95. package/dist/src/utils/settings.d.ts +9 -1
  96. package/dist/src/utils/settings.js +59 -22
  97. package/dist/src/utils/side-question.js +2 -2
  98. package/dist/src/utils/skills.js +3 -3
  99. package/dist/src/utils/update-checker.test.js +5 -1
  100. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  101. package/package.json +1 -1
@@ -23,7 +23,8 @@ import { statusBarStore } from "../ui/status-bar/store.js";
23
23
  import { appendCostLog } from "../usage/cost-log.js";
24
24
  import { appendDecisionLog } from "../usage/decision-log.js";
25
25
  import { projectCostUSD, sanitizeInputTokens } from "../usage/estimator.js";
26
- import { getAutoCompactThresholdPct, getCouncilRounds, getCurrentModel, getCurrentShellSettings, getModeSpecificModel, getRoleModel, getRoleModels, isAutoCompactAfterTurnEnabled, isCouncilMultiProviderPreferred, isProviderDisabled, } from "../utils/settings.js";
26
+ import { logger } from "../utils/logger.js";
27
+ import { getAutoCompactThresholdPct, getCouncilRounds, getCurrentModel, getCurrentShellSettings, getModeSpecificModel, getRoleModel, getRoleModels, isAutoCompactAfterTurnEnabled, isCouncilMultiProviderPreferred, isProviderDisabled, loadUserSettings, } from "../utils/settings.js";
27
28
  import { runSideQuestion } from "../utils/side-question.js";
28
29
  import { buildVerifyDetectPrompt, normalizeVerifyRecipe } from "../verify/entrypoint.js";
29
30
  import { runVerifyOrchestration } from "../verify/orchestrator.js";
@@ -38,7 +39,7 @@ import { DelegationManager } from "./delegations.js";
38
39
  import { loadFlowResumeDigest } from "./flow-resume.js";
39
40
  import { MessageProcessor } from "./message-processor.js";
40
41
  import { lastPersistedSeq } from "./message-seq.js";
41
- import { buildSystemPrompt, MAX_TOOL_ROUNDS } from "./prompts.js";
42
+ import { buildSystemPrompt, HARD_MAX_TOOL_ROUNDS, MAX_TOOL_ROUNDS } from "./prompts.js";
42
43
  import { getReadPathBudgetCap, ReadPathBudget } from "./read-path-budget.js";
43
44
  import { withStreamRetry } from "./retry-stream.js";
44
45
  import { StreamRunner } from "./stream-runner.js";
@@ -136,6 +137,7 @@ export class Agent {
136
137
  /** UI-registered live-queue steer drain; see Agent.setSteerDrain. */
137
138
  steerDrain = null;
138
139
  maxToolRounds;
140
+ hardMaxToolRounds;
139
141
  mode = "agent";
140
142
  modelId;
141
143
  maxTokens;
@@ -240,7 +242,10 @@ export class Agent {
240
242
  this.setApiKey(apiKey, baseURL);
241
243
  }
242
244
  this.schedules = new ScheduleManager(() => this.bash.getCwd(), () => this.modelId);
243
- this.maxToolRounds = maxToolRounds || MAX_TOOL_ROUNDS;
245
+ const settings = loadUserSettings();
246
+ this.maxToolRounds = maxToolRounds || settings.maxToolRounds || MAX_TOOL_ROUNDS;
247
+ const baseHardMax = settings.hardMaxToolRounds || HARD_MAX_TOOL_ROUNDS;
248
+ this.hardMaxToolRounds = Math.max(Math.floor(this.maxToolRounds * 1.5), baseHardMax);
244
249
  const envMax = Number(process.env.MUONROI_MAX_TOKENS);
245
250
  this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
246
251
  this.batchApi = options.batchApi ?? false;
@@ -267,7 +272,7 @@ export class Agent {
267
272
  warmMcpClients(filterMcpServersByMessage(loadMcpServers(), ""));
268
273
  }
269
274
  catch (err) {
270
- console.error(`[orchestrator] MCP pre-warm skipped: ${err?.message}`);
275
+ logger.error("orchestrator", "MCP pre-warm skipped", { error: err });
271
276
  }
272
277
  })();
273
278
  if (options.persistSession !== false) {
@@ -424,6 +429,10 @@ export class Agent {
424
429
  getMessages() {
425
430
  return this.messages;
426
431
  }
432
+ setMessages(messages) {
433
+ this.messages = messages;
434
+ this.messageSeqs = messages.map(() => null);
435
+ }
427
436
  async listSchedules() {
428
437
  return this.schedules.list();
429
438
  }
@@ -1212,6 +1221,20 @@ export class Agent {
1212
1221
  if (this._compactionStats.count >= 2) {
1213
1222
  keepRecentTokens = Math.floor(keepRecentTokens * 0.85);
1214
1223
  }
1224
+ // For sub-sessions, reduce keepRecentTokens to 8000 to keep active history light
1225
+ try {
1226
+ if (this.session) {
1227
+ const { getDatabase } = require("../storage/db.js");
1228
+ const db = getDatabase();
1229
+ const row = db.prepare("SELECT parent_session_id FROM sessions WHERE id = ?").get(this.session.id);
1230
+ if (row?.parent_session_id) {
1231
+ keepRecentTokens = 8000;
1232
+ }
1233
+ }
1234
+ }
1235
+ catch {
1236
+ // Ignored
1237
+ }
1215
1238
  return {
1216
1239
  reserveTokens: Math.max(this.maxTokens, DEFAULT_RESERVE_TOKENS),
1217
1240
  keepRecentTokens,
@@ -1276,7 +1299,23 @@ export class Agent {
1276
1299
  const keptSeqs = this.messageSeqs.slice(preparation.firstKeptIndex);
1277
1300
  const firstKeptSeq = keptSeqs.find((seq) => seq !== null) ?? getNextMessageSequence(this.session.id);
1278
1301
  const compactStartedAt = Date.now();
1279
- const { summary, usage: compactUsage } = await generateCompactionSummary(provider, compactModelId, preparation, undefined, signal);
1302
+ const isSubSession = await (async () => {
1303
+ try {
1304
+ if (!this.session)
1305
+ return false;
1306
+ const { getDatabase } = await import("../storage/db.js");
1307
+ const db = getDatabase();
1308
+ const row = db.prepare("SELECT parent_session_id FROM sessions WHERE id = ?").get(this.session.id);
1309
+ return !!row?.parent_session_id;
1310
+ }
1311
+ catch {
1312
+ return false;
1313
+ }
1314
+ })();
1315
+ const customInstructions = isSubSession
1316
+ ? "This is a temporary sub-session. Under sub-sessions, it is CRITICAL to preserve active files being worked on, compiler/linter error states, and exact line coordinates in the summary. Do not omit details of files edited, tests run, or compiler diagnostics, as the model needs this specific context to continue working without re-reading the files."
1317
+ : undefined;
1318
+ const { summary, usage: compactUsage } = await generateCompactionSummary(provider, compactModelId, preparation, customInstructions, signal);
1280
1319
  // Record compaction call in cost-log — bypasses recordUsage because
1281
1320
  // compaction returns usage separately and isn't routed through the
1282
1321
  // status-bar / usage event pipeline (intentional: it's overhead, not user spend).
@@ -1414,7 +1453,7 @@ export class Agent {
1414
1453
  });
1415
1454
  }
1416
1455
  log(true, `over-threshold (${tokens} >= ${minMeaningfulTokens})`, { tokens, thresholdPct, minMeaningfulTokens });
1417
- await this.compactForContext(provider, system, contextWindow, signal, this.getCompactionSettings(contextWindow), true).catch((err) => console.warn("[compact] failed:", err?.message));
1456
+ await this.compactForContext(provider, system, contextWindow, signal, this.getCompactionSettings(contextWindow), true).catch((err) => logger.warn("orchestrator", "compaction failed", { error: err }));
1418
1457
  }
1419
1458
  // ========================================================================
1420
1459
  // Council system — delegated to CouncilManager (Phase 12.1-02)
@@ -1473,7 +1512,7 @@ export class Agent {
1473
1512
  runDir = nodePath.join(flowDir, "runs", runId);
1474
1513
  }
1475
1514
  catch (err) {
1476
- console.error(`[council] runDir resolution failed (decisions.lock will be skipped): ${err?.message}`);
1515
+ logger.error("router", "runDir resolution failed (decisions.lock will be skipped)", { error: err });
1477
1516
  }
1478
1517
  try {
1479
1518
  const gen = runCouncil(topic, this.modelId, this.messages, this.session?.id, llm, this.councilManager.createQuestionResponder(), this.councilManager.createPreflightResponder(), processMessageFn, {
@@ -2005,6 +2044,9 @@ export class Agent {
2005
2044
  get maxToolRounds() {
2006
2045
  return self.maxToolRounds;
2007
2046
  },
2047
+ get hardMaxToolRounds() {
2048
+ return self.hardMaxToolRounds;
2049
+ },
2008
2050
  get maxTokens() {
2009
2051
  return self.maxTokens;
2010
2052
  },
@@ -2097,32 +2139,255 @@ export class Agent {
2097
2139
  fireHook(input, signal) {
2098
2140
  return executeEventHooks(input, this.bash.getCwd(), signal);
2099
2141
  }
2142
+ estimateConversationChars() {
2143
+ let count = 0;
2144
+ for (const m of this.messages) {
2145
+ if (typeof m.content === "string") {
2146
+ count += m.content.length;
2147
+ }
2148
+ else if (Array.isArray(m.content)) {
2149
+ for (const p of m.content) {
2150
+ if (p.type === "text" && p.text) {
2151
+ count += p.text.length;
2152
+ }
2153
+ }
2154
+ }
2155
+ }
2156
+ return count;
2157
+ }
2100
2158
  // ========================================================================
2101
2159
  // processMessage — main streaming turn loop (PIL enrichment, routing, LLM
2102
2160
  // stream, tool execution, compaction, hooks, observer notifications)
2103
2161
  // ========================================================================
2104
2162
  async *processMessage(userMessage, observer, images) {
2105
- const processor = new MessageProcessor(this._buildMessageProcessorDeps());
2106
- // Deterministic "task done -> commit" (auto-commit): snapshot the dirty set
2107
- // before the turn so we can commit ONLY the files the agent changes during it
2108
- // (and never the user's pre-existing work-in-progress). Gated off under tests
2109
- // and via MUONROI_AUTO_COMMIT=0.
2163
+ const threshold = Number(process.env.MUONROI_SILENT_ROTATION_THRESHOLD) || 80000;
2164
+ const currentChars = this.estimateConversationChars();
2165
+ logger.debug("orchestrator", "Checking silent session rotation threshold", { currentChars, threshold });
2166
+ // 1. Run classifier to decide execution route
2167
+ let routeAction = "DIRECT_ANSWER";
2168
+ try {
2169
+ const { classifySubSessionAction } = await import("../pil/llm-classify.js");
2170
+ const routeResult = await classifySubSessionAction(this.requireProvider(), this.modelId, userMessage);
2171
+ if (routeResult) {
2172
+ routeAction = routeResult.action;
2173
+ logger.info("orchestrator", "Routing action selected for user message", {
2174
+ action: routeAction,
2175
+ confidence: routeResult.confidence,
2176
+ reason: routeResult.reason,
2177
+ });
2178
+ }
2179
+ }
2180
+ catch (err) {
2181
+ logger.error("orchestrator", "Routing classification failed, falling back to DIRECT_ANSWER", { error: err });
2182
+ }
2183
+ const shouldRotate = currentChars > threshold || routeAction === "ROTATE_SESSION";
2184
+ if (shouldRotate && this.session && this.sessionStore) {
2185
+ logger.info("orchestrator", "Triggering silent session rotation", {
2186
+ parentSessionId: this.session.id,
2187
+ currentChars,
2188
+ });
2189
+ yield { type: "content", content: "\n⋯ Xoay vòng session ngầm để tối ưu hóa context (Anti-Mù active)...\n" };
2190
+ const parentSessionId = this.session.id;
2191
+ try {
2192
+ const path = await import("node:path");
2193
+ const flowDir = path.join(this.bash.getCwd(), ".muonroi-flow");
2194
+ const { deliberateCompact } = await import("../flow/compaction/index.js");
2195
+ const { getDatabase } = await import("../storage/db.js");
2196
+ const { appendCompaction, getNextMessageSequence } = await import("../storage/transcript.js");
2197
+ const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.requireProvider(), this.modelId);
2198
+ const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
2199
+ const db = getDatabase();
2200
+ db.prepare("UPDATE sessions SET parent_session_id = ? WHERE id = ?").run(parentSessionId, newSession.id);
2201
+ const summaryMessage = createCompactionSummaryMessage(cr.summary);
2202
+ const nextSeq = getNextMessageSequence(newSession.id);
2203
+ appendCompaction(newSession.id, nextSeq, cr.summary, cr.tokensBeforeCompress);
2204
+ this.session = this.sessionStore.getRequiredSession(newSession.id);
2205
+ this.messages = [summaryMessage];
2206
+ this.messageSeqs = [null];
2207
+ this.sessionStore.touchSession(newSession.id, this.bash.getCwd());
2208
+ logger.info("orchestrator", "Silent session rotation completed successfully", {
2209
+ parentSessionId,
2210
+ newSessionId: newSession.id,
2211
+ summaryLength: cr.summary.length,
2212
+ tokensBeforeCompress: cr.tokensBeforeCompress,
2213
+ });
2214
+ }
2215
+ catch (err) {
2216
+ logger.error("orchestrator", "silent session rotation failed", { error: err });
2217
+ }
2218
+ }
2219
+ let isSubSessionForked = false;
2220
+ let parentSessionId = null;
2221
+ let subSessionId = null;
2222
+ if (routeAction === "SPAWN_SUB_SESSION" && this.session && this.sessionStore) {
2223
+ yield { type: "content", content: "\n⋯ Đang khởi tạo sub-session ngầm để xử lý tác vụ...\n" };
2224
+ parentSessionId = this.session.id;
2225
+ try {
2226
+ const { loadLatestCompaction, getNextMessageSequence, appendCompaction, loadTranscriptState } = await import("../storage/transcript.js");
2227
+ const { getDatabase } = await import("../storage/db.js");
2228
+ const db = getDatabase();
2229
+ // Check if there is already an active child session for this parent session
2230
+ const activeSubSession = db
2231
+ .prepare("SELECT id, updated_at FROM sessions WHERE parent_session_id = ? AND status = 'active' ORDER BY updated_at DESC LIMIT 1")
2232
+ .get(parentSessionId);
2233
+ let shouldResume = false;
2234
+ if (activeSubSession) {
2235
+ const updatedAt = new Date(activeSubSession.updated_at).getTime();
2236
+ const now = Date.now();
2237
+ const diffMins = (now - updatedAt) / (60 * 1000);
2238
+ if (diffMins > 15) {
2239
+ // Stale: mark as abandoned
2240
+ db.prepare("UPDATE sessions SET status = 'abandoned', updated_at = ? WHERE id = ?").run(new Date().toISOString(), activeSubSession.id);
2241
+ logger.info("orchestrator", "Stale sub-session found, marked as abandoned", {
2242
+ parentSessionId,
2243
+ subSessionId: activeSubSession.id,
2244
+ diffMins,
2245
+ });
2246
+ }
2247
+ else {
2248
+ shouldResume = true;
2249
+ subSessionId = activeSubSession.id;
2250
+ }
2251
+ }
2252
+ if (shouldResume && subSessionId) {
2253
+ yield { type: "content", content: "\n⋯ Phát hiện sub-session trước đó bị gián đoạn, đang khôi phục...\n" };
2254
+ this.session = this.sessionStore.getRequiredSession(subSessionId);
2255
+ const childState = loadTranscriptState(subSessionId);
2256
+ this.messages = childState.messages;
2257
+ this.messageSeqs = childState.seqs;
2258
+ this.sessionStore.touchSession(subSessionId, this.bash.getCwd());
2259
+ isSubSessionForked = true;
2260
+ logger.info("orchestrator", "Resumed child sub-session successfully", {
2261
+ parentSessionId,
2262
+ subSessionId,
2263
+ });
2264
+ }
2265
+ else {
2266
+ const latest = loadLatestCompaction(parentSessionId);
2267
+ const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
2268
+ db.prepare("UPDATE sessions SET parent_session_id = ? WHERE id = ?").run(parentSessionId, newSession.id);
2269
+ subSessionId = newSession.id;
2270
+ let seedMessages = [];
2271
+ let seedSeqs = [];
2272
+ if (latest?.summary) {
2273
+ const summaryMsg = createCompactionSummaryMessage(latest.summary);
2274
+ seedMessages = [summaryMsg];
2275
+ seedSeqs = [null];
2276
+ const nextSeq = getNextMessageSequence(subSessionId);
2277
+ appendCompaction(subSessionId, nextSeq, latest.summary, latest.tokensBefore);
2278
+ }
2279
+ else {
2280
+ seedMessages = [...this.messages];
2281
+ seedSeqs = [...this.messageSeqs];
2282
+ }
2283
+ // Add sub-session overlay system message
2284
+ const overlayMessage = {
2285
+ role: "system",
2286
+ content: `You are executing a sub-task delegated by the Main Session in an isolated, temporary Sub-Session.\n` +
2287
+ `Your goal is to satisfy the user's request: "${userMessage}"\n\n` +
2288
+ `Your Operating Boundaries & Rules:\n` +
2289
+ `1. You have full access to tools (bash, edit_file, read_file, grep, etc.). Execute them as needed to build, debug, and verify the work.\n` +
2290
+ `2. Stay strictly focused on completing the request. Do not engage in social chat or pleasantries.\n` +
2291
+ `3. Once the goal is achieved and verified, you MUST return your final response using the 'respond_general' (or final answer) tool.\n` +
2292
+ `4. Your final response MUST contain a structured summary (Key Changes, Verification Details, Result Summary).\n` +
2293
+ `5. All intermediate tool calls, raw tool outputs, and diagnostic traces will remain isolated inside this sub-session and will not bloat the parent session.`,
2294
+ };
2295
+ seedMessages.push(overlayMessage);
2296
+ seedSeqs.push(null);
2297
+ this.session = this.sessionStore.getRequiredSession(subSessionId);
2298
+ this.messages = seedMessages;
2299
+ this.messageSeqs = seedSeqs;
2300
+ this.sessionStore.touchSession(subSessionId, this.bash.getCwd());
2301
+ isSubSessionForked = true;
2302
+ logger.info("orchestrator", "Forked child sub-session successfully", {
2303
+ parentSessionId,
2304
+ subSessionId,
2305
+ });
2306
+ }
2307
+ }
2308
+ catch (err) {
2309
+ logger.error("orchestrator", "Forking child sub-session failed, falling back to main session", { error: err });
2310
+ }
2311
+ }
2312
+ let processor = new MessageProcessor(this._buildMessageProcessorDeps());
2110
2313
  const autoCommitOn = isAutoCommitEnabled();
2111
2314
  const cwd = this.bash.getCwd();
2112
2315
  const dirtyBefore = autoCommitOn ? await snapshotDirtyPaths(cwd) : new Set();
2113
- yield* processor.run(userMessage, observer, images);
2114
- // Reached only when the turn completed normally (an abort/throw propagates
2115
- // through yield* and skips this) — exactly when committing is appropriate.
2116
- if (autoCommitOn) {
2117
- const auto = await maybeAutoCommitTurn({ cwd, dirtyBefore, userMessage }).catch((err) => {
2118
- console.error(`[auto-commit] unexpected failure: ${err?.message}`);
2119
- return { committed: false };
2120
- });
2121
- if (auto.committed) {
2122
- yield {
2123
- type: "content",
2124
- content: `\n✓ Auto-committed ${auto.fileCount} file(s) → ${auto.sha} (${AUTO_COMMIT_ATTRIBUTION})\n`,
2125
- };
2316
+ try {
2317
+ let attempts = 0;
2318
+ const maxAttempts = 3;
2319
+ const messagesSnapshot = [...this.messages];
2320
+ const seqsSnapshot = [...this.messageSeqs];
2321
+ while (attempts < maxAttempts) {
2322
+ try {
2323
+ attempts++;
2324
+ yield* processor.run(userMessage, observer, images);
2325
+ break;
2326
+ }
2327
+ catch (err) {
2328
+ if (isSubSessionForked && isTransientError(err) && attempts < maxAttempts && subSessionId) {
2329
+ logger.warn("orchestrator", "Transient error in sub-session, retrying", {
2330
+ attempt: attempts,
2331
+ error: err,
2332
+ });
2333
+ yield {
2334
+ type: "content",
2335
+ content: `\n⚠️ Có lỗi mạng hoặc rate limit xảy ra trong sub-session (lần thử ${attempts}/${maxAttempts}). Đang thử lại...\n`,
2336
+ };
2337
+ this.messages = [...messagesSnapshot];
2338
+ this.messageSeqs = [...seqsSnapshot];
2339
+ processor = new MessageProcessor(this._buildMessageProcessorDeps());
2340
+ }
2341
+ else {
2342
+ throw err;
2343
+ }
2344
+ }
2345
+ }
2346
+ if (autoCommitOn) {
2347
+ const auto = await maybeAutoCommitTurn({ cwd, dirtyBefore, userMessage }).catch((err) => {
2348
+ logger.error("orchestrator", "auto-commit unexpected failure", { error: err });
2349
+ return { committed: false };
2350
+ });
2351
+ if (auto.committed) {
2352
+ yield {
2353
+ type: "content",
2354
+ content: `\n✓ Auto-committed ${auto.fileCount} file(s) → ${auto.sha} (${AUTO_COMMIT_ATTRIBUTION})\n`,
2355
+ };
2356
+ }
2357
+ }
2358
+ }
2359
+ finally {
2360
+ if (isSubSessionForked && parentSessionId && this.sessionStore) {
2361
+ try {
2362
+ const finalMessages = salvageSubSessionOutput(this.messages);
2363
+ // Restore parent session
2364
+ this.session = this.sessionStore.getRequiredSession(parentSessionId);
2365
+ const { loadTranscriptState } = await import("../storage/transcript.js");
2366
+ const parentState = loadTranscriptState(parentSessionId);
2367
+ this.messages = parentState.messages;
2368
+ this.messageSeqs = parentState.seqs;
2369
+ const userModelMessage = {
2370
+ role: "user",
2371
+ content: userMessage,
2372
+ };
2373
+ if (finalMessages.length > 0) {
2374
+ this.appendCompletedTurn(userModelMessage, finalMessages);
2375
+ logger.info("orchestrator", "Absorbed sub-session outcome into parent session", {
2376
+ parentSessionId,
2377
+ subSessionId,
2378
+ absorbedMessagesCount: finalMessages.length,
2379
+ });
2380
+ }
2381
+ else {
2382
+ logger.warn("orchestrator", "No assistant messages found to absorb from sub-session", {
2383
+ parentSessionId,
2384
+ subSessionId,
2385
+ });
2386
+ }
2387
+ }
2388
+ catch (err) {
2389
+ logger.error("orchestrator", "Failed to absorb sub-session final summary", { error: err });
2390
+ }
2126
2391
  }
2127
2392
  }
2128
2393
  }
@@ -2163,6 +2428,9 @@ export class Agent {
2163
2428
  get maxToolRounds() {
2164
2429
  return self.maxToolRounds;
2165
2430
  },
2431
+ get hardMaxToolRounds() {
2432
+ return self.hardMaxToolRounds;
2433
+ },
2166
2434
  get batchApi() {
2167
2435
  return self.batchApi;
2168
2436
  },
@@ -2257,6 +2525,39 @@ export class Agent {
2257
2525
  },
2258
2526
  requireProvider: () => self.requireProvider(),
2259
2527
  emitSubagentStatus: (s) => self.emitSubagentStatus(s),
2528
+ consultParentSession: async (question) => {
2529
+ if (!self.session) {
2530
+ return "No active session found to consult.";
2531
+ }
2532
+ const { getDatabase } = await import("../storage/db.js");
2533
+ const db = getDatabase();
2534
+ const row = db.prepare("SELECT parent_session_id FROM sessions WHERE id = ?").get(self.session.id);
2535
+ if (!row?.parent_session_id) {
2536
+ return "No parent session found to consult.";
2537
+ }
2538
+ const parentSessionId = row.parent_session_id;
2539
+ const { loadTranscriptState } = await import("../storage/transcript.js");
2540
+ const parentState = loadTranscriptState(parentSessionId);
2541
+ const { serializeConversation } = await import("./compaction.js");
2542
+ const serializedParent = serializeConversation(parentState.messages);
2543
+ const systemPrompt = `You are the Main Session of an AI coding assistant.\n` +
2544
+ `A child sub-session has been spawned to execute a sub-task and is requesting your advice/guidance.\n` +
2545
+ `Below is your conversation history (Main Session Context):\n\n` +
2546
+ `${serializedParent}\n\n` +
2547
+ `Provide clear, actionable guidance to resolve the child's query.`;
2548
+ const { generateText } = await import("ai");
2549
+ const provider = self.requireProvider();
2550
+ const modelId = self.modelId;
2551
+ const runtime = resolveModelRuntime(provider, modelId);
2552
+ const result = await generateText({
2553
+ model: runtime.model,
2554
+ system: systemPrompt,
2555
+ prompt: `Child Sub-session is stuck. Question:\n${question}`,
2556
+ temperature: 0.2,
2557
+ ...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
2558
+ });
2559
+ return result.text;
2560
+ },
2260
2561
  fireHook: (input, signal) => self.fireHook(input, signal),
2261
2562
  consumeBackgroundNotifications: () => self.consumeBackgroundNotifications(),
2262
2563
  initOAuthProvider: () => self._initOAuthProvider(),
@@ -2285,21 +2586,21 @@ export class Agent {
2285
2586
  return await h(info);
2286
2587
  }
2287
2588
  catch (err) {
2288
- console.error(`[Agent] askToolLoopContinue crashed: ${err?.message ?? err}`);
2589
+ logger.error("orchestrator", "askToolLoopContinue crashed", { error: err });
2289
2590
  return "stop";
2290
2591
  }
2291
2592
  },
2292
2593
  askSafetyOverride: async (info) => {
2293
2594
  const h = self._safetyOverrideHandler;
2294
2595
  if (!h) {
2295
- console.warn(`[Agent] askSafetyOverride called but no handler registered — blocking ${info.kind}: ${info.reason}`);
2596
+ logger.warn("orchestrator", `askSafetyOverride called but no handler registered — blocking ${info.kind}: ${info.reason}`);
2296
2597
  return { action: "block" };
2297
2598
  }
2298
2599
  try {
2299
2600
  return await h(info);
2300
2601
  }
2301
2602
  catch (err) {
2302
- console.error(`[Agent] askSafetyOverride crashed: ${err?.message ?? err}`);
2603
+ logger.error("orchestrator", "askSafetyOverride crashed", { error: err });
2303
2604
  return { action: "block" };
2304
2605
  }
2305
2606
  },
@@ -2475,4 +2776,37 @@ export class Agent {
2475
2776
  }
2476
2777
  }
2477
2778
  }
2779
+ function isTransientError(err) {
2780
+ if (!(err instanceof Error))
2781
+ return false;
2782
+ const msg = err.message.toLowerCase();
2783
+ return (msg.includes("fetch failed") ||
2784
+ msg.includes("rate limit") ||
2785
+ msg.includes("429") ||
2786
+ msg.includes("503") ||
2787
+ msg.includes("502") ||
2788
+ msg.includes("network") ||
2789
+ msg.includes("timeout") ||
2790
+ msg.includes("econnreset") ||
2791
+ msg.includes("econnrefused"));
2792
+ }
2793
+ function salvageSubSessionOutput(messages) {
2794
+ let lastAsstIdx = -1;
2795
+ for (let i = messages.length - 1; i >= 0; i--) {
2796
+ if (messages[i].role === "assistant") {
2797
+ lastAsstIdx = i;
2798
+ break;
2799
+ }
2800
+ }
2801
+ const finalMessages = [];
2802
+ if (lastAsstIdx >= 0) {
2803
+ finalMessages.push(messages[lastAsstIdx]);
2804
+ for (let i = lastAsstIdx + 1; i < messages.length; i++) {
2805
+ if (messages[i].role === "tool") {
2806
+ finalMessages.push(messages[i]);
2807
+ }
2808
+ }
2809
+ }
2810
+ return finalMessages;
2811
+ }
2478
2812
  //# sourceMappingURL=orchestrator.js.map
@@ -22,6 +22,7 @@ import * as crypto from "node:crypto";
22
22
  import { promises as fs } from "node:fs";
23
23
  import * as path from "node:path";
24
24
  import { getSessionDir } from "../storage/session-dir.js";
25
+ import { logger } from "../utils/logger.js";
25
26
  // ─── stableCallId ─────────────────────────────────────────────────────────────
26
27
  /**
27
28
  * Derives a deterministic call_id from (turnId, toolName, input).
@@ -160,7 +161,7 @@ export function createPendingCallsLog(sessionId) {
160
161
  void finalExists;
161
162
  }
162
163
  catch (err) {
163
- console.warn(`[muonroi-cli] reconcile: could not clean staged path ${tmp}: ${err.message}`);
164
+ logger.warn("orchestrator", `reconcile: could not clean staged path ${tmp}`, { error: err });
164
165
  }
165
166
  }
166
167
  // Mark the entry as abandoned in the log.
@@ -1,5 +1,6 @@
1
1
  import { runPipeline } from "../pil/pipeline.js";
2
2
  import { getSessionLastTask, recordSessionLastTask, resolveCeiling } from "./scope-ceiling.js";
3
+ import { logger } from "../utils/logger.js";
3
4
  export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
4
5
  // PIL: enrich prompt before pushing to messages (D-01, D-03, D-04)
5
6
  // Promise.race timeout of 200ms is inside runPipeline — fail-open guaranteed
@@ -31,7 +32,7 @@ export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
31
32
  llmFallback = createLlmClassifier(deps.requireProvider(), deps.modelId);
32
33
  }
33
34
  catch (err) {
34
- console.error(`[pil] LLM fallback wiring failed: ${err?.message}`);
35
+ logger.error("pil", "LLM fallback wiring failed", { error: err });
35
36
  }
36
37
  // Model-driven clarification proposer (for discovery interview).
37
38
  // The actual task model (via the same provider + modelId) generates the
@@ -42,7 +43,7 @@ export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
42
43
  clarificationProposer = createModelClarificationProposer(deps.requireProvider(), deps.modelId);
43
44
  }
44
45
  catch (err) {
45
- console.error(`[pil] clarification proposer wiring failed: ${err?.message}`);
46
+ logger.error("pil", "clarification proposer wiring failed", { error: err });
46
47
  }
47
48
  pilCtxResolved = await runPipeline(userMessage, {
48
49
  resumeDigest: deps.getResumeDigest(),
@@ -1,6 +1,8 @@
1
1
  import type { AgentMode, TaskRequest } from "../types/index.js";
2
2
  import { type CustomSubagentConfig, type SandboxMode, type SandboxSettings } from "../utils/settings.js";
3
3
  export declare const MAX_TOOL_ROUNDS: number;
4
+ export declare const HARD_MAX_TOOL_ROUNDS: number;
5
+ export declare const MAX_LLM_CALLS_PER_TURN: number;
4
6
  export declare const VISION_MODEL = "grok-4-1-fast-reasoning";
5
7
  export declare const COMPUTER_MODEL = "grok-4.20-0309-reasoning";
6
8
  /**
@@ -61,6 +63,12 @@ export interface SystemPromptOptions {
61
63
  * and can't run the full toolset anyway. Cuts ~6K tokens per sub-agent turn.
62
64
  */
63
65
  subAgent?: boolean;
66
+ /**
67
+ * When true (tool-turn, i.e. second+ LLM call in the same user-turn tool
68
+ * loop), skip native-capabilities and skills sections that were already
69
+ * shown in the first call. Cuts ~4K tokens per tool round-trip.
70
+ */
71
+ toolTurn?: boolean;
64
72
  }
65
73
  /**
66
74
  * Render the LIVE per-turn MCP tool roster as a system-prompt block.