clouds-coder 2026.5.2__tar.gz → 2026.5.28__tar.gz

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.
@@ -142,6 +142,10 @@ LONG_OUTPUT_LISTING_OFFLOAD_CHARS = 6_000
142
142
  LONG_OUTPUT_READ_PAGE_LINES = 240
143
143
  LONG_OUTPUT_READ_PAGE_MAX_CHARS = 16_000
144
144
  LONG_OUTPUT_TEMP_MAX_FILES = 160
145
+ READ_FILE_DEFAULT_MAX_CHARS = 50_000
146
+ READ_FILE_HARD_MAX_CHARS = 120_000
147
+ READ_FILE_OVERVIEW_HEAD_LINES = 80
148
+ READ_FILE_SEARCH_MAX_MATCHES = 24
145
149
  JSON_FSYNC_ENABLED = str(os.getenv("AGENT_JSON_FSYNC", "true") or "true").strip().lower() not in {"0", "false", "no", "off"}
146
150
  RAG_LIBRARY_DIRNAME = "RAG_Library"
147
151
  RAG_ADMIN_PORT_OFFSET = 2
@@ -447,6 +451,13 @@ COMPACT_TIER3_PCT = 0.10 # <10%: tier 3 heavy
447
451
  # Absolute minimums — prevent percentage instability at low ctx_left
448
452
  COMPACT_TIER1_ABS = 3000
449
453
  COMPACT_TIER2_ABS = 1500
454
+ CONTEXT_COMPACT_INEFFECTIVE_COOLDOWN_SECONDS = max(
455
+ 5.0,
456
+ min(
457
+ 300.0,
458
+ float(str(os.getenv("AGENT_CONTEXT_COMPACT_INEFFECTIVE_COOLDOWN", "45") or "45")),
459
+ ),
460
+ )
450
461
  # File buffer
451
462
  FILE_BUFFER_CONTENT_THRESHOLD = 2000 # chars: content larger than this gets offloaded
452
463
  FILE_BUFFER_MAX_FILES = 500
@@ -6950,7 +6961,7 @@ Use this skill when the agent shows model degradation symptoms:
6950
6961
  2. Record concrete evidence (exact errors / statuses / turn pattern) before changing strategy.
6951
6962
 
6952
6963
  ## Recovery Workflow (Mandatory Order)
6953
- 1. If context may be missing, call `context_recall` first.
6964
+ 1. If context may be missing, call `context_recall` with `mode="summary"` first, then use `mode="search"` or `mode="window"` for focused evidence.
6954
6965
  2. Build or repair todo plan with 3-7 items (one `in_progress`) via `TodoWrite` or `TodoWriteRescue`.
6955
6966
  3. Enter strict execution mode:
6956
6967
  - execute exactly ONE tool call per round,
@@ -6982,7 +6993,7 @@ Return:
6982
6993
 
6983
6994
  # Fast Decision Rules
6984
6995
 
6985
- 1. Missing context -> `context_recall`.
6996
+ 1. Missing context -> `context_recall mode="summary"`, then `mode="search"` or `mode="window"` for focused evidence.
6986
6997
  2. No todo plan -> `TodoWriteRescue`.
6987
6998
  3. Repeated tool failure -> one-tool strict retry with smaller chunk.
6988
6999
  4. Still failing -> explicit blocker, stop loop.
@@ -7025,7 +7036,7 @@ Assess the error and pick the matching depth. This is the single most important
7025
7036
  | Signal | Depth | Budget | Strategy |
7026
7037
  |--------|-------|--------|----------|
7027
7038
  | Typo, missing import, syntax error | **Shallow** | 1-2 tool calls | Pattern-match fix directly from error message |
7028
- | Single clear exception with traceback | **Standard** | 3-6 tool calls | Trace call chain, read crash site ±20 lines, fix + verify |
7039
+ | Single clear exception with traceback | **Standard** | 3-6 tool calls | Trace call chain, read the exact crash construct, fix + verify |
7029
7040
  | Intermittent / multi-component / no clear trace | **Deep** | 8-15 tool calls | Hypothesize → isolate → instrument → validate causal chain |
7030
7041
  | Reproduces only under specific state / concurrency | **Forensic** | 15-25 tool calls | State reconstruction, bisect, invariant analysis |
7031
7042
 
@@ -7047,7 +7058,7 @@ Every bug has a causal chain: **trigger → propagation → manifestation**. Mos
7047
7058
 
7048
7059
  ### Step 3: Targeted Investigation
7049
7060
  - Read ONLY the code that your hypothesis predicts is involved.
7050
- - Use `read_file` with offset/limit read the crash site ±20 lines, not the whole file.
7061
+ - Use `read_file` in the smallest mode that fits the question: `window` for file:line, `symbol` for functions/classes, `search` for locating evidence, and `full` only when exact broad context is needed.
7051
7062
  - If hypothesis is wrong, update it based on what you learned. Don't restart from scratch.
7052
7063
 
7053
7064
  ### Step 4: Fix at the Trigger, Not the Symptom
@@ -7404,7 +7415,7 @@ def ensure_generated_smart_file_navigation_skill(skills_root: Path):
7404
7415
  root = generated_root / "smart-file-navigation"
7405
7416
  skill_md = """---
7406
7417
  name: smart-file-navigation
7407
- description: Adaptive codebase exploration engine that scales reading strategy to project size and task scope — from surgical line-range reads to systematic dependency-graph traversal, with built-in loop prevention and workspace awareness.
7418
+ description: Adaptive codebase exploration engine that scales reading strategy to project size and task scope — from surgical symbol/window reads to systematic dependency-graph traversal, with question-driven navigation and workspace awareness.
7408
7419
  ---
7409
7420
 
7410
7421
  # Smart File Navigation
@@ -7418,7 +7429,7 @@ Decide your reading strategy BEFORE opening any file. Wrong strategy wastes your
7418
7429
 
7419
7430
  | Task Scope | Strategy | Read Budget | Key Principle |
7420
7431
  |-----------|----------|-------------|---------------|
7421
- | Fix a specific error with file:line | **Surgical** | 2-4 reads | Read crash site ±20 lines. Follow ONE call chain. |
7432
+ | Fix a specific error with file:line | **Surgical** | 2-4 reads | Read the exact crash construct. Follow ONE call chain. |
7422
7433
  | Implement feature in known area | **Focused** | 5-10 reads | Scan interfaces of affected modules. Read implementations you'll modify. |
7423
7434
  | Understand unfamiliar module | **Exploratory** | 8-15 reads | Structure scan → entry points → data flow → key abstractions. |
7424
7435
  | Full codebase assessment | **Systematic** | 15-25 reads | Top-down: build config → architecture → module boundaries → hot paths. |
@@ -7430,7 +7441,7 @@ Decide your reading strategy BEFORE opening any file. Wrong strategy wastes your
7430
7441
  ### The Navigation Loop
7431
7442
  1. **State your question**: "What does function X do?" / "Where is Y defined?" / "How does data flow from A to B?"
7432
7443
  2. **Predict the answer's location**: Based on naming conventions, directory structure, imports.
7433
- 3. **Read the minimum needed**: Use offset/limit. Never read 500 lines when 30 suffice.
7444
+ 3. **Read the right shape of context**: use `overview`, `symbol`, `search`, `window`, or `full` according to the question.
7434
7445
  4. **Record the answer**: Note it in your reasoning. Don't re-read to "remember".
7435
7446
  5. **Derive the next question**: Each answer either resolves your task or generates a more specific question.
7436
7447
 
@@ -7440,10 +7451,10 @@ If step 5 generates the SAME question you already answered → you're in a loop.
7440
7451
 
7441
7452
  | File Size | Strategy |
7442
7453
  |-----------|----------|
7443
- | < 150 lines | Read entire file it's cheap |
7444
- | 150-500 lines | Read first 30 lines (imports, class defs), then jump to target with offset |
7445
- | 500-2000 lines | Grep for the specific function/class, read 50-line window around match |
7446
- | 2000+ lines | NEVER read more than 100 lines at a time. Grep offset → targeted read |
7454
+ | < 150 lines | Read the whole file if that answers the question |
7455
+ | 150-500 lines | Read the relevant symbol or window around the target |
7456
+ | 500-2000 lines | Use `search` or `symbol` to locate the exact region, then read a focused window |
7457
+ | 2000+ lines | Use `overview` first, then `search`, `symbol`, or `window` for the exact region |
7447
7458
 
7448
7459
  ## Dependency Tracing
7449
7460
 
@@ -7467,7 +7478,7 @@ When you see an import, make a TRIAGE decision:
7467
7478
  ## Error-Driven Navigation
7468
7479
 
7469
7480
  When you have an error with a location:
7470
- 1. Read `file:line` with offset = line-10, limit = 30. This gives you the crash site with context.
7481
+ 1. Read `file:line` with `mode="window"` and enough context to see the surrounding construct.
7471
7482
  2. Identify the VARIABLE or EXPRESSION that caused the error.
7472
7483
  3. Trace BACKWARD: where was that variable last assigned? Read THAT location.
7473
7484
  4. If the assignment depends on another function's return value, read THAT function (just the return statements).
@@ -7477,11 +7488,8 @@ When you have an error with a location:
7477
7488
 
7478
7489
  ### Self-Monitoring Rules
7479
7490
  - **Track what you've read**: After each read, note "file X, lines Y-Z, learned: ...".
7480
- - **Never re-read the same file range**: If you already read lines 50-100 of foo.py, you have that information. Use it.
7481
- - **The 3-read limit**: If you've read 3 different files without taking ANY action (write, edit, bash), you're probably lost. Stop reading and:
7482
- 1. Write down what you know so far.
7483
- 2. Identify the SPECIFIC gap in your knowledge.
7484
- 3. Take an action based on what you know (even if imperfect).
7491
+ - **Avoid pointless rereads**: If you already know the answer from a range, move to the next missing question instead of reopening the same slice.
7492
+ - **If you keep circling the same question**, pause, summarize what you know, and take an action based on the current hypothesis.
7485
7493
 
7486
7494
  ### Recovery from Navigation Dead Ends
7487
7495
  If you can't find what you're looking for:
@@ -8821,7 +8829,7 @@ clouds_coder:
8821
8829
  - bash
8822
8830
  description: >
8823
8831
  Specialized guide for code library retrieval. Code-specific query patterns,
8824
- language filtering, symbol-aware search, and integration with read_file for full context.
8832
+ language filtering, symbol-aware search, and integration with read_file for focused context.
8825
8833
  TRIGGER when: looking up code implementations, function signatures, API patterns, code review.
8826
8834
  DO NOT TRIGGER for: knowledge/document retrieval (use rag-retrieval-mastery),
8827
8835
  general research (use research-orchestrator-pro).
@@ -8886,7 +8894,7 @@ Code library returns **snippets** (320 chars max). To get full context:
8886
8894
 
8887
8895
  1. **Query**: `result = query_code_library(query="parse config file", top_k=5)`
8888
8896
  2. **Extract path**: Read `citation` field → contains file path
8889
- 3. **Read full file**: `read_file` on the extracted path
8897
+ 3. **Read the best context**: use `read_file` with `mode="symbol"` for named code, `mode="window"` for nearby lines, `mode="search"` for evidence, or `mode="full"` when broad exact context is needed
8890
8898
  4. **Analyze**: Now you have the full function/class context
8891
8899
 
8892
8900
  This is the core workflow: **search → locate → read → understand**.
@@ -10062,7 +10070,7 @@ _BUILTIN_SKILLS: dict[str, dict] = {
10062
10070
  "# Context Management Guide\n"
10063
10071
  "- Context has a token upper bound; keep steps compact.\n"
10064
10072
  "- When <compact-resume> hint appears, inherit pending todos and continue immediately.\n"
10065
- "- After compaction, use context_recall to fetch archived messages by segment_id/query.\n"
10073
+ "- After compaction, use context_recall mode='summary' to map archived messages, then mode='search' or mode='window' for focused evidence.\n"
10066
10074
  "- Do not guess content that was compacted away—recall it first.\n"
10067
10075
  "- For large tasks, break into subtasks to avoid context overflow.\n"
10068
10076
  ),
@@ -11676,6 +11684,16 @@ class MessageBus:
11676
11684
  out.append(row)
11677
11685
  return out
11678
11686
 
11687
+ def peek_inbox(self, name: str) -> list[dict]:
11688
+ path = self._path(name)
11689
+ if not path.exists():
11690
+ return []
11691
+ with self.lock:
11692
+ payload = self.crypto.read_json(path, [])
11693
+ if not isinstance(payload, list):
11694
+ payload = []
11695
+ return [row for row in payload if isinstance(row, dict)]
11696
+
11679
11697
  def broadcast(self, sender: str, content: str, names: list[str]) -> str:
11680
11698
  count = 0
11681
11699
  for name in names:
@@ -11898,6 +11916,10 @@ class WorktreeManager:
11898
11916
  out = payload[-n:] if isinstance(payload, list) else []
11899
11917
  return json_dumps(out, indent=2)
11900
11918
 
11919
+ def events_objects(self) -> list[dict]:
11920
+ payload = self.crypto.read_json(self.events_path, [])
11921
+ return [row for row in payload if isinstance(row, dict)] if isinstance(payload, list) else []
11922
+
11901
11923
  class OllamaError(RuntimeError):
11902
11924
  def __init__(
11903
11925
  self,
@@ -13407,8 +13429,30 @@ TOOLS = [
13407
13429
  tool_def("bash", "Run a shell command.", {"command": {"type": "string"}}, ["command"]),
13408
13430
  tool_def(
13409
13431
  "read_file",
13410
- "Read file content with optional line pagination.",
13411
- {"path": {"type": "string"}, "limit": {"type": "integer"}, "offset": {"type": "integer"}},
13432
+ (
13433
+ "Read files or directories with structure-aware modes. "
13434
+ "Examples: large.py + func_42 -> mode='symbol' target='func_42'; "
13435
+ "app.py line 240 -> mode='window' line=240 context=5; "
13436
+ "run.txt E123 -> mode='search' query='E123'. "
13437
+ "Use mode='auto' by default; use mode='symbol', 'search', or 'window' for focused reads, "
13438
+ "and mode='full' when complete content is explicitly needed."
13439
+ ),
13440
+ {
13441
+ "path": {"type": "string"},
13442
+ "mode": {
13443
+ "type": "string",
13444
+ "enum": ["auto", "full", "overview", "window", "symbol", "search", "directory"],
13445
+ "description": "Reading strategy. Use symbol with target for a function/class; search with query for known text/errors; window with line/context for a line range. Avoid full for large logs when a query is known.",
13446
+ },
13447
+ "target": {"type": "string", "description": "Symbol name for mode='symbol', for example 'ClassName.method' or 'func_42'."},
13448
+ "query": {"type": "string", "description": "Search text or regex for mode='search'; can also be used when target is unknown."},
13449
+ "line": {"type": "integer", "description": "1-based center line for mode='window'."},
13450
+ "context": {"type": "integer", "description": "Number of surrounding lines for mode='window' or mode='search'."},
13451
+ "regex": {"type": "boolean", "description": "Treat query as a regular expression in mode='search'."},
13452
+ "max_chars": {"type": "integer", "description": "Maximum characters to return for broad reads; use only when wider context is needed."},
13453
+ "limit": {"type": "integer", "description": "Legacy line count for compatibility; prefer mode/context for new calls."},
13454
+ "offset": {"type": "integer", "description": "Legacy 0-based line offset for compatibility; prefer mode='window' with line/context."},
13455
+ },
13412
13456
  ["path"],
13413
13457
  ),
13414
13458
  tool_def("write_file", "Write file content.", {"path": {"type": "string"}, "content": {"type": "string"}}, ["path", "content"]),
@@ -13451,15 +13495,26 @@ TOOLS = [
13451
13495
  ),
13452
13496
  tool_def("scan_skills", "Force reload skills from ./skills and return summary.", {}),
13453
13497
  tool_def("compress", "Compress conversation context.", {}),
13454
- tool_def(
13455
- "context_recall",
13456
- "Recall archived compacted context by segment id, query, or recent segments.",
13498
+ tool_def(
13499
+ "context_recall",
13500
+ (
13501
+ "Recall archived compacted context with focused modes. "
13502
+ "Use this tool, not read_from_blackboard, when the user says archived context, compacted context, previous segment, or context segment. "
13503
+ "Use mode='summary' for a map, 'search' with query/tool_name/role for evidence, "
13504
+ "or 'window' with around_index/context for nearby messages."
13505
+ ),
13457
13506
  {
13507
+ "mode": {"type": "string", "enum": ["summary", "search", "recent", "window", "detail"]},
13458
13508
  "segment_id": {"type": "string"},
13459
13509
  "query": {"type": "string"},
13510
+ "role": {"type": "string"},
13511
+ "tool_name": {"type": "string"},
13512
+ "around_index": {"type": "integer"},
13513
+ "context": {"type": "integer"},
13514
+ "max_chars": {"type": "integer"},
13460
13515
  "recent_segments": {"type": "integer"},
13461
13516
  "max_messages": {"type": "integer"},
13462
- "offset": {"type": "integer"},
13517
+ "offset": {"type": "integer", "description": "Legacy offset for compatibility; prefer query or around_index/context."},
13463
13518
  "include_tools": {"type": "boolean"},
13464
13519
  },
13465
13520
  ),
@@ -13511,11 +13566,37 @@ TOOLS = [
13511
13566
  ["media_type", "prompt"],
13512
13567
  ),
13513
13568
  tool_def("background_run", "Run command in background.", {"command": {"type": "string"}, "timeout": {"type": "integer"}}, ["command"]),
13514
- tool_def("check_background", "Check background tasks.", {"task_id": {"type": "string"}}),
13569
+ tool_def(
13570
+ "check_background",
13571
+ (
13572
+ "Inspect background tasks with summary/search/detail/tail modes. "
13573
+ "If the user names an error token, command text, or output text such as E123 or pytest, use mode='search' with query. "
13574
+ "Use tail only for recent jobs when no query is known."
13575
+ ),
13576
+ {
13577
+ "task_id": {"type": "string"},
13578
+ "mode": {"type": "string", "enum": ["summary", "search", "detail", "tail"]},
13579
+ "query": {"type": "string"},
13580
+ "status": {"type": "string"},
13581
+ "limit": {"type": "integer"},
13582
+ "max_chars": {"type": "integer"},
13583
+ },
13584
+ ),
13515
13585
  tool_def("task_create", "Create task.", {"subject": {"type": "string"}, "description": {"type": "string"}}, ["subject"]),
13516
13586
  tool_def("task_get", "Get task.", {"task_id": {"type": "integer"}}, ["task_id"]),
13517
13587
  tool_def("task_update", "Update task.", {"task_id": {"type": "integer"}, "status": {"type": "string"}, "add_blocked_by": {"type": "array", "items": {"type": "integer"}}, "add_blocks": {"type": "array", "items": {"type": "integer"}}}, ["task_id"]),
13518
- tool_def("task_list", "List tasks.", {}),
13588
+ tool_def(
13589
+ "task_list",
13590
+ "List tasks with optional status/owner/query filters and summary/detail modes.",
13591
+ {
13592
+ "mode": {"type": "string", "enum": ["summary", "search", "detail", "recent"]},
13593
+ "query": {"type": "string"},
13594
+ "status": {"type": "string"},
13595
+ "owner": {"type": "string"},
13596
+ "limit": {"type": "integer"},
13597
+ "max_chars": {"type": "integer"},
13598
+ },
13599
+ ),
13519
13600
  tool_def("claim_task", "Claim task.", {"task_id": {"type": "integer"}}, ["task_id"]),
13520
13601
  tool_def("spawn_teammate", "Spawn teammate thread.", {"name": {"type": "string"}, "role": {"type": "string"}, "prompt": {"type": "string"}}, ["name", "role", "prompt"]),
13521
13602
  tool_def("list_teammates", "List teammates.", {}),
@@ -13529,9 +13610,13 @@ TOOLS = [
13529
13610
  },
13530
13611
  ["to", "intent", "content"],
13531
13612
  ),
13532
- tool_def(
13533
- "read_from_blackboard",
13534
- "Read current shared blackboard state or one section.",
13613
+ tool_def(
13614
+ "read_from_blackboard",
13615
+ (
13616
+ "Read shared multi-agent blackboard state with summary/search/recent/window/detail modes. "
13617
+ "Use this for research_notes, execution_logs, review_feedback, code_artifacts, and status; "
13618
+ "use context_recall instead for archived or compacted conversation context."
13619
+ ),
13535
13620
  {
13536
13621
  "section": {
13537
13622
  "type": "string",
@@ -13546,6 +13631,13 @@ TOOLS = [
13546
13631
  "status",
13547
13632
  ],
13548
13633
  },
13634
+ "mode": {"type": "string", "enum": ["summary", "search", "recent", "window", "detail"]},
13635
+ "query": {"type": "string"},
13636
+ "actor": {"type": "string"},
13637
+ "status": {"type": "string"},
13638
+ "around_index": {"type": "integer"},
13639
+ "context": {"type": "integer"},
13640
+ "max_chars": {"type": "integer"},
13549
13641
  "limit": {"type": "integer"},
13550
13642
  },
13551
13643
  ),
@@ -13574,7 +13666,18 @@ TOOLS = [
13574
13666
  ["section", "content"],
13575
13667
  ),
13576
13668
  tool_def("send_message", "Send message.", {"to": {"type": "string"}, "content": {"type": "string"}, "msg_type": {"type": "string"}}, ["to", "content"]),
13577
- tool_def("read_inbox", "Read lead inbox.", {}),
13669
+ tool_def(
13670
+ "read_inbox",
13671
+ "Read lead inbox. Use mode='peek' to inspect without draining, mode='search' to find messages, or mode='drain' to consume them.",
13672
+ {
13673
+ "mode": {"type": "string", "enum": ["peek", "drain", "search", "summary"]},
13674
+ "query": {"type": "string"},
13675
+ "from": {"type": "string"},
13676
+ "type": {"type": "string"},
13677
+ "limit": {"type": "integer"},
13678
+ "max_chars": {"type": "integer"},
13679
+ },
13680
+ ),
13578
13681
  tool_def("broadcast", "Broadcast to teammates.", {"content": {"type": "string"}}, ["content"]),
13579
13682
  tool_def("shutdown_request", "Request teammate shutdown.", {"teammate": {"type": "string"}}, ["teammate"]),
13580
13683
  tool_def("plan_approval", "Respond to plan approval request.", {"request_id": {"type": "string"}, "approve": {"type": "boolean"}, "feedback": {"type": "string"}}, ["request_id", "approve"]),
@@ -13584,7 +13687,23 @@ TOOLS = [
13584
13687
  tool_def("worktree_run", "Run command in worktree.", {"name": {"type": "string"}, "command": {"type": "string"}}, ["name", "command"]),
13585
13688
  tool_def("worktree_keep", "Mark worktree kept.", {"name": {"type": "string"}}, ["name"]),
13586
13689
  tool_def("worktree_remove", "Remove worktree.", {"name": {"type": "string"}, "force": {"type": "boolean"}, "complete_task": {"type": "boolean"}}, ["name"]),
13587
- tool_def("worktree_events", "Read worktree lifecycle events.", {"limit": {"type": "integer"}}),
13690
+ tool_def(
13691
+ "worktree_events",
13692
+ (
13693
+ "Read worktree lifecycle events with summary/search/detail modes. "
13694
+ "For a worktree name like alpha, set worktree='alpha'; do not put worktree names in event. "
13695
+ "For known failure text use mode='search' and query='failed' or the exact error; event is for lifecycle names like worktree.remove.failed."
13696
+ ),
13697
+ {
13698
+ "mode": {"type": "string", "enum": ["summary", "search", "recent", "detail"]},
13699
+ "query": {"type": "string"},
13700
+ "event": {"type": "string"},
13701
+ "worktree": {"type": "string"},
13702
+ "task_id": {"type": "integer"},
13703
+ "limit": {"type": "integer"},
13704
+ "max_chars": {"type": "integer"},
13705
+ },
13706
+ ),
13588
13707
  ]
13589
13708
 
13590
13709
  TOOL_REQUIRED_ARGS: dict[str, list[str]] = {}
@@ -13926,6 +14045,15 @@ class SessionState:
13926
14045
  self.context_limit_locked = bool(context_limit_locked)
13927
14046
  self.context_token_upper_bound = self.max_context_token_limit
13928
14047
  self.context_estimate_calibration = float(CONTEXT_ESTIMATE_SAFETY_MULTIPLIER)
14048
+ self.context_limit_source = "configured"
14049
+ self.context_last_compact_before: dict = {}
14050
+ self.context_last_compact_after: dict = {}
14051
+ self.context_last_compact_effective = True
14052
+ self.context_last_compact_used_reduction = 0
14053
+ self.context_last_compact_skip_ts = 0.0
14054
+ self.context_last_compact_skip_reason = ""
14055
+ self.context_last_next_call_estimate = 0
14056
+ self.context_last_next_call_label = ""
13929
14057
  self.last_context_actual_prompt_tokens = 0
13930
14058
  self.last_context_actual_completion_tokens = 0
13931
14059
  self.last_context_actual_total_tokens = 0
@@ -14838,6 +14966,35 @@ class SessionState:
14838
14966
  )
14839
14967
  except Exception:
14840
14968
  self.context_estimate_calibration = float(CONTEXT_ESTIMATE_SAFETY_MULTIPLIER)
14969
+ self.context_limit_source = trim(str(raw.get("context_limit_source", self.context_limit_source) or "configured"), 80)
14970
+ self.context_last_compact_before = (
14971
+ dict(raw.get("context_last_compact_before", {}) or {})
14972
+ if isinstance(raw.get("context_last_compact_before", {}), dict)
14973
+ else {}
14974
+ )
14975
+ self.context_last_compact_after = (
14976
+ dict(raw.get("context_last_compact_after", {}) or {})
14977
+ if isinstance(raw.get("context_last_compact_after", {}), dict)
14978
+ else {}
14979
+ )
14980
+ self.context_last_compact_effective = bool(
14981
+ raw.get("context_last_compact_effective", self.context_last_compact_effective)
14982
+ )
14983
+ self.context_last_compact_used_reduction = max(
14984
+ 0, int(raw.get("context_last_compact_used_reduction", 0) or 0)
14985
+ )
14986
+ self.context_last_compact_skip_ts = max(
14987
+ 0.0, float(raw.get("context_last_compact_skip_ts", 0.0) or 0.0)
14988
+ )
14989
+ self.context_last_compact_skip_reason = trim(
14990
+ str(raw.get("context_last_compact_skip_reason", "") or ""), 160
14991
+ )
14992
+ self.context_last_next_call_estimate = max(
14993
+ 0, int(raw.get("context_last_next_call_estimate", 0) or 0)
14994
+ )
14995
+ self.context_last_next_call_label = trim(
14996
+ str(raw.get("context_last_next_call_label", "") or ""), 80
14997
+ )
14841
14998
  self.last_context_actual_prompt_tokens = max(0, int(raw.get("last_context_actual_prompt_tokens", 0) or 0))
14842
14999
  self.last_context_actual_completion_tokens = max(0, int(raw.get("last_context_actual_completion_tokens", 0) or 0))
14843
15000
  self.last_context_actual_total_tokens = max(0, int(raw.get("last_context_actual_total_tokens", 0) or 0))
@@ -15192,6 +15349,15 @@ class SessionState:
15192
15349
  "thinking": self.thinking,
15193
15350
  "context_limit_locked": bool(self.context_limit_locked),
15194
15351
  "context_estimate_calibration": float(getattr(self, "context_estimate_calibration", CONTEXT_ESTIMATE_SAFETY_MULTIPLIER) or CONTEXT_ESTIMATE_SAFETY_MULTIPLIER),
15352
+ "context_limit_source": str(getattr(self, "context_limit_source", "") or "configured"),
15353
+ "context_last_compact_before": dict(getattr(self, "context_last_compact_before", {}) or {}),
15354
+ "context_last_compact_after": dict(getattr(self, "context_last_compact_after", {}) or {}),
15355
+ "context_last_compact_effective": bool(getattr(self, "context_last_compact_effective", True)),
15356
+ "context_last_compact_used_reduction": int(getattr(self, "context_last_compact_used_reduction", 0) or 0),
15357
+ "context_last_compact_skip_ts": float(getattr(self, "context_last_compact_skip_ts", 0.0) or 0.0),
15358
+ "context_last_compact_skip_reason": str(getattr(self, "context_last_compact_skip_reason", "") or ""),
15359
+ "context_last_next_call_estimate": int(getattr(self, "context_last_next_call_estimate", 0) or 0),
15360
+ "context_last_next_call_label": str(getattr(self, "context_last_next_call_label", "") or ""),
15195
15361
  "last_context_actual_prompt_tokens": int(getattr(self, "last_context_actual_prompt_tokens", 0) or 0),
15196
15362
  "last_context_actual_completion_tokens": int(getattr(self, "last_context_actual_completion_tokens", 0) or 0),
15197
15363
  "last_context_actual_total_tokens": int(getattr(self, "last_context_actual_total_tokens", 0) or 0),
@@ -16128,6 +16294,80 @@ class SessionState:
16128
16294
  loaded = bb.get("loaded_skills", {})
16129
16295
  return loaded if isinstance(loaded, dict) else {}
16130
16296
 
16297
+ def _loaded_skill_body_from_cache(self, skill_key: str, row: dict | None = None) -> str:
16298
+ key = str(skill_key or "").strip()
16299
+ if not key:
16300
+ return ""
16301
+ cache_row = self.skill_load_cache.get(key, {})
16302
+ if isinstance(cache_row, dict):
16303
+ body_z = str(cache_row.get("body_z", "") or "")
16304
+ if body_z:
16305
+ body = decompress_text_blob(body_z)
16306
+ if body:
16307
+ return body
16308
+ meta_row = row if isinstance(row, dict) else {}
16309
+ skill_path = str(meta_row.get("skill_path", "") or "").strip()
16310
+ if skill_path:
16311
+ try:
16312
+ path_obj = Path(skill_path)
16313
+ if path_obj.exists() and path_obj.is_file():
16314
+ return path_obj.read_text(encoding="utf-8", errors="replace")
16315
+ except Exception:
16316
+ pass
16317
+ return ""
16318
+
16319
+ def _loaded_skills_context_block(self, *, for_role: str = "", max_chars: int = 7000) -> str:
16320
+ """Bounded active-skill instructions rehydrated on every prompt.
16321
+
16322
+ The original <loaded-skill> message may be compacted, so each model
16323
+ call gets a small workflow-critical copy from the skill cache/path.
16324
+ """
16325
+ loaded = self._loaded_skill_rows()
16326
+ if not loaded:
16327
+ return ""
16328
+ budget = max(800, int(max_chars or 7000))
16329
+ parts: list[str] = [
16330
+ "ACTIVE SKILL WORKFLOWS (rehydrated):",
16331
+ "Treat these instructions as active operating procedure for matching steps. "
16332
+ "If they conflict with a generic plan, the skill workflow wins.",
16333
+ ]
16334
+ remaining = budget
16335
+ for skill_key, row_obj in list(loaded.items())[:5]:
16336
+ row = row_obj if isinstance(row_obj, dict) else {}
16337
+ skill_name = str(row.get("skill_name", skill_key) or skill_key).strip() or skill_key
16338
+ skill_path = str(row.get("skill_path", "") or "").strip()
16339
+ body = self._loaded_skill_body_from_cache(str(skill_key), row)
16340
+ preview = trim(str(row.get("preview", "") or ""), 600)
16341
+ source = body or preview
16342
+ if not source:
16343
+ continue
16344
+ per_skill = max(600, min(2600, remaining // max(1, len(loaded))))
16345
+ excerpt = trim(source, per_skill)
16346
+ header = f"\n<active-skill name=\"{skill_name}\" key=\"{skill_key}\""
16347
+ if skill_path:
16348
+ header += f" path=\"{skill_path}\""
16349
+ header += ">"
16350
+ block = f"{header}\n{excerpt}\n</active-skill>"
16351
+ parts.append(block)
16352
+ remaining -= max(1, len(block))
16353
+ if remaining <= 600:
16354
+ break
16355
+ if len(parts) <= 2:
16356
+ return ""
16357
+ role_note = ""
16358
+ role_key = str(for_role or "").strip().lower()
16359
+ if role_key == "manager":
16360
+ role_note = (
16361
+ "\nManager duty: delegate with the concrete tools/scripts/files from the active skill; "
16362
+ "do not replace the skill workflow with a generic approach."
16363
+ )
16364
+ elif role_key:
16365
+ role_note = (
16366
+ "\nWorker duty: before acting, map the current step to the active skill workflow and "
16367
+ "use the specified tools/scripts/files when applicable."
16368
+ )
16369
+ return trim("\n".join(parts) + role_note + "\n", budget)
16370
+
16131
16371
  def _clear_loaded_skill_contexts(self):
16132
16372
  def _filter_rows(rows: list[dict]) -> list[dict]:
16133
16373
  kept: list[dict] = []
@@ -16405,8 +16645,6 @@ class SessionState:
16405
16645
  ({"ppt", "ppt-master", "pptx", "academic-pptx", "slide-making-skill"}, "Presentation/PPT processing"),
16406
16646
  # Research orchestrators
16407
16647
  ({"deep-research-orchestrator", "research-orchestrator-pro"}, "Research orchestration"),
16408
- # Frontend design
16409
- ({"frontend-design", "frontend-composition-algorithm", "canvas-design", "web-artifacts-builder"}, "Frontend/web design"),
16410
16648
  ]
16411
16649
  # Normalize: strip common prefixes/suffixes for matching
16412
16650
  def _norm(name: str) -> set[str]:
@@ -16538,7 +16776,9 @@ class SessionState:
16538
16776
  Keeps all three modes in sync — change here propagates everywhere.
16539
16777
  """
16540
16778
  hint = self._loaded_skills_prompt_hint(for_role=for_role)
16541
- return f"{hint}\nSkills:\n{self.skills.descriptions()}\n"
16779
+ active = self._loaded_skills_context_block(for_role=for_role, max_chars=6500)
16780
+ active_block = f"\n{active}\n" if active else "\n"
16781
+ return f"{hint}{active_block}Skills:\n{self.skills.descriptions()}\n"
16542
16782
 
16543
16783
  def _refresh_runtime_code_reference(self, text: str):
16544
16784
  cb = getattr(self, "reference_prepare_callback", None)
@@ -16763,6 +17003,8 @@ class SessionState:
16763
17003
  "Use load_skill for workspace-paths and tool-best-practices if needed. "
16764
17004
  "Use list_skills to discover available skills for specific tasks. "
16765
17005
  )
17006
+ skill_context_block = self._loaded_skills_context_block(for_role="developer", max_chars=7000)
17007
+ skill_context = f"{skill_context_block}\n" if skill_context_block else ""
16766
17008
  plan_steps_block = ""
16767
17009
  plan_ctx = self._plan_steps_context_for_manager()
16768
17010
  if plan_ctx:
@@ -16786,9 +17028,13 @@ class SessionState:
16786
17028
  f"Context limit ~{max(1, int(self.context_token_upper_bound) - max(1, int(math.ceil(int(self.context_token_upper_bound) * CONTEXT_AUTO_COMPACT_RESERVE_RATIO))))} usable tokens "
16787
17029
  f"(5% reserved for auto compact; raw upper bound ~{self.context_token_upper_bound}). "
16788
17030
  f"{_detect_os_shell_instruction()} "
16789
- "Use tools to inspect, edit, and execute. "
17031
+ "Use tools to inspect, edit, and execute. "
17032
+ "If you say you will create, write, build, copy, modify, or verify an artifact, the same turn must include the concrete tool call that does it; do not stop at a promise to act. "
17033
+ "When reading files, choose the shape that matches the question: mode='window' for file:line, mode='symbol' for named code, mode='search' for keywords/errors, mode='overview' for structure, and mode='full' only when exact broad context is required. "
17034
+ "When inspecting collections or memory, use focused modes too: context_recall/read_from_blackboard/task_list/check_background/read_inbox/worktree_events support mode='summary', mode='search', mode='window', and mode='detail' where applicable. Prefer query/status/actor/tool filters over repeatedly listing recent items. "
16790
17035
  "Call finish_current_task only when the overall user task is done. "
16791
17036
  f"{skill_hint}"
17037
+ f"{skill_context}"
16792
17038
  f"{mm_hint}"
16793
17039
  f"{plan_steps_block}"
16794
17040
  f"{plan_todo_block}"
@@ -17047,8 +17293,199 @@ class SessionState:
17047
17293
  "last_actual_age_seconds": round(float(actual_age), 3) if getattr(self, "last_context_actual_ts", 0.0) else None,
17048
17294
  "last_estimated_prompt_tokens": int(actual_estimate_at_call),
17049
17295
  "calibration_max": float(CONTEXT_USAGE_CALIBRATION_MAX),
17296
+ "limit_source": str(getattr(self, "context_limit_source", "") or "configured"),
17050
17297
  }
17051
17298
 
17299
+ def _context_window_error_hint(self, exc: Exception | str) -> bool:
17300
+ text = str(exc or "").lower()
17301
+ if not text:
17302
+ return False
17303
+ markers = (
17304
+ "context length",
17305
+ "context window",
17306
+ "maximum context",
17307
+ "max context",
17308
+ "num_ctx",
17309
+ "prompt too long",
17310
+ "too many tokens",
17311
+ "token limit",
17312
+ "input is too long",
17313
+ "reduce the length",
17314
+ "exceeds the context",
17315
+ "exceeded context",
17316
+ "context overflow",
17317
+ )
17318
+ return any(marker in text for marker in markers)
17319
+
17320
+ def _shrink_context_upper_bound_from_actual_pressure(
17321
+ self,
17322
+ *,
17323
+ estimated_prompt_tokens: int,
17324
+ reason: str,
17325
+ ) -> bool:
17326
+ if self.context_limit_locked:
17327
+ return False
17328
+ old_bound = int(self.context_token_upper_bound or self.max_context_token_limit)
17329
+ prompt = max(MIN_CONTEXT_TOKEN_LIMIT, int(estimated_prompt_tokens or 0))
17330
+ if prompt <= 0:
17331
+ return False
17332
+ # This is only used after the provider reports a context-window failure.
17333
+ # Leave enough room for recovery prompts, but never infer model context from output length.
17334
+ new_bound = max(MIN_CONTEXT_TOKEN_LIMIT, min(old_bound - 1, int(prompt * 0.96)))
17335
+ if new_bound >= old_bound:
17336
+ return False
17337
+ self.context_token_upper_bound = new_bound
17338
+ self.context_limit_source = f"provider-context-error:{trim(str(reason or 'unknown'), 48)}"
17339
+ self._emit(
17340
+ "status",
17341
+ {
17342
+ "summary": (
17343
+ "context upper bound adjusted from provider context error "
17344
+ f"{old_bound}->{new_bound} (estimated_prompt≈{prompt})"
17345
+ )
17346
+ },
17347
+ )
17348
+ return True
17349
+
17350
+ def _context_metrics_for_model_call(
17351
+ self,
17352
+ messages: list[dict],
17353
+ *,
17354
+ tools: list | None = None,
17355
+ system: str = "",
17356
+ media_inputs: list[dict] | None = None,
17357
+ label: str = "",
17358
+ record: bool = True,
17359
+ ) -> dict:
17360
+ estimate = self._estimate_model_call_prompt_tokens(
17361
+ messages if isinstance(messages, list) else [],
17362
+ tools=tools,
17363
+ system=system,
17364
+ media_inputs=media_inputs,
17365
+ )
17366
+ label_text = trim(str(label or ""), 80)
17367
+ if record:
17368
+ self.context_last_next_call_estimate = int(estimate)
17369
+ self.context_last_next_call_label = label_text
17370
+ metrics = self._context_budget_metrics(token_estimate=estimate)
17371
+ metrics["estimate_source"] = "next-model-call"
17372
+ metrics["next_call_label"] = label_text
17373
+ return metrics
17374
+
17375
+ def _role_specific_context_is_live_call(self, role: str) -> bool:
17376
+ role_key = self._sanitize_agent_role(role)
17377
+ if not role_key:
17378
+ return False
17379
+ if self._is_multi_agent_mode():
17380
+ return True
17381
+ phase = str(self.current_phase or "").strip().lower()
17382
+ active = str(self.active_agent_role or "").strip().lower()
17383
+ return bool(
17384
+ phase.startswith("plan-mode:")
17385
+ and active == role_key
17386
+ and bool(self._agent_context(role_key))
17387
+ )
17388
+
17389
+ def _active_next_call_context_metrics(self, role: str = "", media_inputs: list[dict] | None = None) -> dict:
17390
+ raw_role = str(role or self.active_agent_role or "").strip().lower()
17391
+ if raw_role == "manager":
17392
+ return self._context_metrics_for_model_call(
17393
+ self.manager_context,
17394
+ tools=self._manager_route_tools(),
17395
+ system=self._manager_system_prompt(),
17396
+ media_inputs=media_inputs,
17397
+ label="manager next turn",
17398
+ )
17399
+ role_key = self._sanitize_agent_role(raw_role)
17400
+ role_ctx = self._agent_context(role_key) if role_key in AGENT_ROLES else []
17401
+ if role_key in AGENT_ROLES and self._role_specific_context_is_live_call(role_key):
17402
+ return self._context_metrics_for_model_call(
17403
+ role_ctx,
17404
+ tools=self._tools_for_agent(role_key),
17405
+ system=self._agent_role_system_prompt(role_key),
17406
+ media_inputs=media_inputs,
17407
+ label=f"{role_key} next turn",
17408
+ )
17409
+ return self._context_metrics_for_model_call(
17410
+ self.messages,
17411
+ tools=TOOLS,
17412
+ system=self._system_prompt(),
17413
+ media_inputs=media_inputs,
17414
+ label="single-agent next turn",
17415
+ )
17416
+
17417
+ def _agent_context_budget_metrics_snapshot(self) -> list[dict]:
17418
+ rows: list[dict] = []
17419
+ active = str(self.active_agent_role or "").strip().lower()
17420
+ candidates: list[str] = []
17421
+ multi_mode = self._is_multi_agent_mode()
17422
+ plan_role_active = bool(
17423
+ (not multi_mode)
17424
+ and active in AGENT_ROLES
17425
+ and str(self.current_phase or "").strip().lower().startswith("plan-mode:")
17426
+ )
17427
+ if multi_mode:
17428
+ candidates.append("manager")
17429
+ if multi_mode:
17430
+ role_order = list(self.runtime_participants or [])
17431
+ for role in role_order:
17432
+ role_key = self._sanitize_agent_role(role)
17433
+ if role_key and role_key not in candidates:
17434
+ candidates.append(role_key)
17435
+ elif plan_role_active:
17436
+ candidates.append(active)
17437
+ if not candidates:
17438
+ candidates = ["single"]
17439
+ for role in candidates:
17440
+ try:
17441
+ if role == "manager":
17442
+ messages = self.manager_context
17443
+ tools = self._manager_route_tools()
17444
+ system = self._manager_system_prompt()
17445
+ label = "manager next turn"
17446
+ msg_count = len(self.manager_context)
17447
+ display = backend_role_label("manager", getattr(self, "ui_language", DEFAULT_UI_LANGUAGE))
17448
+ elif role in AGENT_ROLES:
17449
+ ctx = self._agent_context(role)
17450
+ messages = ctx
17451
+ tools = self._tools_for_agent(role)
17452
+ system = self._agent_role_system_prompt(role)
17453
+ label = f"{role} next turn"
17454
+ msg_count = len(ctx)
17455
+ display = self._agent_display_name(role)
17456
+ else:
17457
+ messages = self.messages
17458
+ tools = TOOLS
17459
+ system = self._system_prompt()
17460
+ label = "single-agent next turn"
17461
+ msg_count = len(self.messages)
17462
+ display = "Single"
17463
+ metrics = self._context_metrics_for_model_call(
17464
+ messages,
17465
+ tools=tools,
17466
+ system=system,
17467
+ label=label,
17468
+ record=False,
17469
+ )
17470
+ rows.append(
17471
+ {
17472
+ "role": role,
17473
+ "label": display,
17474
+ "active": bool(active == role),
17475
+ "used": int(metrics.get("used", 0) or 0),
17476
+ "left": int(metrics.get("left", 0) or 0),
17477
+ "left_percent": round(float(metrics.get("left_percent", 0.0)), 2),
17478
+ "effective_limit": int(metrics.get("effective_limit", metrics.get("limit", 0)) or 0),
17479
+ "tier": int(self._context_compression_tier(metrics)),
17480
+ "message_count": int(msg_count),
17481
+ "next_call_label": str(metrics.get("next_call_label", "") or label),
17482
+ }
17483
+ )
17484
+ except Exception:
17485
+ continue
17486
+ rows.sort(key=lambda x: (float(x.get("left_percent", 100.0)), int(x.get("left", 0) or 0)))
17487
+ return rows
17488
+
17052
17489
  def _context_compression_tier(self, metrics: dict | None = None) -> int:
17053
17490
  """Return compression tier 0-3 based on context budget.
17054
17491
  Tier 0: >40% left (normal)
@@ -17080,43 +17517,137 @@ class SessionState:
17080
17517
  "manager_context": [MANAGER_CTX_LIMIT_TIER0, MANAGER_CTX_LIMIT_TIER1, MANAGER_CTX_LIMIT_TIER2, MANAGER_CTX_LIMIT_TIER3][t],
17081
17518
  }
17082
17519
 
17083
- def _compact_agent_contexts(self, tier: int):
17084
- """Compress agent_messages, manager_context, and per-role contexts based on tier."""
17085
- limits = self._tier_agent_context_limits(tier)
17086
- am_limit = limits["agent_messages"]
17087
- ctx_limit = limits["context_per_role"]
17088
- mgr_limit = limits["manager_context"]
17089
- # 1. Microcompact agent messages (tier >= 1)
17090
- if tier >= 1:
17091
- keep = max(1, 3 - tier) # tier1=2, tier2=1, tier3=0(but min 1)
17092
- self._microcompact_agent_messages(self.agent_messages, keep_recent=keep)
17093
- # 2. Trim agent_messages to limit
17094
- if len(self.agent_messages) > am_limit:
17095
- self.agent_messages = self.agent_messages[-am_limit:]
17096
- # 3. Trim manager_context
17097
- if len(self.manager_context) > mgr_limit:
17098
- self.manager_context = self.manager_context[-mgr_limit:]
17099
- # 4. Trim per-role contexts
17100
- for role in AGENT_ROLES:
17101
- ctx = self.contexts.get(role, [])
17102
- if len(ctx) > ctx_limit:
17103
- self.contexts[role] = ctx[-ctx_limit:]
17104
- # 5. Tier 2+: offload large content in agent messages to file buffer
17520
+ def _loaded_skill_stub_content(self, content: str) -> str:
17521
+ raw = str(content or "")
17522
+ if "<loaded-skill name=" not in raw:
17523
+ return raw
17524
+ match = re.search(r'<loaded-skill\s+name=["\']([^"\']+)["\']', raw)
17525
+ key = str(match.group(1) if match else "").strip()
17526
+ row = self._loaded_skill_rows().get(key, {}) if key else {}
17527
+ skill_name = str((row if isinstance(row, dict) else {}).get("skill_name", key) or key or "skill").strip()
17528
+ skill_path = str((row if isinstance(row, dict) else {}).get("skill_path", "") or "").strip()
17529
+ preview = trim(str((row if isinstance(row, dict) else {}).get("preview", "") or ""), 500)
17530
+ path_note = f"\npath: {skill_path}" if skill_path else ""
17531
+ preview_note = f"\npreview: {preview}" if preview else ""
17532
+ return (
17533
+ f"<loaded-skill-stub name=\"{skill_name}\" key=\"{key}\">"
17534
+ f"{path_note}{preview_note}\n"
17535
+ "Full skill instructions are rehydrated in the system prompt as ACTIVE SKILL WORKFLOWS. "
17536
+ "Follow that workflow for matching steps."
17537
+ "\n</loaded-skill-stub>"
17538
+ )
17539
+
17540
+ def _compact_shared_context(self, tier: int):
17541
+ if tier <= 0:
17542
+ return
17543
+ for msg in self.agent_messages:
17544
+ if not isinstance(msg, dict):
17545
+ continue
17546
+ if str(msg.get("agent_role", "") or "") != "shared":
17547
+ continue
17548
+ content = str(msg.get("content", "") or "")
17549
+ if "<loaded-skill name=" in content:
17550
+ msg["content"] = self._loaded_skill_stub_content(content)
17551
+ elif tier >= 3 and len(content) > 1000:
17552
+ msg["content"] = trim(content, 1000)
17105
17553
  if tier >= 2:
17106
- self._offload_agent_message_content(self.agent_messages)
17107
- self._offload_agent_message_content(self.manager_context)
17108
- # 6. Tier 3: aggressive content trimming
17109
- if tier >= 3:
17554
+ self._compact_plan_context(tier)
17555
+
17556
+ def _compact_role_context(self, role: str, tier: int):
17557
+ """Compact one model-call context without globally erasing peer roles."""
17558
+ t = min(max(int(tier or 0), 0), 3)
17559
+ if t <= 0:
17560
+ return
17561
+ role_raw = str(role or "").strip().lower()
17562
+ limits = self._tier_agent_context_limits(t)
17563
+ keep_recent = max(1, 3 - t)
17564
+ if role_raw == "manager":
17565
+ self._microcompact_agent_messages(self.manager_context, keep_recent=keep_recent)
17566
+ if len(self.manager_context) > limits["manager_context"]:
17567
+ self.manager_context = self.manager_context[-limits["manager_context"]:]
17568
+ if t >= 2:
17569
+ self._offload_agent_message_content(self.manager_context)
17570
+ if t >= 3:
17571
+ for msg in self.manager_context:
17572
+ content = str(msg.get("content", "") or "")
17573
+ if len(content) > 700:
17574
+ msg["content"] = trim(content, 700)
17575
+ return
17576
+
17577
+ role_key = self._sanitize_agent_role(role_raw)
17578
+ if not role_key:
17579
+ return
17580
+ scoped_rows = [
17581
+ m for m in self.agent_messages
17582
+ if isinstance(m, dict)
17583
+ and (
17584
+ str(m.get("agent_role", "") or "") == role_key
17585
+ or (str(m.get("role", "") or "") == "user" and not str(m.get("agent_role", "") or "").strip())
17586
+ or str(m.get("agent_role", "") or "") == "shared"
17587
+ )
17588
+ ]
17589
+ self._microcompact_agent_messages(scoped_rows, keep_recent=keep_recent)
17590
+ if t >= 2:
17591
+ self._compact_shared_context(t)
17592
+ self._offload_agent_message_content(
17593
+ [
17594
+ m for m in self.agent_messages
17595
+ if isinstance(m, dict)
17596
+ and (
17597
+ str(m.get("agent_role", "") or "") == role_key
17598
+ or (str(m.get("role", "") or "") == "user" and not str(m.get("agent_role", "") or "").strip())
17599
+ )
17600
+ ]
17601
+ )
17602
+ if t >= 3:
17110
17603
  for msg in self.agent_messages:
17111
- c = msg.get("content", "")
17112
- if isinstance(c, str) and len(c) > 600:
17113
- msg["content"] = trim(c, 600)
17114
- for msg in self.manager_context:
17115
- c = msg.get("content", "")
17116
- if isinstance(c, str) and len(c) > 600:
17117
- msg["content"] = trim(c, 600)
17118
- # 7. Plan mode context compression — tiered by ctx_left
17119
- self._compact_plan_context(tier)
17604
+ if not isinstance(msg, dict):
17605
+ continue
17606
+ msg_role = str(msg.get("agent_role", "") or "")
17607
+ in_scope = msg_role == role_key or (str(msg.get("role", "") or "") == "user" and not msg_role)
17608
+ if not in_scope:
17609
+ continue
17610
+ content = str(msg.get("content", "") or "")
17611
+ if len(content) > 700:
17612
+ msg["content"] = trim(content, 700)
17613
+ role_limit = limits["context_per_role"]
17614
+ scoped_indices = [
17615
+ i for i, m in enumerate(self.agent_messages)
17616
+ if isinstance(m, dict)
17617
+ and (
17618
+ str(m.get("agent_role", "") or "") == role_key
17619
+ or (str(m.get("role", "") or "") == "user" and not str(m.get("agent_role", "") or "").strip())
17620
+ )
17621
+ ]
17622
+ if len(scoped_indices) > role_limit:
17623
+ keep = set(scoped_indices[-role_limit:])
17624
+ self.agent_messages = [
17625
+ m for i, m in enumerate(self.agent_messages)
17626
+ if i not in scoped_indices or i in keep
17627
+ ]
17628
+ self.contexts[role_key] = self._agent_context(role_key)[-400:]
17629
+
17630
+ def _compact_agent_contexts(self, tier: int):
17631
+ """Compress shared state and each role without flattening all agents together."""
17632
+ t = min(max(int(tier or 0), 0), 3)
17633
+ if t <= 0:
17634
+ return
17635
+ limits = self._tier_agent_context_limits(t)
17636
+ self._compact_shared_context(t)
17637
+ self._compact_role_context("manager", t)
17638
+ for role in AGENT_ROLES:
17639
+ self._compact_role_context(role, t)
17640
+ am_limit = int(limits["agent_messages"])
17641
+ if len(self.agent_messages) > int(am_limit * 1.5):
17642
+ shared_rows = [
17643
+ m for m in self.agent_messages
17644
+ if isinstance(m, dict) and str(m.get("agent_role", "") or "") == "shared"
17645
+ ][-12:]
17646
+ tail = self.agent_messages[-am_limit:]
17647
+ existing_ids = {id(m) for m in tail}
17648
+ preserved = [m for m in shared_rows if id(m) not in existing_ids]
17649
+ self.agent_messages = (preserved + tail)[-int(am_limit + len(preserved)):]
17650
+ self._compact_plan_context(t)
17120
17651
 
17121
17652
  def _compact_plan_context(self, tier: int):
17122
17653
  """Compress plan mode context based on tier.
@@ -17182,21 +17713,41 @@ class SessionState:
17182
17713
  if plan_phase not in ("executing", "awaiting_choice"):
17183
17714
  self.runtime_plan_proposal = {}
17184
17715
 
17185
- def _apply_auto_compact_if_needed(self, reason: str = "auto") -> bool:
17186
- metrics = self._context_budget_metrics()
17716
+ def _apply_auto_compact_if_needed(
17717
+ self,
17718
+ reason: str = "auto",
17719
+ *,
17720
+ metrics: dict | None = None,
17721
+ role: str = "",
17722
+ media_inputs: list[dict] | None = None,
17723
+ ) -> bool:
17724
+ metrics = metrics or self._active_next_call_context_metrics(role=role, media_inputs=media_inputs)
17187
17725
  tier = self._context_compression_tier(metrics)
17726
+ role_raw = str(role or self.active_agent_role or "").strip().lower()
17727
+ role_key = "manager" if role_raw == "manager" else self._sanitize_agent_role(role_raw)
17728
+ if role_key in AGENT_ROLES and not self._role_specific_context_is_live_call(role_key):
17729
+ role_key = ""
17188
17730
  # Tier 1+: apply progressively aggressive microcompact
17189
- if tier >= 1:
17731
+ if role_key and tier >= 1:
17732
+ self._compact_role_context(role_key, tier)
17733
+ if tier >= 2:
17734
+ self._compact_shared_context(tier)
17735
+ elif role_key:
17736
+ if role_key == "manager":
17737
+ self._microcompact_agent_messages(self.manager_context, keep_recent=3)
17738
+ else:
17739
+ self._microcompact_agent_messages(self._agent_context(role_key), keep_recent=3)
17740
+ elif tier >= 1:
17190
17741
  keep = max(1, 3 - tier)
17191
17742
  self._microcompact(keep_recent=keep)
17192
17743
  else:
17193
17744
  self._microcompact()
17194
17745
  # Tier 2+: compact agent contexts proactively
17195
- if tier >= 2:
17746
+ if tier >= 2 and not role_key:
17196
17747
  self._compact_agent_contexts(tier)
17197
17748
  # Re-check after tier-based compression. The effective limit keeps a
17198
17749
  # small hard reserve so auto-compact still has room to run.
17199
- metrics = self._context_budget_metrics()
17750
+ metrics = self._active_next_call_context_metrics(role=role, media_inputs=media_inputs)
17200
17751
  used = int(metrics.get("used", 0) or 0)
17201
17752
  effective_limit = max(1, int(metrics.get("effective_limit", metrics.get("limit", 0)) or 0))
17202
17753
  if used < effective_limit:
@@ -17204,7 +17755,13 @@ class SessionState:
17204
17755
  now_tick = now_ts()
17205
17756
  if (now_tick - float(self.last_compact_ts or 0.0)) < 0.8:
17206
17757
  return False
17207
- self._auto_compact(reason)
17758
+ if (
17759
+ not bool(getattr(self, "context_last_compact_effective", True))
17760
+ and (now_tick - float(getattr(self, "context_last_compact_skip_ts", 0.0) or 0.0))
17761
+ < float(CONTEXT_COMPACT_INEFFECTIVE_COOLDOWN_SECONDS)
17762
+ ):
17763
+ return False
17764
+ self._auto_compact(reason, metrics=metrics, role=role, media_inputs=media_inputs)
17208
17765
  return True
17209
17766
 
17210
17767
  def _estimate_output_tokens(self, text: str, thinking_text: str = "", tool_calls: list | None = None) -> int:
@@ -17219,10 +17776,9 @@ class SessionState:
17219
17776
  return max(1, t_main + t_think + t_tools)
17220
17777
 
17221
17778
  def _derive_context_limit_from_output(self, output_tokens: int) -> int:
17222
- estimated = int(
17223
- max(MIN_CONTEXT_TOKEN_LIMIT, min(self.max_context_token_limit, output_tokens * 12))
17224
- )
17225
- return max(MIN_CONTEXT_TOKEN_LIMIT, min(self.max_context_token_limit, estimated))
17779
+ # Kept for compatibility with older call sites. Output length is not a
17780
+ # reliable proxy for input context capacity, so never shrink from it.
17781
+ return max(MIN_CONTEXT_TOKEN_LIMIT, int(self.max_context_token_limit))
17226
17782
 
17227
17783
  def _trim_truncated_tail_line(self, text: str) -> str:
17228
17784
  src = str(text or "")
@@ -17430,9 +17986,9 @@ class SessionState:
17430
17986
  if now_tick - self.last_truncation_ts >= 0.2:
17431
17987
  self.truncation_count += 1
17432
17988
  self.last_truncation_ts = now_tick
17433
- old_bound = int(self.context_token_upper_bound)
17434
- new_bound = self._derive_context_limit_from_output(tokens_now)
17435
- self.context_token_upper_bound = min(old_bound, new_bound)
17989
+ # Output truncation is a generation-budget signal, not proof that the
17990
+ # prompt context window is smaller. Keep the configured/adaptive input
17991
+ # context bound intact; only provider context-window errors may shrink it.
17436
17992
 
17437
17993
  self._publish_live_truncation(
17438
17994
  text=working,
@@ -17861,22 +18417,13 @@ class SessionState:
17861
18417
  return
17862
18418
  self.truncation_count += 1
17863
18419
  self.last_truncation_ts = now_tick
17864
- old_bound = self.context_token_upper_bound
17865
- if not self.context_limit_locked:
17866
- new_bound = self._derive_context_limit_from_output(output_tokens)
17867
- self.context_token_upper_bound = min(old_bound, new_bound)
17868
- if self.context_token_upper_bound < old_bound:
17869
- self._emit(
17870
- "status",
17871
- {
17872
- "summary": (
17873
- f"context upper bound adjusted {old_bound}->{self.context_token_upper_bound} "
17874
- f"(output_tokens≈{output_tokens})"
17875
- )
17876
- },
18420
+ if allow_compact:
18421
+ metrics = self._active_next_call_context_metrics()
18422
+ if int(metrics.get("used", 0) or 0) >= int(metrics.get("effective_limit", 0) or 0):
18423
+ self._apply_auto_compact_if_needed(
18424
+ f"truncation-rescue:{source or 'auto'}",
18425
+ metrics=metrics,
17877
18426
  )
17878
- if allow_compact and self._estimate_tokens() > self.context_token_upper_bound:
17879
- self._auto_compact(f"truncation-rescue:{source or 'auto'}")
17880
18427
  self._ensure_truncation_todos()
17881
18428
  task_ids = self._create_truncation_subtasks(reason)
17882
18429
  self._inject_truncation_rescue_hint(reason, output_tokens, task_ids)
@@ -18220,7 +18767,7 @@ class SessionState:
18220
18767
  if len(model_pages) > 1 or should_temp:
18221
18768
  model_truncated = True
18222
18769
  preview = (
18223
- self._run_read(temp_output_path, LONG_OUTPUT_READ_PAGE_LINES, 0)
18770
+ self._run_read(temp_output_path, mode="overview", max_chars=LONG_OUTPUT_MODEL_PAGE_CHARS)
18224
18771
  if temp_output_path
18225
18772
  else model_pages[0]
18226
18773
  )
@@ -18232,7 +18779,11 @@ class SessionState:
18232
18779
  ]
18233
18780
  if temp_output_path:
18234
18781
  parts.append(
18235
- f"use read_file path=\"{temp_output_path}\" offset=0 limit={LONG_OUTPUT_READ_PAGE_LINES}"
18782
+ f"full_output_path={temp_output_path}"
18783
+ )
18784
+ parts.append(
18785
+ "Use read_file on full_output_path with mode=\"search\" for a keyword/error, "
18786
+ "mode=\"window\" with line/context for nearby lines, or mode=\"full\" with max_chars when broad exact output is needed."
18236
18787
  )
18237
18788
  if buffer_ref:
18238
18789
  parts.append(f"buffer_ref={buffer_ref}")
@@ -18549,8 +19100,29 @@ class SessionState:
18549
19100
  kept.sort(key=lambda x: x[0])
18550
19101
  return [msg for _, msg in kept]
18551
19102
 
18552
- def _auto_compact(self, reason: str):
18553
- context_before = self._context_budget_metrics()
19103
+ def _strip_archival_runtime_hints(self, rows: list[dict]) -> list[dict]:
19104
+ cleaned: list[dict] = []
19105
+ for row in rows or []:
19106
+ item = dict(row) if isinstance(row, dict) else {"role": "", "content": str(row or "")}
19107
+ role = str(item.get("role", "") or "")
19108
+ content = str(item.get("content", "") or "")
19109
+ low = content.strip().lower()
19110
+ if role == "user" and any(low.startswith(prefix) for prefix in RETRY_RUNTIME_HINT_PREFIXES):
19111
+ continue
19112
+ if "<compact-resume>" in low or "<state_handoff>" in low:
19113
+ item["content"] = "[previous compact-resume archived; use context_recall for details]"
19114
+ cleaned.append(item)
19115
+ return cleaned
19116
+
19117
+ def _auto_compact(
19118
+ self,
19119
+ reason: str,
19120
+ *,
19121
+ metrics: dict | None = None,
19122
+ role: str = "",
19123
+ media_inputs: list[dict] | None = None,
19124
+ ):
19125
+ context_before = metrics or self._active_next_call_context_metrics(role=role, media_inputs=media_inputs)
18554
19126
  tier = self._context_compression_tier(context_before)
18555
19127
  transcript_dir = self.root / "transcripts"
18556
19128
  transcript_dir.mkdir(parents=True, exist_ok=True)
@@ -18569,15 +19141,16 @@ class SessionState:
18569
19141
  if len(tail) >= len(self.messages):
18570
19142
  tail = self._select_compact_tail(max(2200, int(tail_budget * 0.55)), min_count=4, max_count=20)
18571
19143
  archived_rows = self.messages[:-len(tail)] if tail else list(self.messages)
19144
+ tail = self._strip_archival_runtime_hints(tail)
18572
19145
  seg = self._archive_context_segment(archived_rows, reason) if archived_rows else {}
18573
19146
  summary = self._summarize_compact_rows(archived_rows)
18574
19147
  seg_id = str(seg.get("id", "")) if isinstance(seg, dict) else ""
18575
19148
  seg_msg_count = int(seg.get("messages", 0) or 0) if isinstance(seg, dict) else 0
18576
19149
  seg_path = str(seg.get("path", "")) if isinstance(seg, dict) else ""
18577
19150
  continuation = (
18578
- f"If details are missing, call context_recall with segment_id='{seg_id}', max_messages=40, offset=0."
19151
+ f"If details are missing, call context_recall with segment_id='{seg_id}', mode='search' and a specific query, or mode='window' with around_index/context."
18579
19152
  if seg_id
18580
- else "If details are missing, call context_recall with recent_segments=2."
19153
+ else "If details are missing, call context_recall with mode='summary' first, then mode='search' or mode='window' for focused evidence."
18581
19154
  )
18582
19155
  # Create checkpoint before compaction
18583
19156
  self._maybe_create_checkpoint()
@@ -18622,14 +19195,38 @@ class SessionState:
18622
19195
  while len(tail) > 5 and self._estimate_messages_tokens(tail) > target_tokens:
18623
19196
  tail.pop(0)
18624
19197
  self.messages = tail
18625
- # Compact agent contexts in sync (critical fix for re-wall prevention)
18626
- self._compact_agent_contexts(max(tier, 1))
19198
+ # Compact only the pressured role when known; shared state remains tier-aware.
19199
+ role_raw = str(role or "").strip().lower()
19200
+ role_key = "manager" if role_raw == "manager" else self._sanitize_agent_role(role_raw)
19201
+ if role_key in AGENT_ROLES and not self._role_specific_context_is_live_call(role_key):
19202
+ role_key = ""
19203
+ if role_key:
19204
+ self._compact_role_context(role_key, max(tier, 1))
19205
+ if tier >= 2:
19206
+ self._compact_shared_context(tier)
19207
+ else:
19208
+ self._compact_agent_contexts(max(tier, 1))
18627
19209
  # Tier 2+: offload remaining large content in messages to file buffer
18628
19210
  if tier >= 2:
18629
19211
  for msg in self.messages:
18630
19212
  c = msg.get("content", "")
18631
19213
  if isinstance(c, str) and len(c) >= FILE_BUFFER_CONTENT_THRESHOLD * 2:
18632
19214
  msg["content"] = self._offload_to_file_buffer(c, label="compact-msg")
19215
+ context_after = self._active_next_call_context_metrics(role=role, media_inputs=media_inputs)
19216
+ before_used = int(context_before.get("used", 0) or 0)
19217
+ after_used = int(context_after.get("used", 0) or 0)
19218
+ reduction = max(0, before_used - after_used)
19219
+ effective = bool(reduction >= max(400, int(before_used * 0.05)) or after_used < int(context_after.get("effective_limit", 0) or 0))
19220
+ self.context_last_compact_before = dict(context_before)
19221
+ self.context_last_compact_after = dict(context_after)
19222
+ self.context_last_compact_effective = bool(effective)
19223
+ self.context_last_compact_used_reduction = int(reduction)
19224
+ if not effective:
19225
+ self.context_last_compact_skip_ts = now_ts()
19226
+ self.context_last_compact_skip_reason = (
19227
+ f"compact ineffective: used {before_used}->{after_used}, "
19228
+ f"limit={int(context_after.get('effective_limit', 0) or 0)}"
19229
+ )
18633
19230
  self.last_compact_reason = str(reason or "")
18634
19231
  self.last_compact_ts = now_ts()
18635
19232
  self._emit(
@@ -18644,6 +19241,13 @@ class SessionState:
18644
19241
  "context_used_before": int(context_before.get("used", 0)),
18645
19242
  "context_left_before": int(context_before.get("left", 0)),
18646
19243
  "context_left_percent_before": round(float(context_before.get("left_percent", 0.0)), 2),
19244
+ "context_used_after": int(context_after.get("used", 0)),
19245
+ "context_left_after": int(context_after.get("left", 0)),
19246
+ "context_left_percent_after": round(float(context_after.get("left_percent", 0.0)), 2),
19247
+ "context_used_reduction": int(reduction),
19248
+ "effective": bool(effective),
19249
+ "next_call_label": str(context_after.get("next_call_label", "") or ""),
19250
+ "role": role_key or "",
18647
19251
  },
18648
19252
  )
18649
19253
 
@@ -21449,47 +22053,315 @@ body{padding:18px}
21449
22053
  body = "\n".join(lines[:line_cap])
21450
22054
  return trim(body, max_chars)
21451
22055
 
21452
- def _large_text_file_overview(self, fp: Path, rel: str, lines: list[str]) -> str:
22056
+ def _read_file_int_arg(self, value: object, default: int, minimum: int, maximum: int) -> int:
22057
+ try:
22058
+ iv = int(value)
22059
+ except Exception:
22060
+ iv = int(default)
22061
+ return max(int(minimum), min(int(maximum), iv))
22062
+
22063
+ def _read_file_max_chars(self, value: object = None, default: int = READ_FILE_DEFAULT_MAX_CHARS) -> int:
22064
+ if value is None or str(value).strip() == "":
22065
+ raw = int(default)
22066
+ else:
22067
+ raw = self._read_file_int_arg(value, int(default), 1200, READ_FILE_HARD_MAX_CHARS)
22068
+ return max(1200, min(int(READ_FILE_HARD_MAX_CHARS), int(raw)))
22069
+
22070
+ def _clip_read_file_output(self, text: str, max_chars: int, hint: str = "") -> str:
22071
+ cap = self._read_file_max_chars(max_chars)
22072
+ raw = str(text or "")
22073
+ if len(raw) <= cap:
22074
+ return raw
22075
+ default_hint = 'Use mode="search", mode="symbol", mode="window", or raise max_chars for a wider read.'
22076
+ suffix = (
22077
+ f"\n[read_file clipped chars=1-{cap} of {len(raw)} by max_chars. "
22078
+ f"{hint or default_hint}]"
22079
+ )
22080
+ return raw[:cap].rstrip() + suffix
22081
+
22082
+ def _tool_int_arg(self, value: object, default: int, minimum: int, maximum: int) -> int:
22083
+ try:
22084
+ iv = int(value)
22085
+ except Exception:
22086
+ iv = int(default)
22087
+ return max(int(minimum), min(int(maximum), iv))
22088
+
22089
+ def _tool_max_chars(self, value: object = None, default: int = 24000) -> int:
22090
+ return self._tool_int_arg(value, default, 1200, READ_FILE_HARD_MAX_CHARS)
22091
+
22092
+ def _row_text_for_search(self, row: object) -> str:
22093
+ try:
22094
+ return json_dumps(row, indent=0)
22095
+ except Exception:
22096
+ return str(row or "")
22097
+
22098
+ def _row_field_value(self, row: object, field: str) -> str:
22099
+ if not isinstance(row, dict):
22100
+ return ""
22101
+ cur: object = row
22102
+ for part in str(field or "").split("."):
22103
+ if not isinstance(cur, dict):
22104
+ return ""
22105
+ cur = cur.get(part)
22106
+ if isinstance(cur, (dict, list)):
22107
+ return self._row_text_for_search(cur)
22108
+ return str(cur or "")
22109
+
22110
+ def _collection_filter_rows(
22111
+ self,
22112
+ rows: list[dict],
22113
+ *,
22114
+ query: str = "",
22115
+ filters: dict[str, object] | None = None,
22116
+ ) -> list[dict]:
22117
+ q = str(query or "").strip().lower()
22118
+ clean_filters = {
22119
+ str(k): str(v).strip().lower()
22120
+ for k, v in (filters or {}).items()
22121
+ if str(v or "").strip()
22122
+ }
22123
+ out: list[dict] = []
22124
+ for row in rows:
22125
+ if not isinstance(row, dict):
22126
+ continue
22127
+ if clean_filters:
22128
+ ok = True
22129
+ for key, wanted in clean_filters.items():
22130
+ actual = self._row_field_value(row, key).strip().lower()
22131
+ if wanted not in actual:
22132
+ ok = False
22133
+ break
22134
+ if not ok:
22135
+ continue
22136
+ if q and q not in self._row_text_for_search(row).lower():
22137
+ continue
22138
+ out.append(row)
22139
+ return out
22140
+
22141
+ def _render_collection_tool_payload(
22142
+ self,
22143
+ *,
22144
+ tool: str,
22145
+ rows: list[dict],
22146
+ mode: object = None,
22147
+ query: object = "",
22148
+ limit: object = None,
22149
+ around_index: object = None,
22150
+ context: object = None,
22151
+ max_chars: object = None,
22152
+ filters: dict[str, object] | None = None,
22153
+ summary_fields: list[str] | None = None,
22154
+ detail_fields: list[str] | None = None,
22155
+ default_limit: int = 12,
22156
+ ) -> str:
22157
+ mode_text = str(mode or "").strip().lower() or ("search" if str(query or "").strip() else "summary")
22158
+ if mode_text not in {"summary", "search", "recent", "window", "detail", "tail", "peek", "drain"}:
22159
+ mode_text = "summary"
22160
+ cap = self._tool_max_chars(max_chars)
22161
+ all_rows = [row for row in rows if isinstance(row, dict)]
22162
+ filtered = self._collection_filter_rows(all_rows, query=str(query or ""), filters=filters)
22163
+ n = self._tool_int_arg(limit, default_limit, 1, 200)
22164
+ if mode_text in {"recent", "tail", "peek", "drain"}:
22165
+ selected = filtered[-n:]
22166
+ elif mode_text == "window":
22167
+ if around_index is None or str(around_index).strip() == "":
22168
+ selected = filtered[-n:]
22169
+ else:
22170
+ idx = self._tool_int_arg(around_index, 0, 0, max(0, len(filtered) - 1))
22171
+ ctx = self._tool_int_arg(context, 4, 0, 80)
22172
+ selected = filtered[max(0, idx - ctx): min(len(filtered), idx + ctx + 1)]
22173
+ elif mode_text == "detail":
22174
+ selected = filtered[:n] if str(query or "").strip() else filtered[-n:]
22175
+ else:
22176
+ selected = filtered[:n] if str(query or "").strip() else filtered[-n:]
22177
+
22178
+ def compact(row: dict, idx: int) -> dict:
22179
+ if mode_text == "detail":
22180
+ if detail_fields:
22181
+ return {k: row.get(k) for k in detail_fields if k in row}
22182
+ return row
22183
+ fields = summary_fields or list(row.keys())[:8]
22184
+ item = {"index": idx}
22185
+ for key in fields:
22186
+ if key in row:
22187
+ val = row.get(key)
22188
+ item[key] = trim(val if not isinstance(val, (dict, list)) else json_dumps(val), 800)
22189
+ return item
22190
+
22191
+ index_by_identity = {id(row): idx for idx, row in enumerate(filtered)}
22192
+ rendered = [
22193
+ compact(row, int(index_by_identity.get(id(row), 0)))
22194
+ for row in selected
22195
+ ]
22196
+ payload = {
22197
+ "tool": tool,
22198
+ "mode": mode_text,
22199
+ "query": str(query or ""),
22200
+ "filters": {k: v for k, v in (filters or {}).items() if str(v or "").strip()},
22201
+ "total_rows": len(all_rows),
22202
+ "matched_rows": len(filtered),
22203
+ "returned": len(rendered),
22204
+ "items": rendered,
22205
+ "focused_reads": [
22206
+ f"{tool} mode='search' query='<term>'",
22207
+ f"{tool} mode='window' around_index=<index> context=4",
22208
+ f"{tool} mode='detail' query='<specific id/name/status>'",
22209
+ ],
22210
+ }
22211
+ return trim(json_dumps(payload, indent=2), cap)
22212
+
22213
+ def _read_file_code_data(self, fp: Path, lines: list[str]) -> dict:
22214
+ text = "\n".join(lines)
22215
+ language = ""
22216
+ imports: list[str] = []
22217
+ symbols: list[dict] = []
22218
+ try:
22219
+ parser = CodeContentParser()
22220
+ language = parser.detect_language(fp, text=text[:120_000]) or ""
22221
+ imports = parser._extract_imports(text[:240_000], language) if language else []
22222
+ if language == "python":
22223
+ _, symbols = parser._python_chunks(text)
22224
+ else:
22225
+ _, symbols = parser._generic_code_chunks(text, language)
22226
+ except Exception:
22227
+ symbols = []
22228
+ if not symbols:
22229
+ symbols = self._read_file_fallback_symbols(fp, lines, language)
22230
+ total = len(lines)
22231
+ clean: list[dict] = []
22232
+ seen: set[tuple[str, int]] = set()
22233
+ for row in symbols:
22234
+ if not isinstance(row, dict):
22235
+ continue
22236
+ name = str(row.get("name", "") or "").strip()
22237
+ kind = str(row.get("kind", "") or row.get("symbol_kind", "") or "symbol").strip() or "symbol"
22238
+ start = self._read_file_int_arg(row.get("line_start", 1), 1, 1, max(1, total))
22239
+ end = self._read_file_int_arg(row.get("line_end", start), start, start, max(start, total))
22240
+ sig = str(row.get("signature", "") or "").strip()
22241
+ if not sig and 1 <= start <= total:
22242
+ sig = lines[start - 1].strip()
22243
+ key = (name.lower(), start)
22244
+ if key in seen:
22245
+ continue
22246
+ seen.add(key)
22247
+ clean.append(
22248
+ {
22249
+ "name": name or sig[:80],
22250
+ "kind": kind,
22251
+ "line_start": start,
22252
+ "line_end": end,
22253
+ "signature": trim(sig, 180),
22254
+ }
22255
+ )
22256
+ clean.sort(key=lambda r: (int(r.get("line_start", 0) or 0), str(r.get("name", ""))))
22257
+ return {"language": language or "text", "imports": imports[:64], "symbols": clean[:240]}
22258
+
22259
+ def _read_file_fallback_symbols(self, fp: Path, lines: list[str], language: str = "") -> list[dict]:
22260
+ symbols: list[dict] = []
22261
+ ext = fp.suffix.lower()
22262
+ patterns: list[tuple[re.Pattern[str], str]] = []
22263
+ if ext in {".py", ".pyi"} or language == "python":
22264
+ patterns = [
22265
+ (re.compile(r"^\s*class\s+([A-Za-z_][A-Za-z0-9_]*)\b"), "class"),
22266
+ (re.compile(r"^\s*async\s+def\s+([A-Za-z_][A-Za-z0-9_]*)\b"), "async_function"),
22267
+ (re.compile(r"^\s*def\s+([A-Za-z_][A-Za-z0-9_]*)\b"), "function"),
22268
+ ]
22269
+ else:
22270
+ try:
22271
+ patterns = CodeContentParser()._decl_matchers(language or "")
22272
+ except Exception:
22273
+ patterns = []
22274
+ if not patterns:
22275
+ patterns = [
22276
+ (re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\b"), "function"),
22277
+ (re.compile(r"^\s*(?:export\s+)?class\s+([A-Za-z_$][\w$]*)\b"), "class"),
22278
+ (re.compile(r"^\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\("), "function"),
22279
+ ]
22280
+ for idx, line in enumerate(lines, 1):
22281
+ for pattern, kind in patterns:
22282
+ m = pattern.search(line)
22283
+ if not m:
22284
+ continue
22285
+ name = str(m.group(1) if m.groups() else "").strip()
22286
+ if not name:
22287
+ continue
22288
+ symbols.append(
22289
+ {
22290
+ "name": name,
22291
+ "kind": kind,
22292
+ "line_start": idx,
22293
+ "line_end": idx,
22294
+ "signature": trim(line.strip(), 180),
22295
+ }
22296
+ )
22297
+ break
22298
+ if len(symbols) >= 240:
22299
+ break
22300
+ for pos, row in enumerate(symbols):
22301
+ start = int(row.get("line_start", 1) or 1)
22302
+ next_start = int(symbols[pos + 1].get("line_start", 0) or 0) if pos + 1 < len(symbols) else 0
22303
+ row["line_end"] = max(start, (next_start - 1) if next_start > start else min(len(lines), start + 120))
22304
+ return symbols
22305
+
22306
+ def _render_text_overview(
22307
+ self,
22308
+ fp: Path,
22309
+ rel: str,
22310
+ lines: list[str],
22311
+ *,
22312
+ max_chars: int | None = None,
22313
+ ) -> str:
21453
22314
  total_lines = len(lines)
21454
22315
  try:
21455
22316
  size = int(fp.stat().st_size)
21456
22317
  except Exception:
21457
22318
  size = 0
21458
- ext = fp.suffix.lower()
21459
- head = "\n".join(lines[:80])
21460
- symbols: list[str] = []
21461
- if ext in {".py", ".pyi"}:
21462
- for idx, line in enumerate(lines, 1):
21463
- stripped = line.strip()
21464
- if stripped.startswith(("class ", "def ", "async def ")):
21465
- symbols.append(f"{idx}: {trim(stripped, 180)}")
21466
- if len(symbols) >= 80:
21467
- break
21468
- elif ext in {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}:
21469
- pattern = re.compile(r"^\s*(?:export\s+)?(?:async\s+)?(?:function|class)\s+([A-Za-z_$][\w$]*)|^\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\(")
21470
- for idx, line in enumerate(lines, 1):
21471
- if pattern.search(line):
21472
- symbols.append(f"{idx}: {trim(line.strip(), 180)}")
21473
- if len(symbols) >= 80:
21474
- break
21475
- next_limit = LONG_OUTPUT_READ_PAGE_LINES
22319
+ cap = self._read_file_max_chars(max_chars, default=READ_FILE_DEFAULT_MAX_CHARS)
22320
+ code_data = self._read_file_code_data(fp, lines)
22321
+ language = str(code_data.get("language", "") or "text")
22322
+ symbols = code_data.get("symbols", []) if isinstance(code_data.get("symbols"), list) else []
22323
+ imports = code_data.get("imports", []) if isinstance(code_data.get("imports"), list) else []
22324
+ is_code = bool(symbols) or (language and language != "text")
21476
22325
  out = [
22326
+ f"[read_file overview path={rel} bytes={size} lines={total_lines} language={language}]",
21477
22327
  (
21478
- f"[large_file_overview path={rel} bytes={size} lines={total_lines} "
21479
- f"auto_paged=true]"
22328
+ "Choose a focused read that matches the question. "
22329
+ "Prefer symbol/search/window for investigation; use full only when exact broad context is needed."
21480
22330
  ),
21481
- "This file is too large to inject fully into model context. Use paged read_file calls or RAG/code-library search for focused retrieval.",
21482
- f"First page: read_file path=\"{rel}\" offset=0 limit={next_limit}",
21483
22331
  ]
22332
+ if imports:
22333
+ out.append("\nImports:")
22334
+ out.append(", ".join(str(x) for x in imports[:24]))
21484
22335
  if symbols:
21485
22336
  out.append("\nSymbols:")
21486
- out.extend(symbols)
22337
+ for row in symbols[:120]:
22338
+ start = int(row.get("line_start", 0) or 0)
22339
+ end = int(row.get("line_end", start) or start)
22340
+ name = str(row.get("name", "") or "").strip()
22341
+ kind = str(row.get("kind", "symbol") or "symbol")
22342
+ sig = str(row.get("signature", "") or "").strip()
22343
+ suffix = f" — {sig}" if sig and sig != name else ""
22344
+ out.append(f"L{start}-{end} {kind} {name}{suffix}")
22345
+ if len(symbols) > 120:
22346
+ out.append(f"... {len(symbols) - 120} more symbols omitted; use mode=\"search\" or mode=\"symbol\".")
22347
+ head = "\n".join(lines[:READ_FILE_OVERVIEW_HEAD_LINES]).strip("\n")
21487
22348
  if head:
21488
22349
  out.append("\nHead preview:")
21489
22350
  out.append(head)
21490
- if total_lines > 80:
21491
- out.append(f"\n[next_page read_file path=\"{rel}\" offset=80 limit={next_limit}]")
21492
- return "\n".join(out)
22351
+ out.append("\nFocused reads:")
22352
+ if symbols:
22353
+ first_symbol = str(symbols[0].get("name", "") or "").strip()
22354
+ if first_symbol:
22355
+ out.append(f"- read_file path=\"{rel}\" mode=\"symbol\" target=\"{first_symbol}\"")
22356
+ out.append(f"- read_file path=\"{rel}\" mode=\"search\" query=\"<term>\" context=6")
22357
+ out.append(f"- read_file path=\"{rel}\" mode=\"window\" line=<line> context=80")
22358
+ out.append(f"- read_file path=\"{rel}\" mode=\"full\" max_chars={min(cap, READ_FILE_DEFAULT_MAX_CHARS)}")
22359
+ if not is_code:
22360
+ out.append("- For long logs or command output, start with mode=\"search\" for the error, warning, filename, or keyword.")
22361
+ return self._clip_read_file_output("\n".join(out), cap)
22362
+
22363
+ def _large_text_file_overview(self, fp: Path, rel: str, lines: list[str]) -> str:
22364
+ return self._render_text_overview(fp, rel, lines)
21493
22365
 
21494
22366
  def add_upload(self, filename: str, raw: bytes, mime: str = "") -> dict:
21495
22367
  safe_name = self._safe_upload_name(filename)
@@ -21978,6 +22850,29 @@ body{padding:18px}
21978
22850
  ]
21979
22851
  return any(x in t for x in continue_markers)
21980
22852
 
22853
+ def _looks_like_action_promise_without_tool(self, text: str) -> bool:
22854
+ raw = strip_thinking_content(str(text or "")).strip()
22855
+ if not raw:
22856
+ return False
22857
+ low = raw.lower()
22858
+ if self._looks_like_conclusive_reply(raw) or self._looks_like_user_decision_needed(raw):
22859
+ return False
22860
+ promise_markers = (
22861
+ "现在创建", "现在开始", "开始构建", "开始编写", "开始生成", "我将创建", "我会创建",
22862
+ "我将编写", "我会编写", "接下来创建", "接下来构建", "让我创建", "让我开始",
22863
+ "现在建立", "開始建立", "開始編寫", "我將建立", "我會建立", "接下來建立",
22864
+ "now create", "now build", "now write", "i will create", "i will build", "i will write",
22865
+ "i'll create", "i'll build", "i'll write", "let me create", "let me build", "let me write",
22866
+ "next i will create", "next i will build",
22867
+ )
22868
+ if not any(marker in low for marker in promise_markers):
22869
+ return False
22870
+ artifact_markers = (
22871
+ "html", "报告", "報告", "交互", "interactive", "file", "文件", "script", "代码", "程式",
22872
+ ".html", ".js", ".css", "write_file", "edit_file", "bash", "生成", "创建", "建立", "构建", "編寫",
22873
+ )
22874
+ return any(marker in low for marker in artifact_markers)
22875
+
21981
22876
  def _looks_like_user_decision_needed(self, text: str) -> bool:
21982
22877
  t = (text or "").strip().lower()
21983
22878
  if not t:
@@ -22288,6 +23183,27 @@ body{padding:18px}
22288
23183
  ]
22289
23184
  ):
22290
23185
  return "TASK_COMPLETED"
23186
+ if any(
23187
+ x in low
23188
+ for x in [
23189
+ "action_required",
23190
+ "action required",
23191
+ "tool_required",
23192
+ "tool required",
23193
+ "execute_tool",
23194
+ "execute tool",
23195
+ "needs tool",
23196
+ "needs_action",
23197
+ "needs action",
23198
+ "需要工具",
23199
+ "需要执行",
23200
+ "需要執行",
23201
+ "需要動作",
23202
+ "工具が必要",
23203
+ "実行が必要",
23204
+ ]
23205
+ ):
23206
+ return "ACTION_REQUIRED"
22291
23207
  if any(
22292
23208
  x in low
22293
23209
  for x in [
@@ -22335,12 +23251,18 @@ body{padding:18px}
22335
23251
  "VALID_PLAN": "VALID_PLANNING",
22336
23252
  "PLANNING": "VALID_PLANNING",
22337
23253
  "PLAN": "VALID_PLANNING",
23254
+ "ACTION": "ACTION_REQUIRED",
23255
+ "REQUIRES_ACTION": "ACTION_REQUIRED",
23256
+ "NEEDS_ACTION": "ACTION_REQUIRED",
23257
+ "TOOL_REQUIRED": "ACTION_REQUIRED",
23258
+ "NEEDS_TOOL": "ACTION_REQUIRED",
23259
+ "EXECUTE_TOOL": "ACTION_REQUIRED",
22338
23260
  "EMPTY": "EMPTY_RAMBLING",
22339
23261
  "RAMBLING": "EMPTY_RAMBLING",
22340
23262
  "IDLE": "EMPTY_RAMBLING",
22341
23263
  }
22342
23264
  status = aliases.get(key, key)
22343
- if status in {"TASK_COMPLETED", "VALID_PLANNING", "EMPTY_RAMBLING"}:
23265
+ if status in {"TASK_COMPLETED", "ACTION_REQUIRED", "VALID_PLANNING", "EMPTY_RAMBLING"}:
22344
23266
  return status
22345
23267
  inferred = self._infer_arbiter_status_from_text(fallback_text)
22346
23268
  return inferred or ""
@@ -22399,25 +23321,32 @@ body{padding:18px}
22399
23321
 
22400
23322
  def _call_arbiter_llm(self, assistant_text: str, thinking_text: str = "") -> dict:
22401
23323
  clean = strip_thinking_content(str(assistant_text or "")).strip()
23324
+ thinking_clean = str(thinking_text or "").strip()
22402
23325
  if not self.arbiter_enabled:
22403
23326
  return {"status": "DISABLED", "reasoning": "arbiter disabled", "raw": ""}
22404
- if len(clean) < int(ARBITER_TRIGGER_MIN_CONTENT_CHARS):
23327
+ probe_len = max(len(clean), len(thinking_clean))
23328
+ if (
23329
+ probe_len < int(ARBITER_TRIGGER_MIN_CONTENT_CHARS)
23330
+ and not self._looks_like_action_promise_without_tool(f"{clean}\n{thinking_clean}")
23331
+ ):
22405
23332
  return {"status": "SKIP_SHORT", "reasoning": "content too short", "raw": ""}
22406
23333
  snapshot = self._arbiter_context_snapshot(clean, thinking_text)
22407
23334
  arbiter_system = (
22408
23335
  "You are a task-state arbiter. "
22409
23336
  "Classify worker output into exactly one status: "
22410
- "TASK_COMPLETED, VALID_PLANNING, or EMPTY_RAMBLING. "
23337
+ "TASK_COMPLETED, ACTION_REQUIRED, VALID_PLANNING, or EMPTY_RAMBLING. "
22411
23338
  "Return strict JSON only."
22412
23339
  )
22413
23340
  arbiter_user = (
22414
23341
  "Read the snapshot and classify the worker state.\n"
22415
23342
  "Status definitions:\n"
22416
23343
  "- TASK_COMPLETED: worker already completed user's target and gave final deliverable/summary.\n"
22417
- "- VALID_PLANNING: worker output is meaningful planning/analysis that should continue into execution.\n"
23344
+ "- ACTION_REQUIRED: worker has decided or promised to create/write/modify/run/verify something, or thinking contains a concrete next tool action, but no tool call was emitted.\n"
23345
+ "- VALID_PLANNING: worker output is useful high-level analysis or design that still needs more reasoning before a concrete tool action.\n"
22418
23346
  "- EMPTY_RAMBLING: worker is stalling, repeating, or hallucinating with no actionable progress.\n"
23347
+ "Prefer ACTION_REQUIRED over VALID_PLANNING when the next step is already a concrete artifact/action.\n"
22419
23348
  "Output JSON only:\n"
22420
- "{\"status\":\"TASK_COMPLETED|VALID_PLANNING|EMPTY_RAMBLING\",\"reasoning\":\"<=40 words\"}\n\n"
23349
+ "{\"status\":\"TASK_COMPLETED|ACTION_REQUIRED|VALID_PLANNING|EMPTY_RAMBLING\",\"reasoning\":\"<=40 words\"}\n\n"
22421
23350
  f"Snapshot:\n{json_dumps(snapshot, indent=2)}"
22422
23351
  )
22423
23352
  box: dict[str, object] = {}
@@ -22447,18 +23376,28 @@ body{padding:18px}
22447
23376
  return {"status": "ARBITER_ERROR", "reasoning": trim(str(err), 220), "raw": ""}
22448
23377
  rsp = box.get("rsp") if isinstance(box.get("rsp"), dict) else {}
22449
23378
  raw_content = trim(str((rsp or {}).get("content", "") or ""), 2000)
23379
+ raw_thinking = trim(str((rsp or {}).get("thinking", "") or ""), 1200)
23380
+ status_source = raw_content or raw_thinking
22450
23381
  payload = extract_json_object_from_text(raw_content, {})
22451
- status = self._normalize_arbiter_status(str(payload.get("status", "") or ""), raw_content)
23382
+ if not payload and raw_thinking:
23383
+ payload = extract_json_object_from_text(raw_thinking, {})
23384
+ status = self._normalize_arbiter_status(str(payload.get("status", "") or ""), status_source)
22452
23385
  if not status:
22453
- status = self._infer_arbiter_status_from_text(raw_content)
23386
+ status = self._infer_arbiter_status_from_text(status_source)
23387
+ if (
23388
+ (not status or status == "UNKNOWN")
23389
+ and self._looks_like_action_promise_without_tool(f"{clean}\n{thinking_clean}\n{status_source}")
23390
+ ):
23391
+ status = "ACTION_REQUIRED"
22454
23392
  reasoning = trim(
22455
23393
  str(payload.get("reasoning", payload.get("reason", "")) or ""),
22456
23394
  280,
22457
- ) or trim(raw_content, 280)
23395
+ ) or trim(status_source, 280)
22458
23396
  return {
22459
23397
  "status": status or "UNKNOWN",
22460
23398
  "reasoning": reasoning,
22461
23399
  "raw": raw_content,
23400
+ "raw_thinking": raw_thinking,
22462
23401
  "model": str(self.arbiter_model or self.ollama.model or "").strip(),
22463
23402
  }
22464
23403
 
@@ -22479,6 +23418,26 @@ body{padding:18px}
22479
23418
  }
22480
23419
  )
22481
23420
 
23421
+ def _inject_arbiter_action_required_hint(self, decision: dict, assistant_text: str = ""):
23422
+ reasoning = trim(str(decision.get("reasoning", "") or "").strip(), 180)
23423
+ latest = trim(strip_thinking_content(str(assistant_text or "")).strip(), 420)
23424
+ self._prune_runtime_retry_hints()
23425
+ self.messages.append(
23426
+ {
23427
+ "role": "user",
23428
+ "content": (
23429
+ "<arbiter-continue>"
23430
+ "系统仲裁判定:当前回复已经到了需要执行工具的阶段,但没有发出工具调用。"
23431
+ "下一轮必须只调用一个具体工具来推进,例如 write_file/edit_file/bash/read_file/check_background;"
23432
+ "如果确实已经完成,则调用 finish_task 并提供文件路径和验证证据。不要再输出计划性说明。"
23433
+ f"{(' 判定依据: ' + reasoning + '。') if reasoning else ''}"
23434
+ f"{(' 上一条文本: ' + latest) if latest else ''}"
23435
+ "</arbiter-continue>"
23436
+ ),
23437
+ "ts": now_ts(),
23438
+ }
23439
+ )
23440
+
22482
23441
  def _mark_all_done_silently(self, reason: str = "") -> dict:
22483
23442
  summary = trim(str(reason or "").strip(), 160) or "arbiter: task completed"
22484
23443
  # Protect plan_step todos — they must only be completed by _advance_plan_step
@@ -23481,6 +24440,15 @@ body{padding:18px}
23481
24440
  system=system,
23482
24441
  media_inputs=media_inputs,
23483
24442
  )
24443
+ label_low = str(context_label or "").strip().lower()
24444
+ context_role_hint = ""
24445
+ if "manager" in label_low:
24446
+ context_role_hint = "manager"
24447
+ else:
24448
+ for _role in AGENT_ROLES:
24449
+ if _role in label_low:
24450
+ context_role_hint = _role
24451
+ break
23484
24452
 
23485
24453
  def _emit_http_retry(meta: dict):
23486
24454
  try:
@@ -23541,6 +24509,28 @@ body{padding:18px}
23541
24509
  last_exc = exc
23542
24510
  if self.cancel_requested or "interrupted by user" in str(exc).lower():
23543
24511
  raise
24512
+ if self._context_window_error_hint(exc):
24513
+ if self._shrink_context_upper_bound_from_actual_pressure(
24514
+ estimated_prompt_tokens=estimated_prompt_tokens,
24515
+ reason=f"{context_label}:{trim(str(exc), 80)}",
24516
+ ):
24517
+ self._apply_auto_compact_if_needed(
24518
+ f"provider-context-error:{context_label}",
24519
+ metrics=self._context_budget_metrics(token_estimate=estimated_prompt_tokens),
24520
+ role=context_role_hint,
24521
+ )
24522
+ if attempt <= retry_budget:
24523
+ self._emit(
24524
+ "status",
24525
+ {
24526
+ "summary": (
24527
+ f"{context_label} context-window retry after compact "
24528
+ f"({attempt}/{retry_budget})"
24529
+ )
24530
+ },
24531
+ )
24532
+ time.sleep(min(1.0, 0.25 * attempt))
24533
+ continue
23544
24534
  # Detect media format incompatibility and auto-fallback
23545
24535
  exc_str = str(exc).lower()
23546
24536
  _media_fmt_err = media_inputs and any(
@@ -24309,7 +25299,16 @@ body{padding:18px}
24309
25299
  scored.sort(key=lambda row: (-row[0], len(row[1]), row[1]))
24310
25300
  return [path for _, path in scored[: max(1, int(limit or 1))]]
24311
25301
 
24312
- def _render_directory_read(self, fp: Path, rel: str, limit: int | None = None, offset: int | None = None) -> str:
25302
+ def _render_directory_read(
25303
+ self,
25304
+ fp: Path,
25305
+ rel: str,
25306
+ limit: int | None = None,
25307
+ offset: int | None = None,
25308
+ *,
25309
+ query: object = "",
25310
+ max_chars: object = None,
25311
+ ) -> str:
24313
25312
  entries = sorted(
24314
25313
  list(fp.iterdir()),
24315
25314
  key=lambda p: (0 if p.is_dir() else 1, p.name.lower()),
@@ -24317,16 +25316,26 @@ body{padding:18px}
24317
25316
  total = len(entries)
24318
25317
  if total == 0:
24319
25318
  return f"[read_file directory path={rel} entries=0]\n(empty directory)"
24320
- offset_val = max(0, int(offset or 0))
24321
- requested_limit = max(1, int(limit or 60))
24322
- if offset_val >= total:
25319
+ query_text = str(query or "").strip().lower()
25320
+ if query_text:
25321
+ filtered = [p for p in entries if query_text in p.name.lower() or query_text in p.as_posix().lower()]
25322
+ else:
25323
+ filtered = entries
25324
+ filtered_total = len(filtered)
25325
+ offset_val = self._read_file_int_arg(offset, 0, 0, max(0, filtered_total))
25326
+ requested_limit = self._read_file_int_arg(limit, 200 if query_text else 120, 1, 500)
25327
+ if offset_val >= filtered_total:
24323
25328
  return (
24324
- f"[read_file directory path={rel} entries=0 of {total} offset={offset_val}]\n"
25329
+ f"[read_file directory path={rel} entries=0 of {filtered_total} total={total} offset={offset_val}]\n"
24325
25330
  "[end_of_directory]"
24326
25331
  )
24327
- page = entries[offset_val: offset_val + requested_limit]
25332
+ page = filtered[offset_val: offset_val + requested_limit]
24328
25333
  lines = [
24329
- f"[read_file directory path={rel} entries={offset_val + 1}-{offset_val + len(page)} of {total} offset={offset_val} limit={requested_limit}]"
25334
+ (
25335
+ f"[read_file directory path={rel} entries={offset_val + 1}-{offset_val + len(page)} "
25336
+ f"of {filtered_total} total={total} mode=directory"
25337
+ f"{f' query={query_text!r}' if query_text else ''}]"
25338
+ )
24330
25339
  ]
24331
25340
  for child in page:
24332
25341
  kind = "dir" if child.is_dir() else "file"
@@ -24335,13 +25344,15 @@ body{padding:18px}
24335
25344
  except Exception:
24336
25345
  size_text = ""
24337
25346
  lines.append(f"{kind} {child.name}{size_text}")
24338
- next_offset = offset_val + len(page)
24339
- if next_offset < total:
24340
- lines.append(f"[next_page read_file path=\"{rel}\" offset={next_offset} limit={requested_limit}]")
24341
- if offset_val > 0:
24342
- prev_offset = max(0, offset_val - requested_limit)
24343
- lines.append(f"[prev_page read_file path=\"{rel}\" offset={prev_offset} limit={requested_limit}]")
24344
- return "\n".join(lines)
25347
+ remaining = max(0, filtered_total - (offset_val + len(page)))
25348
+ if remaining > 0:
25349
+ lines.append(
25350
+ f"[directory has {remaining} more matching entries. Use query to narrow results, "
25351
+ "or read a specific subdirectory/file.]"
25352
+ )
25353
+ if not query_text and total > requested_limit:
25354
+ lines.append(f"[tip read_file path=\"{rel}\" mode=\"directory\" query=\"<name-fragment>\" narrows large directories]")
25355
+ return self._clip_read_file_output("\n".join(lines), self._read_file_max_chars(max_chars))
24345
25356
 
24346
25357
  def _read_text_with_fallback(self, fp: Path) -> str:
24347
25358
  tried: list[str] = []
@@ -24373,7 +25384,176 @@ body{padding:18px}
24373
25384
  )
24374
25385
  return "\n".join(lines)
24375
25386
 
24376
- def _run_read(self, path: str, limit: int | None = None, offset: int | None = None) -> str:
25387
+ def _render_full_text_read(self, rel: str, lines: list[str], *, max_chars: object = None) -> str:
25388
+ full_text = "\n".join(lines)
25389
+ cap = self._read_file_max_chars(max_chars)
25390
+ if len(full_text) <= cap:
25391
+ return full_text
25392
+ header = (
25393
+ f"[read_file full path={rel} chars=1-{cap} of {len(full_text)} max_chars={cap}]\n"
25394
+ )
25395
+ return header + self._clip_read_file_output(
25396
+ full_text,
25397
+ cap,
25398
+ "For exact focused context, use mode=\"search\", mode=\"symbol\", or mode=\"window\"; "
25399
+ "for a wider full read, set a larger max_chars value.",
25400
+ )
25401
+
25402
+ def _render_window_text_read(
25403
+ self,
25404
+ rel: str,
25405
+ lines: list[str],
25406
+ *,
25407
+ limit: int | None = None,
25408
+ offset: int | None = None,
25409
+ line: object = None,
25410
+ context: object = None,
25411
+ max_chars: object = None,
25412
+ ) -> str:
25413
+ total = len(lines)
25414
+ if total <= 0:
25415
+ return f"[read_file window path={rel} lines=0]\n[end_of_file]"
25416
+ ctx = self._read_file_int_arg(context, 60, 0, 2000)
25417
+ default_limit = ctx * 2 + 1 if line not in (None, "") else LONG_OUTPUT_READ_PAGE_LINES
25418
+ requested_limit = self._read_file_int_arg(limit, default_limit, 1, 4000)
25419
+ line_val = self._read_file_int_arg(line, 0, 0, max(1, total)) if line not in (None, "") else 0
25420
+ if line_val > 0:
25421
+ start = max(0, line_val - ctx - 1)
25422
+ else:
25423
+ start = self._read_file_int_arg(offset, 0, 0, max(0, total))
25424
+ end = min(total, start + requested_limit)
25425
+ if start >= total:
25426
+ return f"[read_file window path={rel} lines=0 of {total} start={start + 1}]\n[end_of_file]"
25427
+ body = "\n".join(lines[start:end])
25428
+ header = f"[read_file window path={rel} lines={start + 1}-{end} of {total}]\n"
25429
+ return header + self._clip_read_file_output(body, self._read_file_max_chars(max_chars))
25430
+
25431
+ def _render_search_text_read(
25432
+ self,
25433
+ rel: str,
25434
+ lines: list[str],
25435
+ *,
25436
+ query: object = "",
25437
+ regex: object = False,
25438
+ context: object = None,
25439
+ max_chars: object = None,
25440
+ ) -> str:
25441
+ needle = str(query or "").strip()
25442
+ if not needle:
25443
+ return (
25444
+ f"[read_file search path={rel}]\n"
25445
+ "Error: query is required for mode=\"search\". Use mode=\"overview\" to inspect available structure."
25446
+ )
25447
+ ctx = self._read_file_int_arg(context, 6, 0, 80)
25448
+ total = len(lines)
25449
+ matches: list[int] = []
25450
+ if bool(regex):
25451
+ try:
25452
+ pattern = re.compile(needle)
25453
+ except re.error as exc:
25454
+ return f"Error: invalid regex for read_file search: {exc}"
25455
+ for idx, row in enumerate(lines):
25456
+ if pattern.search(row):
25457
+ matches.append(idx)
25458
+ if len(matches) >= READ_FILE_SEARCH_MAX_MATCHES:
25459
+ break
25460
+ else:
25461
+ low = needle.lower()
25462
+ for idx, row in enumerate(lines):
25463
+ if low in row.lower():
25464
+ matches.append(idx)
25465
+ if len(matches) >= READ_FILE_SEARCH_MAX_MATCHES:
25466
+ break
25467
+ if not matches:
25468
+ return f"[read_file search path={rel} query={needle!r} matches=0]\n(no matches)"
25469
+ ranges: list[tuple[int, int]] = []
25470
+ for idx in matches:
25471
+ start = max(0, idx - ctx)
25472
+ end = min(total, idx + ctx + 1)
25473
+ if ranges and start <= ranges[-1][1]:
25474
+ ranges[-1] = (ranges[-1][0], max(ranges[-1][1], end))
25475
+ else:
25476
+ ranges.append((start, end))
25477
+ out = [
25478
+ (
25479
+ f"[read_file search path={rel} query={needle!r} matches_returned={len(matches)} "
25480
+ f"windows={len(ranges)} total_lines={total}]"
25481
+ )
25482
+ ]
25483
+ for start, end in ranges:
25484
+ out.append(f"\n@@ lines {start + 1}-{end} @@")
25485
+ for line_no in range(start, end):
25486
+ out.append(f"{line_no + 1:>6}: {lines[line_no]}")
25487
+ return self._clip_read_file_output("\n".join(out), self._read_file_max_chars(max_chars))
25488
+
25489
+ def _render_symbol_text_read(
25490
+ self,
25491
+ fp: Path,
25492
+ rel: str,
25493
+ lines: list[str],
25494
+ *,
25495
+ target: object = "",
25496
+ context: object = None,
25497
+ max_chars: object = None,
25498
+ ) -> str:
25499
+ symbol_name = str(target or "").strip()
25500
+ if not symbol_name:
25501
+ return (
25502
+ f"[read_file symbol path={rel}]\n"
25503
+ "Error: target is required for mode=\"symbol\".\n"
25504
+ + self._render_text_overview(fp, rel, lines, max_chars=max_chars)
25505
+ )
25506
+ data = self._read_file_code_data(fp, lines)
25507
+ symbols = data.get("symbols", []) if isinstance(data.get("symbols"), list) else []
25508
+ wanted = symbol_name.lower()
25509
+
25510
+ def score(row: dict) -> int:
25511
+ name = str(row.get("name", "") or "").strip().lower()
25512
+ sig = str(row.get("signature", "") or "").strip().lower()
25513
+ if name == wanted:
25514
+ return 100
25515
+ if name.endswith("." + wanted):
25516
+ return 90
25517
+ if wanted in name:
25518
+ return 70
25519
+ if wanted in sig:
25520
+ return 50
25521
+ return 0
25522
+
25523
+ ranked = sorted(((score(row), row) for row in symbols), key=lambda x: (-x[0], int(x[1].get("line_start", 0) or 0)))
25524
+ match = next((row for sc, row in ranked if sc > 0), None)
25525
+ if not match:
25526
+ available = ", ".join(str(row.get("name", "")) for row in symbols[:40] if str(row.get("name", "")).strip())
25527
+ return (
25528
+ f"[read_file symbol path={rel} target={symbol_name!r} matches=0]\n"
25529
+ f"Available symbols: {available or '(none detected)'}\n"
25530
+ f"Try read_file path=\"{rel}\" mode=\"search\" query=\"{symbol_name}\""
25531
+ )
25532
+ total = len(lines)
25533
+ ctx = self._read_file_int_arg(context, 0, 0, 1000)
25534
+ start = max(1, int(match.get("line_start", 1) or 1) - ctx)
25535
+ end = min(total, int(match.get("line_end", start) or start) + ctx)
25536
+ body = "\n".join(lines[start - 1:end])
25537
+ header = (
25538
+ f"[read_file symbol path={rel} target={symbol_name!r} "
25539
+ f"matched={match.get('kind', 'symbol')} {match.get('name', '')} lines={start}-{end} of {total}]\n"
25540
+ )
25541
+ return header + self._clip_read_file_output(body, self._read_file_max_chars(max_chars))
25542
+
25543
+ def _run_read(
25544
+ self,
25545
+ path: str,
25546
+ limit: int | None = None,
25547
+ offset: int | None = None,
25548
+ *,
25549
+ mode: object = None,
25550
+ target: object = "",
25551
+ query: object = "",
25552
+ line: object = None,
25553
+ context: object = None,
25554
+ regex: object = False,
25555
+ max_chars: object = None,
25556
+ ) -> str:
24377
25557
  try:
24378
25558
  rel = self._normalize_tool_path_text(path)
24379
25559
  fp = self._fuzzy_resolve_path(self._session_path(rel))
@@ -24381,7 +25561,7 @@ body{padding:18px}
24381
25561
  if not fp.exists():
24382
25562
  return self._render_missing_read_hint(rel)
24383
25563
  if fp.is_dir():
24384
- return self._render_directory_read(fp, rel, limit=limit, offset=offset)
25564
+ return self._render_directory_read(fp, rel, limit=limit, offset=offset, query=query, max_chars=max_chars)
24385
25565
  # Multimodal: detect image/audio/video files and handle natively
24386
25566
  ext = fp.suffix.lower() if fp.suffix else ""
24387
25567
  if ext in IMAGE_EXTS:
@@ -24398,54 +25578,65 @@ body{padding:18px}
24398
25578
  file_size = int(fp.stat().st_size)
24399
25579
  except Exception:
24400
25580
  file_size = 0
25581
+ mode_text = str(mode or "auto").strip().lower() or "auto"
25582
+ if mode_text not in {"auto", "full", "overview", "window", "symbol", "search", "directory"}:
25583
+ mode_text = "auto"
25584
+ if mode_text == "auto":
25585
+ if str(target or "").strip():
25586
+ mode_text = "symbol"
25587
+ elif str(query or "").strip():
25588
+ mode_text = "search"
25589
+ elif line not in (None, ""):
25590
+ mode_text = "window"
25591
+ if mode_text == "directory":
25592
+ return f"Error: path is a file, not a directory: {rel}"
25593
+ if mode_text == "overview":
25594
+ return self._render_text_overview(fp, rel, lines, max_chars=max_chars)
25595
+ if mode_text == "full":
25596
+ return self._render_full_text_read(rel, lines, max_chars=max_chars)
25597
+ if mode_text == "search":
25598
+ return self._render_search_text_read(
25599
+ rel,
25600
+ lines,
25601
+ query=query or target,
25602
+ regex=regex,
25603
+ context=context,
25604
+ max_chars=max_chars,
25605
+ )
25606
+ if mode_text == "symbol":
25607
+ return self._render_symbol_text_read(
25608
+ fp,
25609
+ rel,
25610
+ lines,
25611
+ target=target or query,
25612
+ context=context,
25613
+ max_chars=max_chars,
25614
+ )
25615
+ if (
25616
+ mode_text == "window"
25617
+ or limit is not None
25618
+ or self._read_file_int_arg(offset, 0, 0, 1_000_000) > 0
25619
+ or line not in (None, "")
25620
+ ):
25621
+ return self._render_window_text_read(
25622
+ rel,
25623
+ lines,
25624
+ limit=limit,
25625
+ offset=offset,
25626
+ line=line,
25627
+ context=context,
25628
+ max_chars=max_chars,
25629
+ )
24401
25630
  if (
24402
25631
  limit is None
24403
- and int(offset or 0) <= 0
25632
+ and self._read_file_int_arg(offset, 0, 0, 1_000_000) <= 0
24404
25633
  and (file_size >= LARGE_FILE_AUTO_PAGE_BYTES or total_lines >= LARGE_FILE_AUTO_PAGE_LINES)
24405
25634
  ):
24406
- return self._large_text_file_overview(fp, rel, lines)
24407
- offset_val = max(0, int(offset or 0))
24408
- requested_limit = max(1, int(limit or LONG_OUTPUT_READ_PAGE_LINES))
24409
- if offset_val >= total_lines:
24410
- return (
24411
- f"[read_file page path={rel} lines=0 of {total_lines} offset={offset_val}]\n"
24412
- "[end_of_file]"
24413
- )
25635
+ return self._render_text_overview(fp, rel, lines, max_chars=max_chars)
24414
25636
  full_text = "\n".join(lines)
24415
- auto_paginate = limit is None and offset_val == 0 and len(full_text) > LONG_OUTPUT_READ_PAGE_MAX_CHARS
24416
- if not auto_paginate and limit is None and offset_val == 0 and len(full_text) <= MAX_TOOL_OUTPUT:
25637
+ if len(full_text) <= self._read_file_max_chars(max_chars):
24417
25638
  return full_text
24418
- page_parts: list[str] = []
24419
- chars = 0
24420
- idx = offset_val
24421
- while idx < total_lines and len(page_parts) < requested_limit:
24422
- line = lines[idx]
24423
- piece = line if not page_parts else "\n" + line
24424
- if page_parts and (chars + len(piece)) > LONG_OUTPUT_READ_PAGE_MAX_CHARS:
24425
- break
24426
- if (not page_parts) and len(line) > LONG_OUTPUT_READ_PAGE_MAX_CHARS:
24427
- page_parts.append(line[:LONG_OUTPUT_READ_PAGE_MAX_CHARS])
24428
- idx += 1
24429
- break
24430
- page_parts.append(piece if page_parts else line)
24431
- chars += len(piece)
24432
- idx += 1
24433
- body = "".join(page_parts)
24434
- page_no = (offset_val // requested_limit) + 1
24435
- total_pages = max(1, (total_lines + requested_limit - 1) // requested_limit)
24436
- out = [
24437
- (
24438
- f"[read_file page path={rel} lines={offset_val + 1}-{idx} of {total_lines} "
24439
- f"page={page_no}/{total_pages} offset={offset_val} limit={requested_limit}]"
24440
- ),
24441
- body,
24442
- ]
24443
- if idx < total_lines:
24444
- out.append(f"[next_page read_file path=\"{rel}\" offset={idx} limit={requested_limit}]")
24445
- if offset_val > 0:
24446
- prev_offset = max(0, offset_val - requested_limit)
24447
- out.append(f"[prev_page read_file path=\"{rel}\" offset={prev_offset} limit={requested_limit}]")
24448
- return "\n".join(part for part in out if part != "")
25639
+ return self._render_text_overview(fp, rel, lines, max_chars=max_chars)
24449
25640
  except Exception as exc:
24450
25641
  return f"Error: {type(exc).__name__}: {exc}"
24451
25642
 
@@ -24514,6 +25705,22 @@ body{padding:18px}
24514
25705
  f"File exists at {fp}. Use bash tools to process it if needed."
24515
25706
  )
24516
25707
 
25708
+ def _tool_result_context_content(
25709
+ self,
25710
+ name: str,
25711
+ args: dict | None,
25712
+ output: object,
25713
+ default_limit: int = MAX_TOOL_OUTPUT,
25714
+ ) -> str:
25715
+ text = str(output or "")
25716
+ tool_name = canonicalize_tool_name(name)
25717
+ if tool_name == "read_file":
25718
+ req = (args or {}).get("max_chars") if isinstance(args, dict) else None
25719
+ requested = self._read_file_max_chars(req, default=READ_FILE_DEFAULT_MAX_CHARS)
25720
+ cap = max(int(default_limit or 0), min(int(READ_FILE_HARD_MAX_CHARS), requested))
25721
+ return trim(text, cap)
25722
+ return trim(text, int(default_limit or MAX_TOOL_OUTPUT))
25723
+
24517
25724
  def _is_html_file_rel(self, path: str) -> bool:
24518
25725
  low = str(path or "").strip().lower()
24519
25726
  return low.endswith(".html") or low.endswith(".htm")
@@ -24771,8 +25978,29 @@ body{padding:18px}
24771
25978
  tool_def("bash", "Run command.", {"command": {"type": "string"}}, ["command"]),
24772
25979
  tool_def(
24773
25980
  "read_file",
24774
- "Read file with optional line pagination.",
24775
- {"path": {"type": "string"}, "limit": {"type": "integer"}, "offset": {"type": "integer"}},
25981
+ (
25982
+ "Read files or directories with structure-aware modes. "
25983
+ "Examples: large.py + func_42 -> mode='symbol' target='func_42'; "
25984
+ "app.py line 240 -> mode='window' line=240 context=5; "
25985
+ "run.txt E123 -> mode='search' query='E123'. "
25986
+ "Use mode='symbol', 'search', or 'window' for focused reads; use mode='full' only when needed."
25987
+ ),
25988
+ {
25989
+ "path": {"type": "string"},
25990
+ "mode": {
25991
+ "type": "string",
25992
+ "enum": ["auto", "full", "overview", "window", "symbol", "search", "directory"],
25993
+ "description": "Reading strategy. Use symbol with target for a function/class; search with query for known text/errors; window with line/context for a line range. Avoid full for large logs when a query is known.",
25994
+ },
25995
+ "target": {"type": "string", "description": "Symbol name for mode='symbol', for example 'ClassName.method' or 'func_42'."},
25996
+ "query": {"type": "string", "description": "Search text or regex for mode='search'; can also be used when target is unknown."},
25997
+ "line": {"type": "integer", "description": "1-based center line for mode='window'."},
25998
+ "context": {"type": "integer", "description": "Number of surrounding lines for mode='window' or mode='search'."},
25999
+ "regex": {"type": "boolean", "description": "Treat query as a regular expression in mode='search'."},
26000
+ "max_chars": {"type": "integer", "description": "Maximum characters to return for broad reads; use only when wider context is needed."},
26001
+ "limit": {"type": "integer", "description": "Legacy line count for compatibility; prefer mode/context for new calls."},
26002
+ "offset": {"type": "integer", "description": "Legacy 0-based line offset for compatibility; prefer mode='window' with line/context."},
26003
+ },
24776
26004
  ["path"],
24777
26005
  ),
24778
26006
  ]
@@ -24848,14 +26076,30 @@ body{padding:18px}
24848
26076
  if name == "bash":
24849
26077
  out = self._run_bash(args.get("command", ""))
24850
26078
  elif name == "read_file":
24851
- out = self._run_read(args.get("path", ""), args.get("limit"), args.get("offset"))
26079
+ out = self._run_read(
26080
+ args.get("path", ""),
26081
+ args.get("limit"),
26082
+ args.get("offset"),
26083
+ mode=args.get("mode"),
26084
+ target=args.get("target"),
26085
+ query=args.get("query"),
26086
+ line=args.get("line"),
26087
+ context=args.get("context"),
26088
+ regex=args.get("regex"),
26089
+ max_chars=args.get("max_chars"),
26090
+ )
24852
26091
  elif name == "write_file":
24853
26092
  out = self._run_write(args.get("path", ""), args.get("content", ""))
24854
26093
  elif name == "edit_file":
24855
26094
  out = self._run_edit(args.get("path", ""), args.get("old_text", ""), args.get("new_text", ""))
24856
26095
  else:
24857
26096
  out = f"Unknown tool: {name}"
24858
- msgs.append({"role": "tool", "tool_call_id": tc["id"], "name": name, "content": trim(out)})
26097
+ msgs.append({
26098
+ "role": "tool",
26099
+ "tool_call_id": tc["id"],
26100
+ "name": name,
26101
+ "content": self._tool_result_context_content(name, args if isinstance(args, dict) else {}, out),
26102
+ })
24859
26103
  return last_text or "(subagent done)"
24860
26104
 
24861
26105
  def _spawn_teammate(self, name: str, role: str, prompt: str) -> str:
@@ -31359,6 +32603,9 @@ body{padding:18px}
31359
32603
  html_hint = self._html_frontend_boost_instruction()
31360
32604
  # Loaded skills constraint for manager
31361
32605
  skills_constraint = self._loaded_skills_prompt_hint(for_role="manager")
32606
+ skills_context = self._loaded_skills_context_block(for_role="manager", max_chars=5200)
32607
+ if skills_context:
32608
+ skills_constraint += skills_context
31362
32609
  bb_skills = board.get("loaded_skills", {})
31363
32610
  if isinstance(bb_skills, dict) and bb_skills:
31364
32611
  skill_names = list(bb_skills.keys())[:5]
@@ -32628,6 +33875,11 @@ body{padding:18px}
32628
33875
  )
32629
33876
  self._append_manager_context({"role": "user", "content": prompt, "ts": now_ts()})
32630
33877
  self._microcompact_agent_messages(self.manager_context)
33878
+ self._apply_auto_compact_if_needed(
33879
+ "auto:manager",
33880
+ role="manager",
33881
+ media_inputs=media_inputs_round,
33882
+ )
32631
33883
  with self.lock:
32632
33884
  self.current_phase = "manager:model-call"
32633
33885
  self.current_tool_name = ""
@@ -33719,9 +34971,23 @@ body{padding:18px}
33719
34971
  return self._sanitize_agent_role(m.group(1)), self._sanitize_agent_role(m.group(2))
33720
34972
 
33721
34973
  def _effective_execution_mode(self) -> str:
33722
- runtime_mode = normalize_execution_mode(self.runtime_execution_mode, default="")
34974
+ runtime_raw = str(self.runtime_execution_mode or "").strip()
34975
+ runtime_mode = normalize_execution_mode(runtime_raw, default="") if runtime_raw else ""
33723
34976
  if runtime_mode in {EXECUTION_MODE_SINGLE, EXECUTION_MODE_SEQUENTIAL, EXECUTION_MODE_SYNC}:
33724
34977
  return runtime_mode
34978
+ try:
34979
+ bb = self.blackboard if isinstance(self.blackboard, dict) else {}
34980
+ profile = bb.get("task_profile", {}) if isinstance(bb.get("task_profile", {}), dict) else {}
34981
+ judgement = bb.get("manager_judgement", {}) if isinstance(bb.get("manager_judgement", {}), dict) else {}
34982
+ for raw in (
34983
+ profile.get("execution_mode", ""),
34984
+ judgement.get("execution_mode", ""),
34985
+ ):
34986
+ mode = normalize_execution_mode(raw, default="")
34987
+ if mode in {EXECUTION_MODE_SINGLE, EXECUTION_MODE_SEQUENTIAL, EXECUTION_MODE_SYNC}:
34988
+ return mode
34989
+ except Exception:
34990
+ pass
33725
34991
  return normalize_execution_mode(self.execution_mode, default=EXECUTION_MODE_SYNC)
33726
34992
 
33727
34993
  def _is_multi_agent_mode(self) -> bool:
@@ -33999,6 +35265,8 @@ body{padding:18px}
33999
35265
  "Do not leave '/js_lib/...', '/assets/js_lib/...', or other virtual aliases in final exported HTML. "
34000
35266
  "Use blackboard for shared state, ask_colleague for inter-agent communication. "
34001
35267
  "Keep outputs concise and action-oriented. "
35268
+ "When reading files, choose the shape that matches the question: mode='window' for file:line, mode='symbol' for named code, mode='search' for keywords/errors, mode='overview' for structure, and mode='full' only when exact broad context is required. "
35269
+ "When inspecting collections or memory, use focused modes too: context_recall/read_from_blackboard/task_list/check_background/read_inbox/worktree_events support mode='summary', mode='search', mode='window', and mode='detail' where applicable. Prefer query/status/actor/tool filters over repeatedly listing recent items. "
34002
35270
  f"{code_note + ' ' if code_note else ''}"
34003
35271
  f"{engineering_note + ' ' if engineering_note else ''}"
34004
35272
  f"{html_note + ' ' if html_note else ''}"
@@ -34052,8 +35320,8 @@ body{padding:18px}
34052
35320
  )
34053
35321
  return base + (
34054
35322
  "Role: implement code changes, execute tools, record progress to blackboard. "
34055
- "SKILL PRIORITY (critical): When ACTIVE SKILLS are listed above, find the "
34056
- "<loaded-skill> messages in your context and READ them before starting any step. "
35323
+ "SKILL PRIORITY (critical): When ACTIVE SKILLS are listed above, read the "
35324
+ "ACTIVE SKILL WORKFLOWS or <loaded-skill> messages in your context before starting any step. "
34057
35325
  "The skill's workflow, tools, and file structure OVERRIDE the plan's implementation "
34058
35326
  "approach — if the plan says 'use python-pptx' but the skill says 'use PptxGenJS', "
34059
35327
  "use PptxGenJS. The skill defines HOW to implement; the plan defines WHAT to do. "
@@ -34068,13 +35336,13 @@ body{padding:18px}
34068
35336
  "Do not silently batch multiple subtasks and do not delay todo updates until the end of the step. "
34069
35337
  "This manual update is critical because skill re-evaluation is triggered by actual todo progress. "
34070
35338
  "EDIT METHODOLOGY (follow strictly): "
34071
- "1) Read the EXACT target location using read_file before any edit — never edit from memory. "
35339
+ "1) Read the EXACT target location using read_file before any edit — prefer mode='symbol', mode='search', or mode='window' to get the right context directly; never edit from memory. "
34072
35340
  "2) Copy EXACT text into old_text (preserve all whitespace/indentation/line breaks). "
34073
35341
  "3) Keep old_text as SHORT as possible while still unique (1-3 lines ideal). "
34074
35342
  "4) If edit_file fails 'text not found': IMMEDIATELY re-read the file, compare whitespace, retry with exact content. "
34075
35343
  "5) If edit_file fails 2+ times on same file: switch to write_file to rewrite entire file. "
34076
35344
  "6) After every successful edit, run build/test to verify. "
34077
- "NEVER loop on read_file without attempting a concrete edit_file, write_file, path reconciliation, or verification call. "
35345
+ "If read_file is not answering the question, change the read shape (overview/symbol/search/window/full), form a sharper hypothesis, then attempt a concrete edit_file, write_file, path reconciliation, or verification call. "
34078
35346
  "PROBLEM-SOLVING (critical): "
34079
35347
  "When you discover missing files, broken imports, or incomplete source code: "
34080
35348
  "A) Think deeply about what the missing content should contain based on ALL available context "
@@ -34082,7 +35350,7 @@ body{padding:18px}
34082
35350
  "B) CREATE the missing files yourself using write_file — do not wait or re-read. "
34083
35351
  "C) If compilation fails due to missing dependencies, write stub implementations. "
34084
35352
  "D) If read_file or bash says a path is missing, empty, or mismatched, reconcile the path against uploads, recent files, and close matches before trying again. "
34085
- "E) NEVER re-read the same directory/file more than twice after 2 reads, you MUST act. "
35353
+ "E) Avoid repeated identical reads; when you need more evidence, ask a more specific read_file question instead of reopening the same context. "
34086
35354
  "F) Do not declare success until at least one fix-and-verify cycle is complete and the evidence is observable. "
34087
35355
  "G) If truly blocked, explain WHY to the user and propose alternatives. "
34088
35356
  )
@@ -34317,23 +35585,9 @@ body{padding:18px}
34317
35585
  )
34318
35586
 
34319
35587
  def _tighten_context_for_segmented_retry(self, reason: str, output_tokens: int):
34320
- if self.context_limit_locked:
34321
- return
34322
- old_bound = int(self.context_token_upper_bound)
34323
- reduced = int(max(MIN_CONTEXT_TOKEN_LIMIT, min(old_bound - 1200, old_bound * 0.78)))
34324
- derived = self._derive_context_limit_from_output(max(1, output_tokens))
34325
- new_bound = min(old_bound, reduced, derived)
34326
- if new_bound < old_bound:
34327
- self.context_token_upper_bound = new_bound
34328
- self._emit(
34329
- "status",
34330
- {
34331
- "summary": (
34332
- "context upper bound reduced for segmented retry "
34333
- f"{old_bound}->{new_bound} ({trim(reason, 90)})"
34334
- )
34335
- },
34336
- )
35588
+ # Segmented retry is an execution-shaping signal. It should not shrink
35589
+ # the input context window unless a provider reports a real context error.
35590
+ self.context_limit_source = str(getattr(self, "context_limit_source", "") or "configured")
34337
35591
 
34338
35592
  def _inject_segmented_retry_hint(self, name: str, reason: str):
34339
35593
  goal = self._latest_user_goal_text()
@@ -34485,7 +35739,7 @@ body{padding:18px}
34485
35739
  actions.append("split into smaller subtasks and regenerate full tool JSON")
34486
35740
  if compact_hints > 0:
34487
35741
  causes.append("context may be compacted")
34488
- actions.append("call context_recall first for latest segment details")
35742
+ actions.append("call context_recall mode='summary', then mode='search' or mode='window' for focused details")
34489
35743
  if tool_errors > 0:
34490
35744
  causes.append("recent tool execution errors")
34491
35745
  actions.append("repair arguments for one failing tool and retry only that tool")
@@ -34527,7 +35781,7 @@ body{padding:18px}
34527
35781
  seg_id = str(seg.get("id", "")).strip()
34528
35782
  if seg_id:
34529
35783
  recall_hint = (
34530
- f"Call context_recall first with segment_id='{seg_id}', max_messages=40, offset=0. "
35784
+ f"Call context_recall with segment_id='{seg_id}' and mode='summary' to map the segment, then mode='search' for the missing topic or mode='window' with around_index/context for nearby messages. "
34531
35785
  )
34532
35786
  self._prune_runtime_retry_hints()
34533
35787
  self.messages.append(
@@ -34556,6 +35810,30 @@ body{padding:18px}
34556
35810
  },
34557
35811
  )
34558
35812
 
35813
+ def _inject_action_promise_recovery_hint(self, assistant_text: str):
35814
+ goal = trim(str(self._latest_user_goal_text() or ""), 500)
35815
+ latest = trim(strip_thinking_content(str(assistant_text or "")).strip(), 500)
35816
+ self._prune_runtime_retry_hints()
35817
+ self.messages.append(
35818
+ {
35819
+ "role": "user",
35820
+ "content": (
35821
+ "<no-tool-recovery>"
35822
+ "你刚才说明要开始创建/编写/构建产物,但没有调用任何工具,因此产物并未创建。"
35823
+ "如果任务已完成,请调用 finish_task 并给出文件路径和验证证据;"
35824
+ "否则下一轮必须只执行一个具体工具调用来推进,例如 write_file 创建 HTML、"
35825
+ "edit_file 修改文件、或 bash 运行必要的复制/验证命令。不要再输出计划性说明。"
35826
+ f"目标: {goal}. 上一条文本: {latest}"
35827
+ "</no-tool-recovery>"
35828
+ ),
35829
+ "ts": now_ts(),
35830
+ }
35831
+ )
35832
+ self._emit(
35833
+ "status",
35834
+ {"summary": "no-tool action promise detected; forcing one concrete tool turn"},
35835
+ )
35836
+
34559
35837
  def _inject_thinking_empty_recovery_hint(self, streak: int = 1, budget_forced: bool = False):
34560
35838
  self._prune_runtime_retry_hints()
34561
35839
  budget_note = (
@@ -34693,7 +35971,7 @@ body{padding:18px}
34693
35971
  if "<auto-context-recall>" in content:
34694
35972
  return False
34695
35973
  try:
34696
- recalled = self._context_recall({"recent_segments": 1, "max_messages": 24, "offset": 0})
35974
+ recalled = self._context_recall({"recent_segments": 1, "max_messages": 24, "mode": "summary"})
34697
35975
  except Exception:
34698
35976
  return False
34699
35977
  text = str(recalled or "").strip()
@@ -34791,13 +36069,13 @@ body{padding:18px}
34791
36069
  def _context_recall(self, args: dict) -> str:
34792
36070
  segment_id = str(args.get("segment_id", "") or "").strip()
34793
36071
  query = str(args.get("query", "") or "").strip()
34794
- recent_segments = int(args.get("recent_segments", 2) or 2)
34795
- max_messages = int(args.get("max_messages", 30) or 30)
34796
- offset = int(args.get("offset", 0) or 0)
36072
+ mode = str(args.get("mode", "") or "").strip().lower()
36073
+ role_filter = str(args.get("role", "") or "").strip()
36074
+ tool_filter = str(args.get("tool_name", "") or "").strip()
36075
+ recent_segments = self._tool_int_arg(args.get("recent_segments", 2), 2, 1, 20)
36076
+ max_messages = self._tool_int_arg(args.get("max_messages", 30), 30, 1, 120)
36077
+ offset = self._tool_int_arg(args.get("offset", 0), 0, 0, 100000)
34797
36078
  include_tools = bool(args.get("include_tools", True))
34798
- recent_segments = max(1, min(20, recent_segments))
34799
- max_messages = max(1, min(120, max_messages))
34800
- offset = max(0, offset)
34801
36079
 
34802
36080
  segments: list[dict] = []
34803
36081
  if segment_id:
@@ -34819,16 +36097,21 @@ body{padding:18px}
34819
36097
  role = str(row.get("role", ""))
34820
36098
  if not include_tools and role == "tool":
34821
36099
  continue
36100
+ if role_filter and role_filter.lower() not in role.lower():
36101
+ continue
36102
+ name = str(row.get("name", "") or "")
36103
+ if tool_filter and tool_filter.lower() not in name.lower():
36104
+ continue
34822
36105
  content = str(row.get("content", ""))
34823
36106
  if query_low:
34824
- pool = f"{role}\n{row.get('name', '')}\n{content}".lower()
36107
+ pool = f"{role}\n{name}\n{content}".lower()
34825
36108
  if query_low not in pool:
34826
36109
  continue
34827
36110
  out_row = {
34828
36111
  "segment_id": seg_id,
34829
36112
  "index": idx,
34830
36113
  "role": role,
34831
- "name": row.get("name", ""),
36114
+ "name": name,
34832
36115
  "tool_call_id": row.get("tool_call_id", ""),
34833
36116
  "ts": float(row.get("ts", 0.0) or 0.0),
34834
36117
  "content": trim(content, 6000),
@@ -34837,8 +36120,6 @@ body{padding:18px}
34837
36120
  out_row["thinking"] = trim(row.get("thinking", ""), 2500)
34838
36121
  matches.append(out_row)
34839
36122
 
34840
- total = len(matches)
34841
- page = matches[offset : offset + max_messages]
34842
36123
  selected_segments = [
34843
36124
  {
34844
36125
  "id": str(seg.get("id", "")),
@@ -34849,17 +36130,256 @@ body{padding:18px}
34849
36130
  }
34850
36131
  for seg in segments
34851
36132
  ]
34852
- payload = {
34853
- "query": query,
34854
- "segment_id": segment_id or "",
34855
- "segments_considered": selected_segments,
34856
- "total_matches": total,
34857
- "offset": offset,
34858
- "returned": len(page),
34859
- "next_offset": (offset + len(page)) if (offset + len(page)) < total else None,
34860
- "matches": page,
36133
+ if not mode and (args.get("offset") is not None):
36134
+ page = matches[offset : offset + max_messages]
36135
+ payload = {
36136
+ "mode": "legacy",
36137
+ "query": query,
36138
+ "segment_id": segment_id or "",
36139
+ "segments_considered": selected_segments,
36140
+ "total_matches": len(matches),
36141
+ "offset": offset,
36142
+ "returned": len(page),
36143
+ "matches": page,
36144
+ "focused_reads": [
36145
+ "context_recall mode='search' query='<term>'",
36146
+ "context_recall mode='window' around_index=<index> context=4",
36147
+ "context_recall mode='detail' query='<exact evidence>'",
36148
+ ],
36149
+ }
36150
+ return trim(json_dumps(payload, indent=2), self._tool_max_chars(args.get("max_chars"), 24000))
36151
+ rendered = self._render_collection_tool_payload(
36152
+ tool="context_recall",
36153
+ rows=matches,
36154
+ mode=mode or ("search" if query else "summary"),
36155
+ query="",
36156
+ limit=max_messages,
36157
+ around_index=args.get("around_index"),
36158
+ context=args.get("context"),
36159
+ max_chars=args.get("max_chars"),
36160
+ filters={},
36161
+ summary_fields=["segment_id", "index", "role", "name", "content"],
36162
+ detail_fields=["segment_id", "index", "role", "name", "tool_call_id", "ts", "content", "thinking"],
36163
+ default_limit=12,
36164
+ )
36165
+ try:
36166
+ payload = parse_json_object(rendered, {})
36167
+ if isinstance(payload, dict):
36168
+ payload["query"] = query
36169
+ payload["role"] = role_filter
36170
+ payload["tool_name"] = tool_filter
36171
+ payload["segments_considered"] = selected_segments
36172
+ return trim(json_dumps(payload, indent=2), self._tool_max_chars(args.get("max_chars"), 24000))
36173
+ except Exception:
36174
+ pass
36175
+ return rendered
36176
+
36177
+ def _read_blackboard_enhanced(self, args: dict) -> str:
36178
+ section = str(args.get("section", "all") or "all").strip().lower()
36179
+ mode = str(args.get("mode", "") or "").strip().lower()
36180
+ query = str(args.get("query", "") or "").strip()
36181
+ limit = self._tool_int_arg(args.get("limit", 6), 6, 1, 80)
36182
+ board = self._ensure_blackboard()
36183
+ if section in {"", "all"} and not mode and not query and not args.get("actor") and not args.get("around_index"):
36184
+ return self._blackboard_read_state_markdown(max_items=min(20, limit))
36185
+ if section == "original_goal":
36186
+ return trim(str(board.get("original_goal", "") or "").strip(), self._tool_max_chars(args.get("max_chars"), 4000)) or "(empty)"
36187
+ if section == "status":
36188
+ wd = board.get("watchdog", {}) if isinstance(board.get("watchdog"), dict) else {}
36189
+ dq = board.get("decomposition_queue", {}) if isinstance(board.get("decomposition_queue"), dict) else {}
36190
+ return trim(json_dumps(
36191
+ {
36192
+ "status": board.get("status", "INITIALIZING"),
36193
+ "active_agent": board.get("active_agent", ""),
36194
+ "manager_cycles": int(board.get("manager_cycles", 0) or 0),
36195
+ "manager_summary_attempts": int(board.get("manager_summary_attempts", 0) or 0),
36196
+ "approval": board.get("approval", {}),
36197
+ "last_delegate": board.get("last_delegate", {}),
36198
+ "watchdog": {
36199
+ "intent_no_tool_streak": int(wd.get("intent_no_tool_streak", 0) or 0),
36200
+ "repeat_no_tool_streak": int(wd.get("repeat_no_tool_streak", 0) or 0),
36201
+ "state_unchanged_streak": int(wd.get("state_unchanged_streak", 0) or 0),
36202
+ "trigger_count": int(wd.get("trigger_count", 0) or 0),
36203
+ "last_trigger_reason": trim(str(wd.get("last_trigger_reason", "") or "").strip(), 160),
36204
+ },
36205
+ "decomposition_queue": {
36206
+ "active": bool(dq.get("active", False)),
36207
+ "trigger_reason": trim(str(dq.get("trigger_reason", "") or "").strip(), 160),
36208
+ "cursor": int(dq.get("cursor", 0) or 0),
36209
+ "total": len(dq.get("steps", []) or []),
36210
+ "last_error": trim(str(dq.get("last_error", "") or "").strip(), 220),
36211
+ },
36212
+ },
36213
+ indent=2,
36214
+ ), self._tool_max_chars(args.get("max_chars"), 12000))
36215
+ sections = ["research_notes", "execution_logs", "review_feedback", "conversation_history"]
36216
+ rows: list[dict] = []
36217
+ if section == "code_artifacts":
36218
+ artifacts = board.get("code_artifacts", {})
36219
+ if not isinstance(artifacts, dict):
36220
+ artifacts = {}
36221
+ for key, value in artifacts.items():
36222
+ row = dict(value) if isinstance(value, dict) else {"value": value}
36223
+ row["key"] = key
36224
+ rows.append(row)
36225
+ elif section in sections:
36226
+ raw_rows = board.get(section, [])
36227
+ rows = [row for row in raw_rows if isinstance(row, dict)] if isinstance(raw_rows, list) else []
36228
+ elif section in {"", "all"}:
36229
+ for sec in sections:
36230
+ raw_rows = board.get(sec, [])
36231
+ if isinstance(raw_rows, list):
36232
+ for row in raw_rows:
36233
+ if isinstance(row, dict):
36234
+ item = dict(row)
36235
+ item["section"] = sec
36236
+ rows.append(item)
36237
+ artifacts = board.get("code_artifacts", {})
36238
+ if isinstance(artifacts, dict):
36239
+ for key, value in artifacts.items():
36240
+ item = dict(value) if isinstance(value, dict) else {"value": value}
36241
+ item["section"] = "code_artifacts"
36242
+ item["key"] = key
36243
+ rows.append(item)
36244
+ else:
36245
+ return f"Error: unsupported blackboard section '{section}'"
36246
+ return self._render_collection_tool_payload(
36247
+ tool="read_from_blackboard",
36248
+ rows=rows,
36249
+ mode=mode or ("search" if query else "summary"),
36250
+ query=query,
36251
+ limit=limit,
36252
+ around_index=args.get("around_index"),
36253
+ context=args.get("context"),
36254
+ max_chars=args.get("max_chars"),
36255
+ filters={"actor": args.get("actor"), "status": args.get("status")},
36256
+ summary_fields=["section", "key", "actor", "status", "content", "summary", "path"],
36257
+ detail_fields=[],
36258
+ default_limit=limit,
36259
+ )
36260
+
36261
+ def _task_list_enhanced(self, args: dict) -> str:
36262
+ rows = []
36263
+ for task in self.tasks.list_objects():
36264
+ if isinstance(task, dict):
36265
+ rows.append(dict(task))
36266
+ if not rows:
36267
+ return "No tasks."
36268
+ mode = str(args.get("mode", "") or "").strip().lower()
36269
+ query = str(args.get("query", "") or "").strip()
36270
+ limit = self._tool_int_arg(args.get("limit", 30), 30, 1, 200)
36271
+ if not mode and not query and not args.get("status") and not args.get("owner"):
36272
+ return self.tasks.list_all()
36273
+ return self._render_collection_tool_payload(
36274
+ tool="task_list",
36275
+ rows=rows,
36276
+ mode=mode or ("search" if query else "summary"),
36277
+ query=query,
36278
+ limit=limit,
36279
+ max_chars=args.get("max_chars"),
36280
+ filters={"status": args.get("status"), "owner": args.get("owner")},
36281
+ summary_fields=["id", "status", "owner", "subject", "blockedBy", "worktree", "updated_at"],
36282
+ detail_fields=[],
36283
+ default_limit=limit,
36284
+ )
36285
+
36286
+ def _check_background_enhanced(self, args: dict) -> str:
36287
+ task_id = str(args.get("task_id", "") or "").strip()
36288
+ mode = str(args.get("mode", "") or "").strip().lower()
36289
+ query = str(args.get("query", "") or "").strip()
36290
+ if task_id and not mode and not query:
36291
+ return self.bg.check(task_id)
36292
+ rows = self.bg.list_objects()
36293
+ if not rows:
36294
+ return "No bg tasks."
36295
+ if not task_id and not mode and not query and not args.get("status"):
36296
+ return self.bg.check(None)
36297
+ known_ids = {str(row.get("id", "") or "").strip() for row in rows if isinstance(row, dict)}
36298
+ if task_id and task_id not in known_ids and (mode or query or args.get("status")):
36299
+ if not query:
36300
+ query = task_id
36301
+ task_id = ""
36302
+ if query and mode in {"tail", "recent"}:
36303
+ mode = "search"
36304
+ filters = {"status": args.get("status")}
36305
+ if task_id:
36306
+ filters["id"] = task_id
36307
+ return self._render_collection_tool_payload(
36308
+ tool="check_background",
36309
+ rows=rows,
36310
+ mode=mode or ("search" if query else "summary"),
36311
+ query=query,
36312
+ limit=args.get("limit", 20),
36313
+ max_chars=args.get("max_chars"),
36314
+ filters=filters,
36315
+ summary_fields=["id", "status", "command", "started_at", "finished_at"],
36316
+ detail_fields=[],
36317
+ default_limit=20,
36318
+ )
36319
+
36320
+ def _read_inbox_enhanced(self, args: dict) -> str:
36321
+ mode = str(args.get("mode", "") or "").strip().lower()
36322
+ query = str(args.get("query", "") or "").strip()
36323
+ # Backward compatibility: no arguments means consume the inbox as before.
36324
+ if not args or (not mode and not query and not args.get("from") and not args.get("type")):
36325
+ return json_dumps(self.bus.read_inbox("lead"), indent=2)
36326
+ rows = self.bus.peek_inbox("lead")
36327
+ if mode == "drain":
36328
+ rows = self.bus.read_inbox("lead")
36329
+ if not rows:
36330
+ return "[]"
36331
+ return self._render_collection_tool_payload(
36332
+ tool="read_inbox",
36333
+ rows=rows,
36334
+ mode=mode or ("search" if query else "peek"),
36335
+ query=query,
36336
+ limit=args.get("limit", 20),
36337
+ max_chars=args.get("max_chars"),
36338
+ filters={"from": args.get("from"), "type": args.get("type")},
36339
+ summary_fields=["timestamp", "from", "type", "content"],
36340
+ detail_fields=[],
36341
+ default_limit=20,
36342
+ )
36343
+
36344
+ def _worktree_events_enhanced(self, args: dict) -> str:
36345
+ rows = self.worktrees.events_objects()
36346
+ if not rows:
36347
+ return "[]"
36348
+ mode = str(args.get("mode", "") or "").strip().lower()
36349
+ query = str(args.get("query", "") or "").strip()
36350
+ event_arg = str(args.get("event", "") or "").strip()
36351
+ worktree_arg = str(args.get("worktree", "") or "").strip()
36352
+ limit = self._tool_int_arg(args.get("limit", 20), 20, 1, 200)
36353
+ known_events = {
36354
+ str(row.get("event", "") or "").strip().lower()
36355
+ for row in rows
36356
+ if isinstance(row, dict) and str(row.get("event", "") or "").strip()
34861
36357
  }
34862
- return json_dumps(payload, indent=2)
36358
+ if event_arg and event_arg.lower() not in known_events and "." not in event_arg:
36359
+ if not worktree_arg:
36360
+ worktree_arg = event_arg
36361
+ elif not query:
36362
+ query = event_arg
36363
+ event_arg = ""
36364
+ if not mode and not query and not event_arg and not worktree_arg and not args.get("task_id"):
36365
+ return self.worktrees.events_recent(limit)
36366
+ filters = {
36367
+ "event": event_arg,
36368
+ "worktree.name": worktree_arg,
36369
+ "task.id": args.get("task_id"),
36370
+ }
36371
+ return self._render_collection_tool_payload(
36372
+ tool="worktree_events",
36373
+ rows=rows,
36374
+ mode=mode or ("search" if query else "summary"),
36375
+ query=query,
36376
+ limit=limit,
36377
+ max_chars=args.get("max_chars"),
36378
+ filters=filters,
36379
+ summary_fields=["ts", "event", "task", "worktree", "error"],
36380
+ detail_fields=[],
36381
+ default_limit=limit,
36382
+ )
34863
36383
 
34864
36384
  def _attempt_malformed_tool_repair(self, name: str, raw_args: object) -> tuple[bool, str]:
34865
36385
  # Safe auto-repair only for todo tools; file/code tools require regenerate.
@@ -34980,10 +36500,24 @@ body{padding:18px}
34980
36500
  rel = self._session_rel(fp)
34981
36501
  except Exception as exc:
34982
36502
  return f"Error: {type(exc).__name__}: {exc}"
34983
- out = self._run_read(rel, args.get("limit"), args.get("offset"))
34984
- limit_val = int(args.get("limit", 0) or 0) if args.get("limit") is not None else 0
34985
- offset_val = int(args.get("offset", 0) or 0) if args.get("offset") is not None else 0
36503
+ out = self._run_read(
36504
+ rel,
36505
+ args.get("limit"),
36506
+ args.get("offset"),
36507
+ mode=args.get("mode"),
36508
+ target=args.get("target"),
36509
+ query=args.get("query"),
36510
+ line=args.get("line"),
36511
+ context=args.get("context"),
36512
+ regex=args.get("regex"),
36513
+ max_chars=args.get("max_chars"),
36514
+ )
36515
+ limit_val = self._read_file_int_arg(args.get("limit", 0), 0, 0, 1_000_000) if args.get("limit") is not None else 0
36516
+ offset_val = self._read_file_int_arg(args.get("offset", 0), 0, 0, 1_000_000) if args.get("offset") is not None else 0
36517
+ mode_val = str(args.get("mode", "") or "").strip()
34986
36518
  summary = f"read file: {rel}"
36519
+ if mode_val:
36520
+ summary += f" mode={mode_val}"
34987
36521
  if offset_val > 0 or limit_val > 0:
34988
36522
  summary += (
34989
36523
  f" offset={offset_val}"
@@ -34993,12 +36527,13 @@ body{padding:18px}
34993
36527
  "file_read",
34994
36528
  {
34995
36529
  "path": rel,
36530
+ "mode": mode_val or "auto",
34996
36531
  "offset": offset_val,
34997
36532
  "limit": limit_val,
34998
36533
  "summary": summary,
34999
36534
  "large_file_guard": bool(
35000
36535
  limit_val <= 0
35001
- and str(out).startswith("[large_file_overview")
36536
+ and str(out).startswith("[read_file overview")
35002
36537
  ),
35003
36538
  },
35004
36539
  )
@@ -35205,8 +36740,9 @@ body{padding:18px}
35205
36740
  ):
35206
36741
  return (
35207
36742
  "Error: reviewer finalization requires blackboard evidence read. "
35208
- "Call read_from_blackboard first (sections: code_artifacts, execution_logs, "
35209
- "review_feedback, status), then call finish_task with structured summary."
36743
+ "Call read_from_blackboard with mode='summary' or mode='search' "
36744
+ "(sections: code_artifacts, execution_logs, review_feedback, status), "
36745
+ "then call finish_task with structured summary."
35210
36746
  )
35211
36747
  if not self._final_summary_sufficient(summary, strict=True):
35212
36748
  return (
@@ -35215,7 +36751,8 @@ body{padding:18px}
35215
36751
  "(1) changes/files touched, "
35216
36752
  "(2) validation evidence (tests/commands/results), "
35217
36753
  "(3) residual risks or next steps. "
35218
- "If evidence is missing, read_from_blackboard first or ask Explorer for final_summary_request."
36754
+ "If evidence is missing, use read_from_blackboard mode='summary' or mode='search', "
36755
+ "or ask Explorer for final_summary_request."
35219
36756
  )
35220
36757
  if name == "finish_task":
35221
36758
  todo_mark = self.todo.complete_all_open(summary)
@@ -35351,7 +36888,7 @@ body{padding:18px}
35351
36888
  )
35352
36889
  return str(payload.get("output", out_filtered or "(no output)"))
35353
36890
  if name == "check_background":
35354
- return self.bg.check(args.get("task_id"))
36891
+ return self._check_background_enhanced(args)
35355
36892
  if name == "task_create":
35356
36893
  return self.tasks.create(args["subject"], args.get("description", ""))
35357
36894
  if name == "task_get":
@@ -35359,7 +36896,7 @@ body{padding:18px}
35359
36896
  if name == "task_update":
35360
36897
  return self.tasks.update(int(args["task_id"]), args.get("status"), args.get("add_blocked_by"), args.get("add_blocks"))
35361
36898
  if name == "task_list":
35362
- return self.tasks.list_all()
36899
+ return self._task_list_enhanced(args)
35363
36900
  if name == "claim_task":
35364
36901
  return self.tasks.claim(int(args["task_id"]), "lead")
35365
36902
  if name == "spawn_teammate":
@@ -35402,57 +36939,7 @@ body{padding:18px}
35402
36939
  f"intent={env.get('intent')}, id={env.get('id')})"
35403
36940
  )
35404
36941
  if name == "read_from_blackboard":
35405
- section = str(args.get("section", "all") or "all").strip().lower()
35406
- limit = max(1, min(20, int(args.get("limit", 6) or 6)))
35407
- board = self._ensure_blackboard()
35408
- if section in {"", "all"}:
35409
- return self._blackboard_read_state_markdown(max_items=limit)
35410
- if section == "original_goal":
35411
- return trim(str(board.get("original_goal", "") or "").strip(), 4000) or "(empty)"
35412
- if section == "status":
35413
- wd = board.get("watchdog", {}) if isinstance(board.get("watchdog"), dict) else {}
35414
- dq = board.get("decomposition_queue", {}) if isinstance(board.get("decomposition_queue"), dict) else {}
35415
- return json_dumps(
35416
- {
35417
- "status": board.get("status", "INITIALIZING"),
35418
- "active_agent": board.get("active_agent", ""),
35419
- "manager_cycles": int(board.get("manager_cycles", 0) or 0),
35420
- "manager_summary_attempts": int(board.get("manager_summary_attempts", 0) or 0),
35421
- "approval": board.get("approval", {}),
35422
- "last_delegate": board.get("last_delegate", {}),
35423
- "watchdog": {
35424
- "intent_no_tool_streak": int(wd.get("intent_no_tool_streak", 0) or 0),
35425
- "repeat_no_tool_streak": int(wd.get("repeat_no_tool_streak", 0) or 0),
35426
- "state_unchanged_streak": int(wd.get("state_unchanged_streak", 0) or 0),
35427
- "trigger_count": int(wd.get("trigger_count", 0) or 0),
35428
- "last_trigger_reason": trim(str(wd.get("last_trigger_reason", "") or "").strip(), 160),
35429
- },
35430
- "decomposition_queue": {
35431
- "active": bool(dq.get("active", False)),
35432
- "trigger_reason": trim(str(dq.get("trigger_reason", "") or "").strip(), 160),
35433
- "cursor": int(dq.get("cursor", 0) or 0),
35434
- "total": len(dq.get("steps", []) or []),
35435
- "last_error": trim(str(dq.get("last_error", "") or "").strip(), 220),
35436
- },
35437
- },
35438
- indent=2,
35439
- )
35440
- if section == "code_artifacts":
35441
- artifacts = board.get("code_artifacts", {})
35442
- if not isinstance(artifacts, dict):
35443
- artifacts = {}
35444
- rows = sorted(
35445
- list(artifacts.items()),
35446
- key=lambda item: float((item[1] or {}).get("updated_at", 0.0) if isinstance(item[1], dict) else 0.0),
35447
- reverse=True,
35448
- )
35449
- return json_dumps({k: v for k, v in rows[:limit]}, indent=2)
35450
- if section in {"research_notes", "execution_logs", "review_feedback", "conversation_history"}:
35451
- rows = board.get(section, [])
35452
- if not isinstance(rows, list):
35453
- rows = []
35454
- return json_dumps(rows[-limit:], indent=2)
35455
- return f"Error: unsupported blackboard section '{section}'"
36942
+ return self._read_blackboard_enhanced(args)
35456
36943
  if name == "write_to_blackboard":
35457
36944
  section = str(args.get("section", "") or "").strip().lower()
35458
36945
  content = trim(str(args.get("content", "") or "").strip(), BLACKBOARD_MAX_TEXT)
@@ -35482,7 +36969,7 @@ body{padding:18px}
35482
36969
  if name == "send_message":
35483
36970
  return self.bus.send("lead", args["to"], args["content"], args.get("msg_type", "message"))
35484
36971
  if name == "read_inbox":
35485
- return json_dumps(self.bus.read_inbox("lead"), indent=2)
36972
+ return self._read_inbox_enhanced(args)
35486
36973
  if name == "broadcast":
35487
36974
  return self.bus.broadcast("lead", args["content"], list(self.teammates.keys()))
35488
36975
  if name == "shutdown_request":
@@ -35541,7 +37028,7 @@ body{padding:18px}
35541
37028
  if name == "worktree_remove":
35542
37029
  return self.worktrees.remove(args["name"], bool(args.get("force", False)), bool(args.get("complete_task", False)))
35543
37030
  if name == "worktree_events":
35544
- return self.worktrees.events_recent(int(args.get("limit", 20)))
37031
+ return self._worktree_events_enhanced(args)
35545
37032
  return f"Unknown tool: {name}"
35546
37033
 
35547
37034
  def _live_input_delay_locked(self) -> tuple[int, str]:
@@ -36166,6 +37653,12 @@ body{padding:18px}
36166
37653
  if not ctx:
36167
37654
  return {"status": "skip", "reason": "empty-context", "role": role_key}
36168
37655
  self._microcompact_agent_messages(ctx)
37656
+ self._apply_auto_compact_if_needed(
37657
+ f"auto:agent:{role_key}",
37658
+ role=role_key,
37659
+ media_inputs=media_inputs_round,
37660
+ )
37661
+ ctx = self._agent_context(role_key)
36169
37662
  with self.lock:
36170
37663
  self.current_phase = f"agent:{role_key}:model-call"
36171
37664
  self.current_tool_name = ""
@@ -36290,7 +37783,7 @@ body{padding:18px}
36290
37783
  "role": "tool",
36291
37784
  "tool_call_id": tc["id"],
36292
37785
  "name": name,
36293
- "content": trim(output),
37786
+ "content": self._tool_result_context_content(name, args if isinstance(args, dict) else {}, output),
36294
37787
  "ts": now_ts(),
36295
37788
  "agent_role": role_key,
36296
37789
  },
@@ -36545,7 +38038,7 @@ body{padding:18px}
36545
38038
  self._emit("status", {"summary": "sync loop break: stall escalated to plan mode"})
36546
38039
  break
36547
38040
  self._inject_pending_user_inputs()
36548
- self._apply_auto_compact_if_needed("auto:multi-sync")
38041
+ self._apply_auto_compact_if_needed("auto:multi-sync", role="manager")
36549
38042
  # Periodic checkpoint in multi-agent sync loop
36550
38043
  if rounds_used % CHECKPOINT_INTERVAL_ROUNDS == 0:
36551
38044
  self._maybe_create_checkpoint()
@@ -36898,7 +38391,7 @@ body{padding:18px}
36898
38391
  if self.cancel_requested:
36899
38392
  self._emit("status", {"summary": "run interrupted"})
36900
38393
  break
36901
- self._apply_auto_compact_if_needed("auto:multi-seq")
38394
+ self._apply_auto_compact_if_needed("auto:multi-seq", role=current_role)
36902
38395
  with self.lock:
36903
38396
  self.agent_round_index = int(self.agent_round_index) + 1
36904
38397
  latest_user_ts = self._latest_user_message_ts()
@@ -37118,11 +38611,13 @@ body{padding:18px}
37118
38611
  if preview:
37119
38612
  skill_previews.append(f"- **{skey}**: {preview}")
37120
38613
  if skill_previews:
38614
+ active_skill_workflows = self._loaded_skills_context_block(for_role="explorer", max_chars=5000)
37121
38615
  skills_section = (
37122
38616
  "\n## Loaded Skills\n"
37123
38617
  + "\n".join(skill_previews)
38618
+ + (f"\n\n{active_skill_workflows}\n" if active_skill_workflows else "")
37124
38619
  + "\n\nCRITICAL: For each loaded skill, you MUST:\n"
37125
- "1. Read the skill's full content from your context (it was injected as <loaded-skill>)\n"
38620
+ "1. Read the active-skill workflow block or <loaded-skill> content available in your context\n"
37126
38621
  "2. Identify the skill's concrete workflow steps (e.g., what scripts to run, what files to read/write)\n"
37127
38622
  "3. List the specific tool calls and file paths each skill requires\n"
37128
38623
  "4. Include these details in your plan_findings so the synthesis phase can produce actionable steps\n"
@@ -37163,13 +38658,15 @@ body{padding:18px}
37163
38658
  self._append_agent_context_message("explorer", {
37164
38659
  "role": "system",
37165
38660
  "content": (
37166
- "You are Explorer in plan-mode (read-only research). "
37167
- "Analyze the codebase to understand the task scope. "
37168
- "Do NOT modify any files. Use read_file, bash (read-only commands), "
37169
- "list_skills, load_skill, and blackboard tools only. "
37170
- f"{skills_block}"
37171
- "IMPORTANT: If the task requires specialized output (PPTX, reports, deep research, code review), "
37172
- "call list_skills first to discover relevant skills, then note in plan_findings which skills to use. "
38661
+ "You are Explorer in plan-mode (read-only research). "
38662
+ "Analyze the codebase to understand the task scope. "
38663
+ "Do NOT modify any files. Use read_file, bash (read-only commands), "
38664
+ "list_skills, load_skill, and blackboard tools only. "
38665
+ "When reading files, choose the shape that matches the question: mode='window' for file:line, mode='symbol' for named code, mode='search' for keywords/errors, mode='overview' for structure, and mode='full' only when exact broad context is required. "
38666
+ "When reading blackboard or archived context, use mode='summary' first, then mode='search' or mode='window' for focused evidence. "
38667
+ f"{skills_block}"
38668
+ "IMPORTANT: If the task requires specialized output (PPTX, reports, deep research, code review), "
38669
+ "call list_skills first to discover relevant skills, then note in plan_findings which skills to use. "
37173
38670
  f"{os_note} "
37174
38671
  f"{model_language_instruction(self.ui_language)}"
37175
38672
  ),
@@ -37200,6 +38697,8 @@ body{padding:18px}
37200
38697
  self.current_phase = f"plan-mode:explorer:round-{round_idx}"
37201
38698
  self.current_tool_name = ""
37202
38699
  self.active_agent_role = "explorer"
38700
+ self._apply_auto_compact_if_needed("auto:plan-explorer", role="explorer")
38701
+ ctx = self._agent_context("explorer")
37203
38702
  # Build skills awareness block (same as sync/single mode)
37204
38703
  skills_block = self._skills_awareness_block(for_role="explorer")
37205
38704
  response = self._chat_with_same_model_retry(
@@ -37209,6 +38708,8 @@ body{padding:18px}
37209
38708
  "You are Explorer in plan-mode research. Read-only analysis. "
37210
38709
  "Do NOT create, write, or edit files. "
37211
38710
  f"Workspace: \"{self.files_root}\" ($SESSION_ROOT). "
38711
+ "When reading files, choose the shape that matches the question: mode='window' for file:line, mode='symbol' for named code, mode='search' for keywords/errors, mode='overview' for structure, and mode='full' only when exact broad context is required. "
38712
+ "When reading blackboard or archived context, use mode='summary' first, then mode='search' or mode='window' for focused evidence. "
37212
38713
  f"{skills_block}"
37213
38714
  f"{_detect_os_shell_instruction()} "
37214
38715
  f"{model_language_instruction(self.ui_language)}"
@@ -37292,7 +38793,12 @@ body{padding:18px}
37292
38793
  self._append_agent_context_message("explorer", {
37293
38794
  "role": "tool",
37294
38795
  "tool_call_id": tc["id"],
37295
- "content": trim(result_content, 8000),
38796
+ "content": self._tool_result_context_content(
38797
+ fn_name,
38798
+ fn_args if isinstance(fn_args, dict) else {},
38799
+ result_content,
38800
+ 8000,
38801
+ ),
37296
38802
  "ts": now_ts(),
37297
38803
  "agent_role": "explorer",
37298
38804
  }, mirror_to_global=False)
@@ -37798,9 +39304,11 @@ body{padding:18px}
37798
39304
  if preview:
37799
39305
  skill_previews.append(f"- {skey}: {preview}")
37800
39306
  if skill_previews:
39307
+ active_skill_workflows = self._loaded_skills_context_block(for_role="developer", max_chars=6500)
37801
39308
  skills_section = (
37802
39309
  "\n## Available Skills\n"
37803
39310
  + "\n".join(skill_previews)
39311
+ + (f"\n\n{active_skill_workflows}\n" if active_skill_workflows else "")
37804
39312
  + "\n\nWhen skills are loaded, each step MUST specify concrete actions:\n"
37805
39313
  "- Which tool to call (bash, read_file, write_file, etc.)\n"
37806
39314
  "- Which specific file paths to use (e.g., 'Read uploaded/IEDM_.parsed.md')\n"
@@ -37887,14 +39395,32 @@ body{padding:18px}
37887
39395
  ), "ts": now_ts()},
37888
39396
  {"role": "user", "content": synthesis_prompt, "ts": now_ts()},
37889
39397
  ]
39398
+ synthesis_tools = self._plan_mode_synthesis_tools()
39399
+ synthesis_system = (
39400
+ "Generate a structured plan proposal. "
39401
+ "You MUST call submit_plan_proposal exactly once. "
39402
+ "Do not answer with plain text."
39403
+ )
39404
+ synthesis_metrics = self._context_metrics_for_model_call(
39405
+ synthesis_ctx,
39406
+ tools=synthesis_tools,
39407
+ system=synthesis_system,
39408
+ label="plan-mode synthesis",
39409
+ )
39410
+ synthesis_tier = self._context_compression_tier(synthesis_metrics)
39411
+ if synthesis_tier >= 1:
39412
+ self._compact_plan_context(synthesis_tier)
39413
+ self._compact_role_context("explorer", synthesis_tier)
39414
+ if synthesis_tier >= 2:
39415
+ compact_findings = trim(findings_text, 2400 if synthesis_tier == 2 else 1200)
39416
+ synthesis_ctx[1]["content"] = synthesis_ctx[1]["content"].replace(
39417
+ f"## Research Findings\n{trim(findings_text, 6000)}",
39418
+ f"## Research Findings\n{compact_findings}",
39419
+ )
37890
39420
  response = self._chat_with_same_model_retry(
37891
39421
  synthesis_ctx,
37892
- tools=self._plan_mode_synthesis_tools(),
37893
- system=(
37894
- "Generate a structured plan proposal. "
37895
- "You MUST call submit_plan_proposal exactly once. "
37896
- "Do not answer with plain text."
37897
- ),
39422
+ tools=synthesis_tools,
39423
+ system=synthesis_system,
37898
39424
  max_tokens=PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS,
37899
39425
  think=False,
37900
39426
  stream_thinking=False,
@@ -37917,7 +39443,7 @@ body{padding:18px}
37917
39443
  "ts": now_ts(),
37918
39444
  }
37919
39445
  ],
37920
- tools=self._plan_mode_synthesis_tools(),
39446
+ tools=synthesis_tools,
37921
39447
  system="You MUST call submit_plan_proposal exactly once. Do not answer with plain text.",
37922
39448
  max_tokens=PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS,
37923
39449
  think=False,
@@ -38692,6 +40218,56 @@ body{padding:18px}
38692
40218
  target = base_level + int(shift)
38693
40219
  return max(min(TASK_LEVEL_CHOICES), min(max(TASK_LEVEL_CHOICES), int(target)))
38694
40220
 
40221
+ def _prune_planner_context_after_plan_approval(self, choice_id: str, chosen: dict, grouped_steps: list):
40222
+ planner_rows = [
40223
+ m for m in self.messages
40224
+ if isinstance(m, dict) and str(m.get("agent_role", "") or "") == "planner"
40225
+ ]
40226
+ if not planner_rows:
40227
+ return
40228
+ seg = self._archive_context_segment(planner_rows, "plan-approved")
40229
+ seg_id = str(seg.get("id", "") if isinstance(seg, dict) else "")
40230
+ title = trim(str((chosen or {}).get("title", "") or choice_id), 220)
40231
+ summary = trim(str((chosen or {}).get("summary", "") or ""), 900)
40232
+ steps_text = "\n".join(
40233
+ f"- {trim(str(step or ''), 260)}"
40234
+ for step in list(grouped_steps or [])[:12]
40235
+ if str(step or "").strip()
40236
+ )
40237
+ handoff = (
40238
+ "<plan-approved-handoff>\n"
40239
+ f"chosen: {choice_id} {title}\n"
40240
+ f"summary: {summary}\n"
40241
+ f"steps:\n{steps_text or '- See blackboard plan.steps and .clouds_coder/plan.md'}\n"
40242
+ f"archived_planner_context: {seg_id or 'none'}\n"
40243
+ "Execution source of truth: blackboard plan.steps, project_todos, and .clouds_coder/plan.md. "
40244
+ "Old plan-mode research/proposal bubbles were compacted after approval; use context_recall only for missing evidence.\n"
40245
+ "</plan-approved-handoff>"
40246
+ )
40247
+ self.messages = [
40248
+ m for m in self.messages
40249
+ if not (isinstance(m, dict) and str(m.get("agent_role", "") or "") == "planner")
40250
+ ][-380:]
40251
+ self.messages.append(
40252
+ {
40253
+ "role": "assistant",
40254
+ "content": trim(handoff, 5000),
40255
+ "ts": now_ts(),
40256
+ "agent_role": "planner",
40257
+ "_compact_planner_handoff": True,
40258
+ }
40259
+ )
40260
+ self.runtime_plan_proposal = {
40261
+ "context": "approved plan compacted",
40262
+ "options": [dict(chosen or {})],
40263
+ "recommended": choice_id,
40264
+ }
40265
+ try:
40266
+ self._compact_role_context("explorer", 2)
40267
+ self._compact_shared_context(1)
40268
+ except Exception:
40269
+ pass
40270
+
38695
40271
  def _inject_plan_into_context(self, choice_id: str):
38696
40272
  chosen = next(
38697
40273
  (o for o in self.runtime_plan_proposal.get("options", [])
@@ -38843,6 +40419,10 @@ body{padding:18px}
38843
40419
  self._preload_skills_from_plan_steps(grouped_steps)
38844
40420
  except Exception:
38845
40421
  pass
40422
+ try:
40423
+ self._prune_planner_context_after_plan_approval(choice_id, chosen, grouped_steps)
40424
+ except Exception:
40425
+ pass
38846
40426
 
38847
40427
  def _preload_skills_from_plan_steps(self, steps: list):
38848
40428
  """Scan plan step text for skill names and auto-load any that aren't already loaded."""
@@ -39054,7 +40634,7 @@ body{padding:18px}
39054
40634
  },
39055
40635
  )
39056
40636
  break
39057
- self._apply_auto_compact_if_needed("auto")
40637
+ self._apply_auto_compact_if_needed("auto", role=single_role)
39058
40638
  # Periodic checkpoint in single-agent loop
39059
40639
  _sa_round = int(getattr(self, "agent_round_index", 0) or 0)
39060
40640
  if _sa_round > 0 and _sa_round % CHECKPOINT_INTERVAL_ROUNDS == 0:
@@ -39343,7 +40923,12 @@ body{padding:18px}
39343
40923
  )
39344
40924
  continue
39345
40925
  clean_decision_probe = strip_thinking_content(decision_probe).strip()
39346
- if bool(self.arbiter_enabled) and len(clean_decision_probe) >= int(ARBITER_TRIGGER_MIN_CONTENT_CHARS):
40926
+ arbiter_probe_len = max(len(clean_decision_probe), len(str(thinking_text or "").strip()))
40927
+ arbiter_should_run = bool(self.arbiter_enabled) and (
40928
+ arbiter_probe_len >= int(ARBITER_TRIGGER_MIN_CONTENT_CHARS)
40929
+ or self._looks_like_action_promise_without_tool(done_probe)
40930
+ )
40931
+ if arbiter_should_run:
39347
40932
  arbiter_decision = self._call_arbiter_llm(clean_decision_probe, thinking_text)
39348
40933
  arbiter_status = str(arbiter_decision.get("status", "") or "").strip().upper()
39349
40934
  if arbiter_status == "TASK_COMPLETED":
@@ -39362,6 +40947,35 @@ body{padding:18px}
39362
40947
  },
39363
40948
  )
39364
40949
  break
40950
+ if arbiter_status == "ACTION_REQUIRED":
40951
+ arbiter_planning_rounds = 0
40952
+ no_tool_rounds = 0
40953
+ fault_counter = 0
40954
+ last_fault_reason = ""
40955
+ force_single_tool_rounds = max(force_single_tool_rounds, 2)
40956
+ self._inject_arbiter_action_required_hint(arbiter_decision, done_probe)
40957
+ if auto_continue_budget > 0:
40958
+ auto_continue_budget -= 1
40959
+ self._emit(
40960
+ "status",
40961
+ {
40962
+ "summary": (
40963
+ "arbiter decision=ACTION_REQUIRED; "
40964
+ "auto-continue to concrete tool execution "
40965
+ f"(remaining={auto_continue_budget})"
40966
+ )
40967
+ },
40968
+ )
40969
+ continue
40970
+ self._emit(
40971
+ "status",
40972
+ {
40973
+ "summary": (
40974
+ "arbiter decision=ACTION_REQUIRED but auto-continue budget exhausted; "
40975
+ "falling back to no-tool handling"
40976
+ )
40977
+ },
40978
+ )
39365
40979
  if arbiter_status == "VALID_PLANNING":
39366
40980
  arbiter_planning_rounds += 1
39367
40981
  if arbiter_planning_rounds <= int(ARBITER_VALID_PLANNING_STREAK_LIMIT):
@@ -39438,6 +41052,7 @@ body{padding:18px}
39438
41052
  substantial_reply = self._looks_like_substantial_informative_reply(done_probe)
39439
41053
  done_like = self._looks_like_conclusive_reply(done_probe)
39440
41054
  todo_blocking = self._todo_should_block_auto_continue(done_probe)
41055
+ action_promise_pending = self._looks_like_action_promise_without_tool(done_probe)
39441
41056
  endpoint = self._detect_endpoint_intent(done_probe, tool_calls)
39442
41057
  if bool(endpoint.get("matched", False)):
39443
41058
  arbiter_planning_rounds = 0
@@ -39493,7 +41108,7 @@ body{padding:18px}
39493
41108
  break
39494
41109
  no_tool_rounds += 1
39495
41110
  diagnosis = self._diagnose_no_tool_idle(decision_probe, no_tool_rounds)
39496
- pending_like = bool(diagnosis.get("work_pending", False)) or todo_blocking
41111
+ pending_like = bool(diagnosis.get("work_pending", False)) or todo_blocking or action_promise_pending
39497
41112
  if no_tool_rounds >= 2 and pending_like:
39498
41113
  fault_counter += 1
39499
41114
  last_fault_reason = f"no-tool-idle(streak={no_tool_rounds})"
@@ -39515,7 +41130,11 @@ body{padding:18px}
39515
41130
  if (not done_like) and no_tool_rounds >= 1 and pending_like:
39516
41131
  if auto_continue_budget > 0:
39517
41132
  auto_continue_budget -= 1
39518
- if no_tool_rounds >= 2:
41133
+ if action_promise_pending:
41134
+ force_single_tool_rounds = max(force_single_tool_rounds, 2)
41135
+ self._inject_action_promise_recovery_hint(done_probe)
41136
+ summary = "no-tool action promise recovered"
41137
+ elif no_tool_rounds >= 2:
39519
41138
  force_single_tool_rounds = max(force_single_tool_rounds, 2)
39520
41139
  self._inject_no_tool_recovery_hint(diagnosis)
39521
41140
  summary = "no-tool recovery mode engaged"
@@ -39535,7 +41154,7 @@ body{padding:18px}
39535
41154
  if auto_continue_budget > 8 and not self._is_long_running_engineering_context():
39536
41155
  auto_continue_budget = min(auto_continue_budget, 8)
39537
41156
  can_continue = auto_continue_budget > 0 and (
39538
- todo_blocking or self._looks_like_incomplete_reply(text)
41157
+ todo_blocking or action_promise_pending or self._looks_like_incomplete_reply(text)
39539
41158
  )
39540
41159
  if can_continue:
39541
41160
  if self.cancel_requested:
@@ -39832,7 +41451,13 @@ body{padding:18px}
39832
41451
  manual_compact = True
39833
41452
  if dispatched_name in {"finish_task", "finish_current_task", "mark_done"} and not str(output).startswith("Error:"):
39834
41453
  stop_due_to_finish_task = True
39835
- self.messages.append({"role": "tool", "tool_call_id": tc["id"], "name": name, "content": trim(output), "ts": now_ts()})
41454
+ self.messages.append({
41455
+ "role": "tool",
41456
+ "tool_call_id": tc["id"],
41457
+ "name": name,
41458
+ "content": self._tool_result_context_content(name, args if isinstance(args, dict) else {}, output),
41459
+ "ts": now_ts(),
41460
+ })
39836
41461
  single_round_tool_results.append(
39837
41462
  {
39838
41463
  "name": dispatched_name or name,
@@ -40014,12 +41639,10 @@ body{padding:18px}
40014
41639
  "content": (
40015
41640
  "<read-loop-intervention>"
40016
41641
  f"{_read_loop_reason} "
40017
- "MANDATORY: Stop reading and take ONE concrete action: "
40018
- "1) If files are MISSING: create them based on context (docs, Makefile, imports). "
40019
- "2) If compilation FAILS: fix the error, do not re-read the same file. "
40020
- "3) If you are STUCK: report the blocker to the user and stop. "
40021
- "4) Think deeply about the user's goal and the project structure to find the solution. "
40022
- "Do NOT run ls, cat, find, or head on the same paths again."
41642
+ "Change strategy now: state the exact unanswered question, then use a more focused tool call "
41643
+ "(read_file mode='overview', 'symbol', 'search', or 'window'), reconcile the path, "
41644
+ "or take a concrete edit/verification action based on the current evidence. "
41645
+ "Think about the user's goal and project structure before opening more broad listings."
40023
41646
  "</read-loop-intervention>"
40024
41647
  ),
40025
41648
  "ts": now_ts(),
@@ -40593,6 +42216,7 @@ body{padding:18px}
40593
42216
  row["data"] = data
40594
42217
  operations_view.append(row)
40595
42218
  ctx = self._context_budget_metrics()
42219
+ agent_contexts_view = self._agent_context_budget_metrics_snapshot()
40596
42220
  model_catalog = self.model_catalog() if include_model_catalog else None
40597
42221
  blackboard = self._normalize_blackboard(self.blackboard)
40598
42222
  blackboard_view = (
@@ -40651,6 +42275,7 @@ body{padding:18px}
40651
42275
  "agent_round_index": int(self.agent_round_index),
40652
42276
  "agent_phase": str(self.current_phase or "idle"),
40653
42277
  "agent_active_tool": str(self.current_tool_name or ""),
42278
+ "agent_contexts": agent_contexts_view,
40654
42279
  "blackboard": blackboard_view,
40655
42280
  "queued_user_inputs_count": len(self.pending_user_inputs),
40656
42281
  "scheduler_queued_inputs_count": sum(
@@ -40663,6 +42288,12 @@ body{padding:18px}
40663
42288
  "context_reserve_percent": float(ctx.get("reserve_percent", 0.0)),
40664
42289
  "context_token_limit_config": int(self.max_context_token_limit),
40665
42290
  "context_token_limit_locked": bool(self.context_limit_locked),
42291
+ "context_limit_source": str(getattr(self, "context_limit_source", "") or "configured"),
42292
+ "context_next_call_estimate": int(getattr(self, "context_last_next_call_estimate", 0) or 0),
42293
+ "context_next_call_label": str(getattr(self, "context_last_next_call_label", "") or ""),
42294
+ "context_last_compact_effective": bool(getattr(self, "context_last_compact_effective", True)),
42295
+ "context_last_compact_used_reduction": int(getattr(self, "context_last_compact_used_reduction", 0) or 0),
42296
+ "context_last_compact_skip_reason": str(getattr(self, "context_last_compact_skip_reason", "") or ""),
40666
42297
  "context_estimator": str(ctx.get("estimator", "")),
40667
42298
  "context_estimate_safety_multiplier": float(ctx.get("safety_multiplier", CONTEXT_ESTIMATE_SAFETY_MULTIPLIER)),
40668
42299
  "context_estimate_base_safety_multiplier": float(ctx.get("base_safety_multiplier", CONTEXT_ESTIMATE_SAFETY_MULTIPLIER)),
@@ -42313,8 +43944,9 @@ body[data-ui-style="trad"] .msg-event-cell{background:#fff}
42313
43944
  .msg-run-dot{width:8px;height:8px;border-radius:999px;background:#13b8a6;box-shadow:0 0 0 0 rgba(19,184,166,.38);animation:msgRunPulse 1.6s ease-out infinite}
42314
43945
  @keyframes msgRunPulse{0%{box-shadow:0 0 0 0 rgba(19,184,166,.36)}70%{box-shadow:0 0 0 9px rgba(19,184,166,0)}100%{box-shadow:0 0 0 0 rgba(19,184,166,0)}}
42315
43946
  @media (prefers-reduced-motion:reduce){.msg-run-dot{animation:none}}
42316
- .msg-code-shell{margin:0;max-height:210px;overflow:auto;padding:8px;border:1px solid #dfe6ef;border-radius:8px;background:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;line-height:1.35;overscroll-behavior:contain;scrollbar-gutter:stable}
42317
- .msg-diff-shell{max-height:210px;overflow:auto;padding:8px;border:1px solid #dfe6ef;border-radius:8px;background:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;line-height:1.35;overscroll-behavior:contain;scrollbar-gutter:stable}
43947
+ .msg-code-shell{margin:0;max-height:210px;overflow:auto;padding:8px;border:1px solid #dfe6ef;border-radius:8px;background:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;line-height:1.35;white-space:pre;tab-size:4;word-break:normal;overflow-wrap:normal;overscroll-behavior:contain;scrollbar-gutter:stable}
43948
+ .msg-diff-shell{max-height:210px;overflow:auto;padding:8px;border:1px solid #dfe6ef;border-radius:8px;background:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;line-height:1.35;white-space:pre;tab-size:4;word-break:normal;overflow-wrap:normal;overscroll-behavior:contain;scrollbar-gutter:stable}
43949
+ .msg-diff-shell .diff-row{display:block;min-width:max-content;white-space:pre;tab-size:4}
42318
43950
  .composer{border-top:1px solid var(--line);padding-top:10px;margin-top:10px}
42319
43951
  .composer-shell{position:relative;border:1px solid var(--control-line);border-radius:16px;background:linear-gradient(180deg,var(--control-panel),var(--control-panel-soft));box-shadow:inset 0 1px 0 rgba(255,255,255,.7),0 10px 22px rgba(15,23,42,.05);overflow:hidden;transition:border-color .18s ease,box-shadow .18s ease,transform .18s ease}
42320
43952
  .composer-shell:focus-within{border-color:var(--focus-border);box-shadow:0 0 0 4px var(--focus-ring),inset 0 1px 0 rgba(255,255,255,.78),0 16px 34px rgba(15,23,42,.08)}
@@ -42344,7 +43976,11 @@ body[data-ui-style="trad"] .msg-event-cell{background:#fff}
42344
43976
  .ctx-live{margin-left:auto;display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid #d6deea;border-radius:999px;background:#f8fbff;min-width:250px}
42345
43977
  .ctx-live-dot{width:8px;height:8px;border-radius:50%;background:#13b8a6;box-shadow:0 0 0 rgba(19,184,166,.45)}
42346
43978
  .ctx-live-bar{position:relative;display:inline-block;width:84px;height:6px;border-radius:999px;background:#e5edf8;overflow:hidden}
42347
- .ctx-live-fill{display:block;height:100%;width:0%;background:linear-gradient(90deg,#13b8a6,#1f6feb);transition:width .24s ease,background .24s ease}
43979
+ .ctx-live-fill{position:relative;display:block;height:100%;width:0%;background:linear-gradient(90deg,#13b8a6,#1f6feb);transition:width .24s ease,background .24s ease}
43980
+ .ctx-live-fill.segmented{width:100%;background:#e5edf8;transition:background .24s ease}
43981
+ .ctx-live-fill.multi{width:100%;height:100%;background:transparent;transition:none}
43982
+ .ctx-live-agent-layer{position:absolute;left:0;right:0;top:50%;height:4px;border-radius:999px;transform:translateY(-50%);background:transparent;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ctx-agent-color,#4a5568) 42%, transparent);overflow:hidden;transition:height .24s ease}
43983
+ .ctx-live-agent-fill{position:absolute;left:0;top:0;bottom:0;width:0%;border-radius:999px;background:linear-gradient(90deg,color-mix(in srgb,var(--ctx-agent-color,#4a5568) 82%, #ffffff 18%),var(--ctx-agent-color,#4a5568));box-shadow:0 0 0 1px rgba(255,255,255,.22) inset;transition:width .24s ease}
42348
43984
  @media (max-width:760px){
42349
43985
  .composer-footer{flex-direction:column;align-items:stretch}
42350
43986
  .composer-file-btn{justify-content:center}
@@ -42352,8 +43988,12 @@ body[data-ui-style="trad"] .msg-event-cell{background:#fff}
42352
43988
  }
42353
43989
  .ctx-live.warn .ctx-live-dot{background:#e1a400}
42354
43990
  .ctx-live.warn .ctx-live-fill{background:linear-gradient(90deg,#ffcc66,#e1a400)}
43991
+ .ctx-live.warn .ctx-live-fill.segmented{background:#e5edf8}
43992
+ .ctx-live.warn .ctx-live-fill.multi{background:transparent}
42355
43993
  .ctx-live.danger .ctx-live-dot{background:#cf3b3b}
42356
43994
  .ctx-live.danger .ctx-live-fill{background:linear-gradient(90deg,#ff8a8a,#cf3b3b)}
43995
+ .ctx-live.danger .ctx-live-fill.segmented{background:#e5edf8}
43996
+ .ctx-live.danger .ctx-live-fill.multi{background:transparent}
42357
43997
  .ctx-live.danger{border-color:#f1c5c5;background:#fff4f4}
42358
43998
  .error-box{margin-top:8px;padding:8px 10px;border:1px solid #f2b4b4;background:#fff1f1;color:#8f1d1d;border-radius:8px}
42359
43999
  .hidden{display:none}
@@ -42366,6 +44006,18 @@ body[data-ui-style="trad"] .msg-event-cell{background:#fff}
42366
44006
  .runtime-pill-label{flex:0 0 auto;font-weight:700;color:#667b94;white-space:nowrap}
42367
44007
  .runtime-pill-value{min-width:0;color:#17283d;white-space:normal;overflow-wrap:anywhere;word-break:break-word}
42368
44008
  .runtime-pill-value.mono{font-size:.76rem}
44009
+ .agent-ctx-group{display:flex;flex-wrap:wrap;gap:6px;width:100%;margin-top:2px}
44010
+ .agent-ctx-chip{display:inline-flex;align-items:center;gap:6px;min-width:0;padding:5px 8px;border:1px solid #dbe5f2;border-radius:999px;background:#fff;font-size:.75rem;color:#24364b}
44011
+ .agent-ctx-chip.active{box-shadow:0 0 0 2px rgba(47,111,237,.12)}
44012
+ .agent-ctx-chip.warn{border-color:#f1c97a;background:#fff9ea}
44013
+ .agent-ctx-chip.danger{border-color:#ef9a9a;background:#fff1f1}
44014
+ .agent-ctx-chip .agent-dot{width:7px;height:7px;border-radius:50%;background:#718096;flex:0 0 auto}
44015
+ .agent-ctx-chip.role-explorer .agent-dot{background:#ff4d4f}
44016
+ .agent-ctx-chip.role-developer .agent-dot{background:#20c997}
44017
+ .agent-ctx-chip.role-reviewer .agent-dot{background:#f59f00}
44018
+ .agent-ctx-chip.role-manager .agent-dot{background:#9b5cff}
44019
+ .agent-ctx-chip.role-single .agent-dot{background:#64748b}
44020
+ .agent-ctx-chip .agent-ctx-value{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:#17283d;white-space:nowrap}
42369
44021
  .render-bridge{margin:0 0 10px;border:1px solid #d9e4f1;border-radius:10px;background:#fbfdff;overflow:hidden}
42370
44022
  .render-meta{padding:6px 8px;border-bottom:1px solid #e6edf7;color:#51627a;font-size:.76rem;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
42371
44023
  .render-canvas{display:block;width:100%;height:220px;background:#ffffff}
@@ -42434,6 +44086,7 @@ h3{font-size:.96rem;margin:10px 0 6px}
42434
44086
  .diff-item{margin-bottom:8px;padding:6px;border:1px solid #e7edf5;border-radius:8px;background:#fff;min-width:0}
42435
44087
  .diff-head{font-weight:600;margin-bottom:4px;overflow-wrap:anywhere;word-break:break-word}
42436
44088
  .diff-body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;white-space:pre;overflow:auto;max-height:220px;background:#f8fafc;border-radius:6px;padding:6px}
44089
+ .diff-body .diff-row{display:block;min-width:max-content;white-space:pre;tab-size:4}
42437
44090
  .diff-line-add{background:#eaffea;color:#0f6a1b}
42438
44091
  .diff-line-del{background:#ffeaea;color:#8a1d1d}
42439
44092
  .diff-line-hunk{background:#edf4ff;color:#1f4b8f}
@@ -42686,7 +44339,7 @@ const I18N={
42686
44339
  };
42687
44340
  Object.assign(I18N['en'],{
42688
44341
  sec_todos:'Todos',sec_tasks:'Tasks',sec_activity:'Activity',sec_commands:'Commands',sec_diffs:'File Diffs',sec_catalog:'Catalog',
42689
- role_explorer:'Explorer',role_developer:'Developer',role_reviewer:'Reviewer',role_manager:'Manager',role_planner:'Planner',role_agent:'Agent',
44342
+ role_explorer:'Explorer',role_developer:'Developer',role_reviewer:'Reviewer',role_manager:'Manager',role_planner:'Planner',role_agent:'Agent',role_single:'Context',
42690
44343
  callout_warning:'Warning',callout_notice:'Notice',callout_instruction:'Instruction',callout_tip:'Tip',callout_reminder:'Reminder',
42691
44344
  event_manager_delegate_title:'Manager Delegate',event_objective:'Objective',event_instruction:'Instruction',event_intent:'intent',
42692
44345
  event_tool_calls_title:'Tool Calls',event_tool_calls_note:'Model scheduled these tools for the current turn.',event_tool_calls_empty:'No structured tool metadata was attached to this turn.',
@@ -42700,7 +44353,7 @@ Object.assign(I18N['en'],{
42700
44353
  event_scheduler_queued_title:'Queued Task',event_scheduler_queued_note:'This message is saved and waiting for an execution slot.',event_scheduler_queue_position:'queue position',event_scheduler_reason:'reason',event_scheduler_queued_hint:'queued',
42701
44354
  event_auto_continue:'Auto Continue',event_arbiter_continue:'Arbiter Continue',event_continuation_briefing:'Continuation Briefing',event_reminder:'Reminder',event_todo_rescue:'Todo Rescue',event_tool_retry:'Tool Retry',event_segmented_retry:'Segmented Retry',event_forced_converge:'Forced Converge',event_no_tool_recovery:'No-Tool Recovery',event_context_recall:'Context Recall',event_failure_recovery:'Failure Recovery',event_truncate_rescue:'Truncation Rescue',event_thinking_recovery:'Thinking Recovery',event_fault_prefill:'Fault Prefill',event_edit_recovery:'Edit Recovery',
42702
44355
  state_on:'on',state_off:'off',
42703
- rt_session:'session',rt_model:'model',rt_thinking:'thinking',rt_thinking_stream:'thinking_stream',rt_mode:'mode',rt_active_agent:'active_agent',rt_blackboard:'bb',rt_task:'task',rt_complexity:'complexity',rt_judgement:'judgement',rt_budget:'budget',rt_remaining:'remaining',rt_blackboard_cycles:'bb_cycles',rt_round_limit:'round_limit',rt_round:'round',rt_phase:'phase',rt_queued_inputs:'queued_inputs',rt_run_timeout:'run_timeout',rt_ctx_used:'ctx_used',rt_ctx_limit:'ctx_limit',rt_ctx_mode:'ctx_mode',rt_manual_lock:'manual-lock',rt_adaptive:'adaptive',rt_ctx_left:'ctx_left',rt_truncation:'truncation',rt_trunc_retry:'trunc_retry',rt_trunc_tokens:'trunc_tokens~',rt_archive:'archive',rt_last_compact:'last_compact',rt_ollama:'ollama',rt_files:'files',rt_ui_mode:'ui_mode',rt_state:'state',
44356
+ rt_session:'session',rt_model:'model',rt_thinking:'thinking',rt_thinking_stream:'thinking_stream',rt_mode:'mode',rt_active_agent:'active_agent',rt_blackboard:'bb',rt_task:'task',rt_complexity:'complexity',rt_judgement:'judgement',rt_budget:'budget',rt_remaining:'remaining',rt_blackboard_cycles:'bb_cycles',rt_round_limit:'round_limit',rt_round:'round',rt_phase:'phase',rt_queued_inputs:'queued_inputs',rt_run_timeout:'run_timeout',rt_ctx_used:'ctx_used',rt_ctx_limit:'ctx_limit',rt_ctx_mode:'ctx_mode',rt_manual_lock:'manual-lock',rt_adaptive:'adaptive',rt_ctx_left:'ctx_left',rt_ctx_left_for:'{label} left',rt_ctx_live_title:'Remaining context budget by active call',rt_truncation:'truncation',rt_trunc_retry:'trunc_retry',rt_trunc_tokens:'trunc_tokens~',rt_archive:'archive',rt_last_compact:'last_compact',rt_ollama:'ollama',rt_files:'files',rt_ui_mode:'ui_mode',rt_state:'state',
42704
44357
  preview_download:'Download',preview_source:'Source',preview_rendered:'Preview',
42705
44358
  fe_nodes:'nodes={n}',fe_loading:'loading...',fe_tree_truncated:'tree truncated at {n} nodes',fe_items:'{n} item(s)',
42706
44359
  cmd_ui_preview_truncated:'UI preview truncated',cmd_model_context_truncated:'Model context truncated',cmd_temp_read_file_ready:'Temp read_file ready',cmd_buffered_copy:'Buffered copy',cmd_prev:'Prev',cmd_next:'Next',cmd_preview:'preview',cmd_of:'of',cmd_read_file_path:'read_file path',cmd_buffer_ref:'buffer_ref',cmd_chars:'chars',cmd_lines:'lines',cmd_strategy:'strategy',cmd_full_output:'full_output',cmd_exit:'exit',cmd_default_name:'command'
@@ -42708,7 +44361,7 @@ Object.assign(I18N['en'],{
42708
44361
  Object.assign(I18N['zh-CN'],{
42709
44362
  sec_todos:'待办',sec_tasks:'任务',sec_activity:'活动',sec_commands:'命令',sec_diffs:'文件差异',sec_catalog:'目录',
42710
44363
  no_todos:'暂无待办',no_tasks:'暂无任务',no_catalog:'暂无目录',
42711
- role_explorer:'探索者',role_developer:'开发者',role_reviewer:'审查者',role_manager:'管理者',role_planner:'规划者',role_agent:'Agent',
44364
+ role_explorer:'探索者',role_developer:'开发者',role_reviewer:'审查者',role_manager:'管理者',role_planner:'规划者',role_agent:'Agent',role_single:'上下文',
42712
44365
  callout_warning:'警告',callout_notice:'提示',callout_instruction:'指令',callout_tip:'建议',callout_reminder:'提醒',
42713
44366
  event_manager_delegate_title:'管理者委派',event_objective:'目标',event_instruction:'指令',event_intent:'意图',
42714
44367
  event_tool_calls_title:'工具调用',event_tool_calls_note:'模型已为当前轮安排以下工具调用。',event_tool_calls_empty:'当前轮没有附带结构化工具元数据。',
@@ -42722,7 +44375,7 @@ Object.assign(I18N['zh-CN'],{
42722
44375
  event_scheduler_queued_title:'任务已排队',event_scheduler_queued_note:'这条消息已保存,正在等待后台执行名额。',event_scheduler_queue_position:'队列位置',event_scheduler_reason:'原因',event_scheduler_queued_hint:'已排队',
42723
44376
  event_auto_continue:'自动继续',event_arbiter_continue:'裁决继续',event_continuation_briefing:'续跑简报',event_reminder:'提醒',event_todo_rescue:'待办救援',event_tool_retry:'工具重试',event_segmented_retry:'分段重试',event_forced_converge:'强制收敛',event_no_tool_recovery:'无工具恢复',event_context_recall:'上下文召回',event_failure_recovery:'故障恢复',event_truncate_rescue:'截断救援',event_thinking_recovery:'思考恢复',event_fault_prefill:'故障预填',event_edit_recovery:'编辑恢复',
42724
44377
  state_on:'开',state_off:'关',
42725
- rt_session:'会话',rt_model:'模型',rt_thinking:'思考',rt_thinking_stream:'思考流',rt_mode:'模式',rt_active_agent:'活跃代理',rt_blackboard:'黑板',rt_task:'任务',rt_complexity:'复杂度',rt_judgement:'裁决',rt_budget:'预算',rt_remaining:'剩余',rt_blackboard_cycles:'黑板轮次',rt_round_limit:'轮次上限',rt_round:'轮次',rt_phase:'阶段',rt_queued_inputs:'排队输入',rt_run_timeout:'运行超时',rt_ctx_used:'上下文已用',rt_ctx_limit:'上下文上限',rt_ctx_mode:'上下文模式',rt_manual_lock:'手动锁定',rt_adaptive:'自适应',rt_ctx_left:'上下文剩余',rt_truncation:'截断数',rt_trunc_retry:'截断重试',rt_trunc_tokens:'截断Token~',rt_archive:'归档',rt_last_compact:'最近压缩',rt_ollama:'Ollama',rt_files:'文件根目录',rt_ui_mode:'界面模式',rt_state:'状态',
44378
+ rt_session:'会话',rt_model:'模型',rt_thinking:'思考',rt_thinking_stream:'思考流',rt_mode:'模式',rt_active_agent:'活跃代理',rt_blackboard:'黑板',rt_task:'任务',rt_complexity:'复杂度',rt_judgement:'裁决',rt_budget:'预算',rt_remaining:'剩余',rt_blackboard_cycles:'黑板轮次',rt_round_limit:'轮次上限',rt_round:'轮次',rt_phase:'阶段',rt_queued_inputs:'排队输入',rt_run_timeout:'运行超时',rt_ctx_used:'上下文已用',rt_ctx_limit:'上下文上限',rt_ctx_mode:'上下文模式',rt_manual_lock:'手动锁定',rt_adaptive:'自适应',rt_ctx_left:'上下文剩余',rt_ctx_left_for:'{label}剩余',rt_ctx_live_title:'按真实调用显示上下文剩余',rt_truncation:'截断数',rt_trunc_retry:'截断重试',rt_trunc_tokens:'截断Token~',rt_archive:'归档',rt_last_compact:'最近压缩',rt_ollama:'Ollama',rt_files:'文件根目录',rt_ui_mode:'界面模式',rt_state:'状态',
42726
44379
  preview_download:'下载',preview_source:'源码',preview_rendered:'预览',
42727
44380
  fe_nodes:'节点={n}',fe_loading:'加载中...',fe_tree_truncated:'目录树在 {n} 个节点处被截断',fe_items:'{n} 项',
42728
44381
  cmd_ui_preview_truncated:'UI 预览截断',cmd_model_context_truncated:'模型上下文截断',cmd_temp_read_file_ready:'临时 read_file 已就绪',cmd_buffered_copy:'缓冲副本',cmd_prev:'上一页',cmd_next:'下一页',cmd_preview:'预览',cmd_of:'共',cmd_read_file_path:'read_file 路径',cmd_buffer_ref:'缓冲引用',cmd_chars:'字符',cmd_lines:'行',cmd_strategy:'策略',cmd_full_output:'完整输出',cmd_exit:'退出码',cmd_default_name:'命令'
@@ -42733,7 +44386,7 @@ Object.assign(I18N['zh-TW'],{
42733
44386
  sec_todos:'待辦',sec_tasks:'任務',sec_activity:'活動',sec_commands:'命令',sec_diffs:'檔案差異',sec_catalog:'目錄',
42734
44387
  no_todos:'尚無待辦',no_tasks:'尚無任務',no_catalog:'尚無目錄',
42735
44388
  level_3_collab:'L3 協作',
42736
- role_explorer:'探索者',role_developer:'開發者',role_reviewer:'審查者',role_manager:'管理者',role_planner:'規劃者',role_agent:'Agent',
44389
+ role_explorer:'探索者',role_developer:'開發者',role_reviewer:'審查者',role_manager:'管理者',role_planner:'規劃者',role_agent:'Agent',role_single:'上下文',
42737
44390
  callout_warning:'警告',callout_notice:'提示',callout_instruction:'指令',callout_tip:'建議',callout_reminder:'提醒',
42738
44391
  event_manager_delegate_title:'管理者委派',event_objective:'目標',event_instruction:'指令',event_intent:'意圖',
42739
44392
  event_tool_calls_title:'工具呼叫',event_tool_calls_note:'模型已為目前輪安排以下工具呼叫。',event_tool_calls_empty:'目前輪沒有附帶結構化工具中繼資料。',
@@ -42747,7 +44400,7 @@ Object.assign(I18N['zh-TW'],{
42747
44400
  event_scheduler_queued_title:'任務已排隊',event_scheduler_queued_note:'這則訊息已保存,正在等待背景執行名額。',event_scheduler_queue_position:'佇列位置',event_scheduler_reason:'原因',event_scheduler_queued_hint:'已排隊',
42748
44401
  event_auto_continue:'自動繼續',event_arbiter_continue:'裁決繼續',event_continuation_briefing:'續跑簡報',event_reminder:'提醒',event_todo_rescue:'待辦救援',event_tool_retry:'工具重試',event_segmented_retry:'分段重試',event_forced_converge:'強制收斂',event_no_tool_recovery:'無工具恢復',event_context_recall:'上下文召回',event_failure_recovery:'故障恢復',event_truncate_rescue:'截斷救援',event_thinking_recovery:'思考恢復',event_fault_prefill:'故障預填',event_edit_recovery:'編輯恢復',
42749
44402
  state_on:'開',state_off:'關',
42750
- rt_session:'會話',rt_model:'模型',rt_thinking:'思考',rt_thinking_stream:'思考流',rt_mode:'模式',rt_active_agent:'活躍代理',rt_blackboard:'黑板',rt_task:'任務',rt_complexity:'複雜度',rt_judgement:'裁決',rt_budget:'預算',rt_remaining:'剩餘',rt_blackboard_cycles:'黑板輪次',rt_round_limit:'輪次上限',rt_round:'輪次',rt_phase:'階段',rt_queued_inputs:'排隊輸入',rt_run_timeout:'執行逾時',rt_ctx_used:'上下文已用',rt_ctx_limit:'上下文上限',rt_ctx_mode:'上下文模式',rt_manual_lock:'手動鎖定',rt_adaptive:'自適應',rt_ctx_left:'上下文剩餘',rt_truncation:'截斷數',rt_trunc_retry:'截斷重試',rt_trunc_tokens:'截斷Token~',rt_archive:'封存',rt_last_compact:'最近壓縮',rt_ollama:'Ollama',rt_files:'檔案根目錄',rt_ui_mode:'介面模式',rt_state:'狀態',
44403
+ rt_session:'會話',rt_model:'模型',rt_thinking:'思考',rt_thinking_stream:'思考流',rt_mode:'模式',rt_active_agent:'活躍代理',rt_blackboard:'黑板',rt_task:'任務',rt_complexity:'複雜度',rt_judgement:'裁決',rt_budget:'預算',rt_remaining:'剩餘',rt_blackboard_cycles:'黑板輪次',rt_round_limit:'輪次上限',rt_round:'輪次',rt_phase:'階段',rt_queued_inputs:'排隊輸入',rt_run_timeout:'執行逾時',rt_ctx_used:'上下文已用',rt_ctx_limit:'上下文上限',rt_ctx_mode:'上下文模式',rt_manual_lock:'手動鎖定',rt_adaptive:'自適應',rt_ctx_left:'上下文剩餘',rt_ctx_left_for:'{label}剩餘',rt_ctx_live_title:'依真實呼叫顯示上下文剩餘',rt_truncation:'截斷數',rt_trunc_retry:'截斷重試',rt_trunc_tokens:'截斷Token~',rt_archive:'封存',rt_last_compact:'最近壓縮',rt_ollama:'Ollama',rt_files:'檔案根目錄',rt_ui_mode:'介面模式',rt_state:'狀態',
42751
44404
  preview_download:'下載',preview_source:'原始碼',preview_rendered:'預覽',
42752
44405
  fe_nodes:'節點={n}',fe_loading:'載入中...',fe_tree_truncated:'目錄樹在 {n} 個節點處被截斷',fe_items:'{n} 項',
42753
44406
  cmd_ui_preview_truncated:'UI 預覽截斷',cmd_model_context_truncated:'模型上下文截斷',cmd_temp_read_file_ready:'暫存 read_file 已就緒',cmd_buffered_copy:'緩衝副本',cmd_prev:'上一頁',cmd_next:'下一頁',cmd_preview:'預覽',cmd_of:'共',cmd_read_file_path:'read_file 路徑',cmd_buffer_ref:'緩衝引用',cmd_chars:'字元',cmd_lines:'行',cmd_strategy:'策略',cmd_full_output:'完整輸出',cmd_exit:'退出碼',cmd_default_name:'命令'
@@ -42756,7 +44409,7 @@ Object.assign(I18N['ja'],{
42756
44409
  sec_todos:'Todo',sec_tasks:'タスク',sec_activity:'アクティビティ',sec_commands:'コマンド',sec_diffs:'ファイル差分',sec_catalog:'カタログ',
42757
44410
  thinking:'思考',thinking_stream:'思考(ストリーム)',copy_code:'コードをコピー',copy_done:'コピーしました',
42758
44411
  no_todos:'Todo はありません',no_tasks:'タスクはありません',no_catalog:'カタログなし',
42759
- role_explorer:'探索担当',role_developer:'開発担当',role_reviewer:'レビュー担当',role_manager:'マネージャー',role_planner:'プランナー',role_agent:'Agent',
44412
+ role_explorer:'探索担当',role_developer:'開発担当',role_reviewer:'レビュー担当',role_manager:'マネージャー',role_planner:'プランナー',role_agent:'Agent',role_single:'コンテキスト',
42760
44413
  callout_warning:'警告',callout_notice:'通知',callout_instruction:'指示',callout_tip:'ヒント',callout_reminder:'リマインダー',
42761
44414
  event_manager_delegate_title:'マネージャー委任',event_objective:'目的',event_instruction:'指示',event_intent:'意図',
42762
44415
  event_tool_calls_title:'ツール呼び出し',event_tool_calls_note:'モデルはこのターンで次のツール呼び出しを予定しました。',event_tool_calls_empty:'このターンには構造化されたツールメタデータがありません。',
@@ -42770,7 +44423,7 @@ Object.assign(I18N['ja'],{
42770
44423
  event_scheduler_queued_title:'キュー済みタスク',event_scheduler_queued_note:'このメッセージは保存され、実行枠を待っています。',event_scheduler_queue_position:'キュー位置',event_scheduler_reason:'理由',event_scheduler_queued_hint:'キュー済み',
42771
44424
  event_auto_continue:'自動継続',event_arbiter_continue:'判定継続',event_continuation_briefing:'継続ブリーフ',event_reminder:'リマインダー',event_todo_rescue:'Todo 救援',event_tool_retry:'ツール再試行',event_segmented_retry:'分割再試行',event_forced_converge:'強制収束',event_no_tool_recovery:'ツールなし復旧',event_context_recall:'コンテキスト再呼び出し',event_failure_recovery:'障害復旧',event_truncate_rescue:'切り詰め救援',event_thinking_recovery:'思考復旧',event_fault_prefill:'障害プリフィル',event_edit_recovery:'編集復旧',
42772
44425
  state_on:'オン',state_off:'オフ',
42773
- rt_session:'セッション',rt_model:'モデル',rt_thinking:'思考',rt_thinking_stream:'思考ストリーム',rt_mode:'モード',rt_active_agent:'アクティブAgent',rt_blackboard:'黒板',rt_task:'タスク',rt_complexity:'複雑度',rt_judgement:'判定',rt_budget:'予算',rt_remaining:'残り',rt_blackboard_cycles:'黒板サイクル',rt_round_limit:'ラウンド上限',rt_round:'ラウンド',rt_phase:'フェーズ',rt_queued_inputs:'待機入力',rt_run_timeout:'実行タイムアウト',rt_ctx_used:'コンテキスト使用量',rt_ctx_limit:'コンテキスト上限',rt_ctx_mode:'コンテキストモード',rt_manual_lock:'手動固定',rt_adaptive:'適応',rt_ctx_left:'残りコンテキスト',rt_truncation:'切り詰め数',rt_trunc_retry:'切り詰め再試行',rt_trunc_tokens:'切り詰めToken~',rt_archive:'アーカイブ',rt_last_compact:'直近 compact',rt_ollama:'Ollama',rt_files:'ファイルルート',rt_ui_mode:'UIモード',rt_state:'状態',
44426
+ rt_session:'セッション',rt_model:'モデル',rt_thinking:'思考',rt_thinking_stream:'思考ストリーム',rt_mode:'モード',rt_active_agent:'アクティブAgent',rt_blackboard:'黒板',rt_task:'タスク',rt_complexity:'複雑度',rt_judgement:'判定',rt_budget:'予算',rt_remaining:'残り',rt_blackboard_cycles:'黒板サイクル',rt_round_limit:'ラウンド上限',rt_round:'ラウンド',rt_phase:'フェーズ',rt_queued_inputs:'待機入力',rt_run_timeout:'実行タイムアウト',rt_ctx_used:'コンテキスト使用量',rt_ctx_limit:'コンテキスト上限',rt_ctx_mode:'コンテキストモード',rt_manual_lock:'手動固定',rt_adaptive:'適応',rt_ctx_left:'残りコンテキスト',rt_ctx_left_for:'{label}残り',rt_ctx_live_title:'実際の呼び出し別の残りコンテキスト',rt_truncation:'切り詰め数',rt_trunc_retry:'切り詰め再試行',rt_trunc_tokens:'切り詰めToken~',rt_archive:'アーカイブ',rt_last_compact:'直近 compact',rt_ollama:'Ollama',rt_files:'ファイルルート',rt_ui_mode:'UIモード',rt_state:'状態',
42774
44427
  preview_download:'ダウンロード',preview_source:'ソース',preview_rendered:'プレビュー',
42775
44428
  fe_nodes:'ノード={n}',fe_loading:'読み込み中...',fe_tree_truncated:'ツリーは {n} ノードで切り詰められました',fe_items:'{n} 件',
42776
44429
  cmd_ui_preview_truncated:'UI プレビュー切り詰め',cmd_model_context_truncated:'モデルコンテキスト切り詰め',cmd_temp_read_file_ready:'一時 read_file 準備完了',cmd_buffered_copy:'バッファコピー',cmd_prev:'前へ',cmd_next:'次へ',cmd_preview:'プレビュー',cmd_of:'全',cmd_read_file_path:'read_file パス',cmd_buffer_ref:'buffer_ref',cmd_chars:'文字',cmd_lines:'行',cmd_strategy:'戦略',cmd_full_output:'完全出力',cmd_exit:'終了コード',cmd_default_name:'コマンド'
@@ -42781,7 +44434,7 @@ function applyUiStyle(){const style=normalizeUiStyle(S.config?.ui_style||'neo');
42781
44434
  function t(key,vars){const lang=currentLang();const pack=I18N[lang]||I18N['en'];const fallback=I18N['en'];let txt=String((pack&&pack[key])??(fallback&&fallback[key])??key);if(vars&&typeof vars==='object'){for(const [k,v] of Object.entries(vars)){txt=txt.replaceAll('{'+k+'}',String(v??''))}}return txt}
42782
44435
  function setText(id,key){const el=E(id);if(el)el.textContent=t(key)}
42783
44436
  function setPlaceholder(id,key){const el=E(id);if(el)el.placeholder=t(key)}
42784
- function applyMainI18n(){document.documentElement.lang=currentLang();const h1=document.querySelector('header h1');if(h1)h1.textContent=t('app_title');const hp=document.querySelectorAll('header p');if(hp&&hp[0])hp[0].textContent=t('app_subtitle');if(hp&&hp[1])hp[1].textContent=t('powered_by');setText('applyModelBtn','apply_model');setText('llmConfigBtn','upload_llm_config');setText('llmModalTitle','llm_fill_config');setText('llmProviderLabel','llm_provider');setText('llmConfigConfirm','llm_confirm');setText('llmConfigImport','llm_import_config');setText('newSessionBtn','btn_new_session');setText('renameSessionBtn','btn_rename');setText('deleteSessionBtn','btn_delete');setText('sendBtn','btn_send');setText('interruptBtn','btn_interrupt');setText('toolsMenuBtn','btn_tools');setText('compactAction','btn_compact_action');setText('refreshAction','btn_refresh_action');setText('previewReloadBtn','btn_refresh');setText('previewCopyBtn','copy_code');setText('downloadSessionBtn','btn_export_session');setText('clearStaleTodosBtn','btn_clear_stale_todos');setText('refreshFilesBtn','btn_refresh');setPlaceholder('prompt','prompt_placeholder');const up=E('uploadDrop');if(up)up.textContent=t('upload_drop');const pfht=E('promptFileHintText');if(pfht)pfht.textContent=t('upload_file_hint');const pfpk=E('promptFilePick');if(pfpk)pfpk.textContent=t('upload_pick_file');const pdol=E('promptDropOverlay');if(pdol)pdol.textContent=t('upload_drop_release');const panels=document.querySelectorAll('.panel-title');if(panels&&panels[0])panels[0].textContent=t('panel_sessions');if(panels&&panels[1])panels[1].textContent=t('panel_conversation');if(panels&&panels[2])panels[2].textContent=t('panel_runtime');const hs=document.querySelectorAll('#runtimeScroll h3');const keys=['sec_todos','sec_tasks','sec_activity','sec_commands','sec_diffs','sec_files','sec_catalog'];for(let i=0;i<hs.length&&i<keys.length;i++){hs[i].textContent=t(keys[i])}const _lvl2=S.snap?.user_task_level||0;updateLevelBtn(_lvl2);renderPreviewTabs()}
44437
+ function applyMainI18n(){document.documentElement.lang=currentLang();const h1=document.querySelector('header h1');if(h1)h1.textContent=t('app_title');const hp=document.querySelectorAll('header p');if(hp&&hp[0])hp[0].textContent=t('app_subtitle');if(hp&&hp[1])hp[1].textContent=t('powered_by');setText('applyModelBtn','apply_model');setText('llmConfigBtn','upload_llm_config');setText('llmModalTitle','llm_fill_config');setText('llmProviderLabel','llm_provider');setText('llmConfigConfirm','llm_confirm');setText('llmConfigImport','llm_import_config');setText('newSessionBtn','btn_new_session');setText('renameSessionBtn','btn_rename');setText('deleteSessionBtn','btn_delete');setText('sendBtn','btn_send');setText('interruptBtn','btn_interrupt');setText('toolsMenuBtn','btn_tools');setText('compactAction','btn_compact_action');setText('refreshAction','btn_refresh_action');setText('previewReloadBtn','btn_refresh');setText('previewCopyBtn','copy_code');setText('downloadSessionBtn','btn_export_session');setText('clearStaleTodosBtn','btn_clear_stale_todos');setText('refreshFilesBtn','btn_refresh');setPlaceholder('prompt','prompt_placeholder');const up=E('uploadDrop');if(up)up.textContent=t('upload_drop');const pfht=E('promptFileHintText');if(pfht)pfht.textContent=t('upload_file_hint');const pfpk=E('promptFilePick');if(pfpk)pfpk.textContent=t('upload_pick_file');const pdol=E('promptDropOverlay');if(pdol)pdol.textContent=t('upload_drop_release');const ctxLive=E('ctxLive');if(ctxLive)ctxLive.setAttribute('title',t('rt_ctx_live_title'));const panels=document.querySelectorAll('.panel-title');if(panels&&panels[0])panels[0].textContent=t('panel_sessions');if(panels&&panels[1])panels[1].textContent=t('panel_conversation');if(panels&&panels[2])panels[2].textContent=t('panel_runtime');const hs=document.querySelectorAll('#runtimeScroll h3');const keys=['sec_todos','sec_tasks','sec_activity','sec_commands','sec_diffs','sec_files','sec_catalog'];for(let i=0;i<hs.length&&i<keys.length;i++){hs[i].textContent=t(keys[i])}const _lvl2=S.snap?.user_task_level||0;updateLevelBtn(_lvl2);renderPreviewTabs()}
42785
44438
  function renderLanguageControls(){const sel=E('langSelect');if(!sel)return;const langs=Array.isArray(S.config?.supported_languages)?S.config.supported_languages:[];if(!langs.length){sel.innerHTML='';return}const cur=String(S.config?.language||currentLang());sel.innerHTML='';for(const row of langs){const code=String(row?.code||'').trim();if(!code)continue;const op=document.createElement('option');op.value=code;op.textContent=String(row?.label||code);sel.appendChild(op)}if(cur)sel.value=cur}
42786
44439
  async function setLanguage(lang){const code=String(lang||'').trim();if(!code)return;await api('/api/config/language',{method:'POST',body:JSON.stringify({language:code})});S.config=S.config||{};S.config.language=code;if(S.snap)S.snap.ui_language=code;if(S.mdWorker){try{S.mdWorker.terminate()}catch(_){}S.mdWorker=null}applyMainI18n();renderLanguageControls();renderStats();renderSessions();renderBoards();renderUploadList();scheduleRenderChat('language');renderSkillsEntryLink()}
42787
44440
  function globalApiTimeoutMs(){const vals=[S.snap?.max_run_seconds,S.config?.request_timeout_default,S.config?.run_timeout];for(const raw of vals){const n=Number(raw);if(Number.isFinite(n)&&n>0)return Math.max(1000,Math.min(86400000,Math.round(n*1000)))}return 45000}
@@ -42838,8 +44491,13 @@ function setPanelHtml(id,html){
42838
44491
  }
42839
44492
  }
42840
44493
  function formatContextLeft(snap){const left=Number(snap?.context_left_tokens);const pct=Number(snap?.context_left_percent);if(!Number.isFinite(left)||!Number.isFinite(pct))return '-';return `${left} (${pct.toFixed(1)}%)`}
44494
+ function contextLiveRows(snap){const mode=String(snap?.execution_mode||'').trim().toLowerCase();const showAgents=(mode==='sync'||mode==='sequential');const rows=showAgents&&Array.isArray(snap?.agent_contexts)?snap.agent_contexts.slice():[];const valid=rows.filter(r=>Number.isFinite(Number(r?.left))&&Number.isFinite(Number(r?.left_percent))).sort((a,b)=>(Number(a.left_percent)-Number(b.left_percent))||(Number(a.left)-Number(b.left)));if(valid.length>1)return valid;const left=Number(snap?.context_left_tokens);const pct=Number(snap?.context_left_percent);if(Number.isFinite(left)&&Number.isFinite(pct))return[{role:'single',label:t('role_single'),left,left_percent:pct,tier:0,active:true}];if(valid.length===1)return valid;return[]}
44495
+ function agentCtxColor(role){const r=String(role||'').trim().toLowerCase();if(r==='explorer')return'#ff4d4f';if(r==='developer')return'#20c997';if(r==='reviewer')return'#f59f00';if(r==='manager')return'#9b5cff';return'#64748b'}
44496
+ function agentContextLabel(row){const role=String(row?.role||'single').trim().toLowerCase();const roleKey=_chatVirtAgentRoleKey(role);if(roleKey)return _chatVirtAgentRoleLabel(roleKey);const raw=String(row?.label||'').trim();if(!raw||raw.toLowerCase()==='single')return t('role_single');return raw}
44497
+ function contextLiveNestedHtml(rows){const valid=(Array.isArray(rows)?rows:[]).filter(r=>Number.isFinite(Number(r?.left_percent))).slice(0,6);if(valid.length<=1)return'';const maxH=6;return valid.map((row,i)=>{const pct=Math.max(0,Math.min(100,Number(row?.left_percent)));const left=Number(row?.left);const tier=Number(row?.tier||0);const role=String(row?.role||'single').trim().toLowerCase();const roleKey=_chatVirtAgentRoleKey(role)||role||'single';const label=agentContextLabel(row);const color=agentCtxColor(roleKey);const h=Math.max(1.5,maxH-(i*1.2));const z=valid.length-i;const title=`${t('rt_ctx_left_for',{label})}=${Number.isFinite(left)?left:'-'} (${pct.toFixed(1)}%) · T${tier}`;return `<span class=\"ctx-live-agent-layer role-${esc(roleKey)}\" style=\"--ctx-agent-color:${esc(color)};height:${h.toFixed(1)}px;z-index:${z}\" title=\"${esc(title)}\"><span class=\"ctx-live-agent-fill\" style=\"width:${pct.toFixed(2)}%\"></span></span>`}).join('')}
44498
+ function agentContextChipsHtml(snap){const mode=String(snap?.execution_mode||'').trim().toLowerCase();if(mode!=='sync'&&mode!=='sequential')return'';const rows=Array.isArray(snap?.agent_contexts)?snap.agent_contexts:[];if(rows.length<=1)return'';return `<span class=\"agent-ctx-group\">${rows.slice(0,6).map(row=>{const role=String(row?.role||'single').trim().toLowerCase();const roleKey=_chatVirtAgentRoleKey(role)||role||'single';const label=agentContextLabel(row);const left=Number(row?.left);const pct=Number(row?.left_percent);const tier=Number(row?.tier||0);const safePct=Number.isFinite(pct)?Math.max(0,Math.min(100,pct)):0;const value=Number.isFinite(left)&&Number.isFinite(pct)?`${left} · ${safePct.toFixed(1)}% · T${tier}`:'-';const tone=safePct<=15?' danger':(safePct<=35?' warn':'');const active=row?.active?' active':'';return `<span class=\"agent-ctx-chip role-${esc(roleKey)}${tone}${active}\" title=\"${esc(String(row?.next_call_label||''))}\"><span class=\"agent-dot\"></span><span>${esc(label)}</span><span class=\"agent-ctx-value\">${esc(value)}</span></span>`}).join('')}</span>`}
42841
44499
  function scheduleCompactRefreshBurst(count=COMPACT_AUTO_REFRESH_COUNT){if(!S.activeId)return;const n=Math.max(1,Math.min(10,Number(count)||COMPACT_AUTO_REFRESH_COUNT));const delay=Math.max(90,Math.min(1400,90+((n-1)*COMPACT_AUTO_REFRESH_INTERVAL_MS)));scheduleSnapshot({forceFull:false,delayMs:delay,allowWhenFrozen:true})}
42842
- function renderCtxLive(snap){const box=E('ctxLive');const textEl=E('ctxLiveText');const fill=E('ctxLiveFill');if(!box||!textEl||!fill)return;const left=Number(snap?.context_left_tokens);const pct=Number(snap?.context_left_percent);if(!Number.isFinite(left)||!Number.isFinite(pct)){textEl.textContent=`${t('rt_ctx_left')}=-`;fill.style.width='0%';box.classList.remove('warn','danger');return}const safePct=Math.max(0,Math.min(100,pct));textEl.textContent=`${t('rt_ctx_left')}=${left} (${safePct.toFixed(1)}%)`;fill.style.width=`${safePct}%`;box.classList.toggle('warn',safePct<=35&&safePct>15);box.classList.toggle('danger',safePct<=15)}
44500
+ function renderCtxLive(snap){const box=E('ctxLive');const textEl=E('ctxLiveText');const fill=E('ctxLiveFill');if(!box||!textEl||!fill)return;const rows=contextLiveRows(snap);const tight=rows[0]||null;const left=Number(tight?.left);const pct=Number(tight?.left_percent);if(!tight||!Number.isFinite(left)||!Number.isFinite(pct)){textEl.textContent=`${t('rt_ctx_left')}=-`;fill.innerHTML='';fill.style.width='0%';fill.style.background='';fill.classList.remove('segmented','multi');box.classList.remove('warn','danger','multi');return}const safePct=Math.max(0,Math.min(100,pct));if(rows.length>1){const label=agentContextLabel(tight);textEl.textContent=`${t('rt_ctx_left_for',{label})}=${left} (${safePct.toFixed(1)}%)`;box.classList.add('multi');fill.classList.remove('segmented');fill.classList.add('multi');fill.style.width='100%';fill.style.background='';fill.innerHTML=contextLiveNestedHtml(rows);const titleRows=rows.slice(0,6).map(row=>{const rowPct=Math.max(0,Math.min(100,Number(row?.left_percent)));const rowLeft=Number(row?.left);const rowLabel=agentContextLabel(row);return `${t('rt_ctx_left_for',{label:rowLabel})}=${Number.isFinite(rowLeft)?rowLeft:'-'} (${Number.isFinite(rowPct)?rowPct.toFixed(1):'-'}%)`}).join(' | ');if(titleRows)fill.setAttribute('title',titleRows)}else{textEl.textContent=`${t('rt_ctx_left')}=${left} (${safePct.toFixed(1)}%)`;box.classList.remove('multi');fill.innerHTML='';fill.classList.remove('segmented','multi');fill.style.width=`${safePct}%`;fill.style.background='';fill.removeAttribute('title')}box.classList.toggle('warn',safePct<=35&&safePct>15);box.classList.toggle('danger',safePct<=15)}
42843
44501
  function showCompactToast(text){let el=document.querySelector('.compact-toast');if(!el){el=document.createElement('div');el.className='compact-toast';document.body.appendChild(el)}el.textContent=text;el.classList.add('show');if(el._t)clearTimeout(el._t);el._t=setTimeout(()=>el.classList.remove('show'),2800)}
42844
44502
  function parseCompactReason(data){const direct=String(data?.reason||'').trim();if(direct)return direct;const s=String(data?.summary||'');const m=s.match(/context compacted \\(([^)]*)\\)/);return m?String(m[1]||'').trim():''}
42845
44503
  function isRenderRuntimeEventType(evtType){return RENDER_EVT_TYPES.has(String(evtType||''))}
@@ -43176,7 +44834,7 @@ function _deltaStartWatchdog(){
43176
44834
  function renderSkillsEntryLink(){const link=E('downloadBtn');if(!link)return;const host=location.hostname||'127.0.0.1';const enabled=Boolean(S.config?.skills_ui_enabled);const fromConfig=String(S.config?.skills_ui_url||'').trim();const skillsPort=Number(S.config?.skills_port||0);let href='#';if(enabled){if(fromConfig){href=fromConfig}else if(Number.isFinite(skillsPort)&&skillsPort>0){const currentPort=Number(location.port||0);if(!(currentPort&&skillsPort===currentPort)){href=`${location.protocol}//${host}:${skillsPort}`}}}const offline=(href==='#');link.href=href;link.classList.toggle('disabled',offline);link.textContent=offline?t('skills_offline'):t('open_skills')}
43177
44835
  function tailSig(rows,count,mapper){const arr=Array.isArray(rows)?rows:[];if(!arr.length)return'';return arr.slice(Math.max(0,arr.length-count)).map(mapper).join('|')}
43178
44836
  function feedSignature(snap){const feed=Array.isArray(snap?.conversation_feed)?snap.conversation_feed:(Array.isArray(snap?.messages)?snap.messages:[]);const sig=tailSig(feed,8,row=>`${Number(row?.ts||0)}:${String(row?.role||'')}:${String(row?.agent_role||'')}:${String(row?.type||'')}:${String(row?.text||'').length}:${String(row?.thinking||'').length}:${String(row?.text||'').slice(-12)}:${String(row?.thinking||'').slice(-12)}`);const live=String(snap?.live_thinking||'');const runActive=snap?.live_run_notice_active?1:0;const runLabel=String(snap?.live_run_notice_label||'');const runStart=Number(snap?.live_run_notice_started_at||0);const truncText=String(snap?.live_truncation_text||'');const truncKind=String(snap?.live_truncation_kind||'');const truncTool=String(snap?.live_truncation_tool||'');const truncAttempts=Number(snap?.live_truncation_attempts||0);const truncTokens=Number(snap?.live_truncation_tokens||0);const truncActive=snap?.live_truncation_active?1:0;return `${feed.length}|${sig}|lt=${live.length}:${live.slice(-12)}|rn=${runActive}:${runStart}:${runLabel.slice(-12)}|tr=${truncActive}:${truncAttempts}:${truncTokens}:${truncKind.slice(-12)}:${truncTool.slice(-12)}:${truncText.length}`}
43179
- function boardsSignature(snap){return [snap?.running?1:0,snap?.agent_phase||'',Number(snap?.agent_round_index||0),Number(snap?.queued_user_inputs_count||0),Number(snap?.truncation_count||0),Number(snap?.live_truncation_attempts||0),Number(snap?.live_truncation_tokens||0),snap?.live_truncation_active?1:0,Number(snap?.context_tokens_estimate||0),Number(snap?.context_left_tokens||0),Number(snap?.context_left_percent||0),Number(snap?.render_bridge?.seq||0),(snap?.todos||[]).length,(snap?.tasks||[]).length,(snap?.activity||[]).length,(snap?.operations||[]).length,(snap?.uploads||[]).length].join('|')}
44837
+ function boardsSignature(snap){const agentCtx=(Array.isArray(snap?.agent_contexts)?snap.agent_contexts:[]).map(r=>`${r.role}:${r.left}:${r.left_percent}:${r.tier}:${r.active?1:0}`).join(',');return [snap?.running?1:0,snap?.agent_phase||'',Number(snap?.agent_round_index||0),Number(snap?.queued_user_inputs_count||0),Number(snap?.truncation_count||0),Number(snap?.live_truncation_attempts||0),Number(snap?.live_truncation_tokens||0),snap?.live_truncation_active?1:0,Number(snap?.context_tokens_estimate||0),Number(snap?.context_left_tokens||0),Number(snap?.context_left_percent||0),agentCtx,Number(snap?.render_bridge?.seq||0),(snap?.todos||[]).length,(snap?.tasks||[]).length,(snap?.activity||[]).length,(snap?.operations||[]).length,(snap?.uploads||[]).length].join('|')}
43180
44838
  function sessionsSignature(list){const rows=Array.isArray(list)?list:[];const sig=tailSig(rows,6,row=>`${String(row?.id||'')}:${row?.running?1:0}:${Number(row?.message_count||0)}:${Number(row?.updated_at||0)}`);const aid=String(S.activeId||'').trim();let activeSig='-';if(aid){const activeRow=rows.find(row=>String(row?.id||'')===aid);if(activeRow){activeSig=`${aid}:${activeRow?.running?1:0}:${Number(activeRow?.message_count||0)}:${Number(activeRow?.updated_at||0)}`}else{activeSig=`missing:${aid}`}}return `${rows.length}|active=${activeSig}|${sig}`}
43181
44839
  function _statInfinite(n){const v=Number(n);return(Number.isFinite(v)&&v>0)?String(v):'∞'}
43182
44840
  function applyRuntimeConfigStats(cfg){if(!cfg||typeof cfg!=='object')return;S.config=S.config||{};if(cfg.scheduler&&typeof cfg.scheduler==='object')S.config.scheduler=cfg.scheduler;if(cfg.session_creation_limit&&typeof cfg.session_creation_limit==='object')S.config.session_creation_limit=cfg.session_creation_limit;if(Object.prototype.hasOwnProperty.call(cfg,'daily_session_limit'))S.config.daily_session_limit=cfg.daily_session_limit;if(Object.prototype.hasOwnProperty.call(cfg,'download_js_lib_enabled'))S.config.download_js_lib_enabled=!!cfg.download_js_lib_enabled;if(Object.prototype.hasOwnProperty.call(cfg,'request_timeout_default'))S.config.request_timeout_default=cfg.request_timeout_default;if(Object.prototype.hasOwnProperty.call(cfg,'run_timeout'))S.config.run_timeout=cfg.run_timeout;if(Object.prototype.hasOwnProperty.call(cfg,'shell_command_timeout_seconds'))S.config.shell_command_timeout_seconds=cfg.shell_command_timeout_seconds;if(Object.prototype.hasOwnProperty.call(cfg,'model')&&String(cfg.model||'').trim())S.config.model=cfg.model}
@@ -43213,7 +44871,7 @@ function renderSessions(){
43213
44871
  }
43214
44872
  function _syncActiveSessionSummaryFromSnapshot(){const sid=String(S.activeId||'').trim();const snap=S.snap;if(!sid||!snap)return false;const rows=Array.isArray(S.sessions)?S.sessions.slice():[];let idx=rows.findIndex(row=>String(row?.id||'')===sid);const running=!!snap?.running;let updatedAt=Number(snap?.updated_at||0);if(!Number.isFinite(updatedAt)||updatedAt<=0){updatedAt=(Date.now()/1000)}let msgCount=Number(snap?.message_count);if(!Number.isFinite(msgCount)||msgCount<0){const arr=Array.isArray(snap?.messages)?snap.messages:[];let cnt=0;for(const row of arr){if(String(row?.role||'').trim()==='tool')continue;cnt+=1}msgCount=cnt}msgCount=Math.max(0,Math.floor(Number(msgCount)||0));const title=String(snap?.title||'').trim();if(idx<0){rows.push({id:sid,title:title||sid,running:running,updated_at:updatedAt,message_count:msgCount});idx=rows.length-1}else{const cur=rows[idx]||{};const next={...cur};let changed=false;if(!!cur.running!==running){next.running=running;changed=true}if(Number(cur.message_count||0)!==msgCount){next.message_count=msgCount;changed=true}if(Number(cur.updated_at||0)!==updatedAt){next.updated_at=updatedAt;changed=true}if(title&&String(cur.title||'')!==title){next.title=title;changed=true}if(!changed)return false;rows[idx]=next}rows.sort((a,b)=>Number(b?.updated_at||0)-Number(a?.updated_at||0));S.sessions=rows;return true}
43215
44873
  function diffLineClass(line){const t=String(line||'').trimStart();if(t.startsWith('+')||/^\\d+\\s+\\+\\s/.test(t))return 'diff-line-add';if(t.startsWith('-')||/^\\d+\\s+-\\s/.test(t))return 'diff-line-del';if(t.startsWith('@@')||t==='⋮'||t.startsWith('⋮ '))return 'diff-line-hunk';return ''}
43216
- function diffHtml(diff){return String(diff||'').split('\\n').map(line=>`<div class=\"${diffLineClass(line)}\">${esc(line)}</div>`).join('')}
44874
+ function diffHtml(diff){return String(diff||'').split('\\n').map(line=>`<div class=\"diff-row ${diffLineClass(line)}\">${esc(line)}</div>`).join('')}
43217
44875
  function _scrollContainerToNodeCenter(container,target){
43218
44876
  if(!container||!target)return;
43219
44877
  const maxTop=Math.max(0,Number(container.scrollHeight||0)-Number(container.clientHeight||0));
@@ -44592,8 +46250,8 @@ function _chatVirtReleaseNode(node){
44592
46250
  CHAT_VIRT.poolSize=Number(CHAT_VIRT.poolSize||0)+1;
44593
46251
  }
44594
46252
  function _chatVirtReleaseRendered(root){if(!root)return;for(const node of root.querySelectorAll('.msg[data-vk]')){_chatVirtReleaseNode(node)}}
44595
- function _chatVirtAgentRoleKey(raw){const role=String(raw||'').trim().toLowerCase();return(role==='explorer'||role==='developer'||role==='reviewer'||role==='manager'||role==='planner')?role:''}
44596
- function _chatVirtAgentRoleLabel(role){if(role==='explorer')return t('role_explorer');if(role==='developer')return t('role_developer');if(role==='reviewer')return t('role_reviewer');if(role==='manager')return t('role_manager');if(role==='planner')return t('role_planner');return t('role_agent')}
46253
+ function _chatVirtAgentRoleKey(raw){const role=String(raw||'').trim().toLowerCase();return(role==='explorer'||role==='developer'||role==='reviewer'||role==='manager'||role==='planner'||role==='single')?role:''}
46254
+ function _chatVirtAgentRoleLabel(role){if(role==='explorer')return t('role_explorer');if(role==='developer')return t('role_developer');if(role==='reviewer')return t('role_reviewer');if(role==='manager')return t('role_manager');if(role==='planner')return t('role_planner');if(role==='single')return t('role_single');return t('role_agent')}
44597
46255
  function _stripLeadingAgentTitle(raw,agentRole){
44598
46256
  let txt=String(raw||'').replace(/^\\uFEFF/,'').trimStart();
44599
46257
  const role=_chatVirtAgentRoleKey(agentRole);
@@ -45422,7 +47080,7 @@ function _cmdPageCount(op){const d=(op&&typeof op==='object'&&op.data&&typeof op
45422
47080
  function _cmdCurrentPage(op){if(!S.commandPageState||typeof S.commandPageState!=='object')S.commandPageState={};const key=_cmdStateKey(op);const total=_cmdPageCount(op);let page=Number(S.commandPageState[key]||1);if(!Number.isFinite(page)||page<1)page=1;if(page>total)page=total;S.commandPageState[key]=page;return page}
45423
47081
  function _cmdPageText(op,page){const d=(op&&typeof op==='object'&&op.data&&typeof op.data==='object')?op.data:{};const pages=Array.isArray(d.ui_output_pages)?d.ui_output_pages:[];if(!pages.length)return String(d.output||'');const idx=Math.max(0,Math.min(pages.length-1,Number(page||1)-1));return String(pages[idx]||'')}
45424
47082
  function _runtimePillHtml(label,value,opts={}){const wide=opts&&opts.wide?' runtime-pill-wide':'';const tone=opts&&opts.tone?` ${opts.tone}`:'';const mono=opts&&opts.mono?' mono':'';return `<span class=\"runtime-pill${wide}${tone}\"><span class=\"runtime-pill-label\">${esc(label)}</span><span class=\"runtime-pill-value${mono}\">${esc(String(value??'-'))}</span></span>`}
45425
- function renderBoards(){const uiState=S.staticMode?(S.frozen?'static':'live'):'live';const boolWord=v=>t(v?'state_on':'state_off');const activeRole=String(S.snap?.agent_active_role||'').trim();const activeRoleLabel=activeRole?_chatVirtAgentRoleLabel(activeRole):'-';const runtimeItems=[{label:t('rt_session'),value:S.snap?.id||'-',mono:true},{label:t('rt_model'),value:S.snap?.model||'-',mono:true},{label:t('rt_thinking'),value:boolWord(S.snap?.thinking)},{label:t('rt_thinking_stream'),value:boolWord(S.snap?.thinking_stream)},{label:t('rt_mode'),value:S.snap?.execution_mode||S.config?.execution_mode||'sync'},{label:t('rt_active_agent'),value:activeRoleLabel},{label:t('rt_blackboard'),value:S.snap?.blackboard?.status||'-'},{label:t('rt_task'),value:S.snap?.blackboard?.task_profile?.task_type||'-'},{label:t('rt_complexity'),value:S.snap?.blackboard?.task_profile?.complexity||'-'},{label:t('rt_judgement'),value:S.snap?.blackboard?.manager_judgement?.progress||'-'},{label:t('rt_budget'),value:S.snap?.blackboard?.task_profile?.round_budget??'-'},{label:t('rt_remaining'),value:S.snap?.blackboard?.manager_judgement?.remaining_rounds??'-'},{label:t('rt_blackboard_cycles'),value:S.snap?.blackboard?.manager_cycles??'-'},{label:t('rt_round_limit'),value:S.snap?.max_agent_rounds||'-'},{label:t('rt_round'),value:S.snap?.agent_round_index??'-'},{label:t('rt_phase'),value:S.snap?.agent_phase||t('idle')},{label:t('rt_queued_inputs'),value:S.snap?.queued_user_inputs_count??0},{label:t('rt_run_timeout'),value:`${S.snap?.max_run_seconds??'-'}s`},{label:t('rt_ctx_used'),value:S.snap?.context_tokens_estimate??'-'},{label:t('rt_ctx_limit'),value:S.snap?.context_effective_token_limit||S.snap?.context_token_upper_bound||'-'},{label:t('rt_ctx_mode'),value:t(S.snap?.context_token_limit_locked?'rt_manual_lock':'rt_adaptive')},{label:t('rt_ctx_left'),value:formatContextLeft(S.snap)},{label:t('rt_truncation'),value:S.snap?.truncation_count||0},{label:t('rt_trunc_retry'),value:S.snap?.live_truncation_attempts||0},{label:t('rt_trunc_tokens'),value:S.snap?.live_truncation_tokens||0},{label:t('rt_archive'),value:S.snap?.compact_segments_count||0},{label:t('rt_last_compact'),value:S.snap?.last_compact_reason||'-'},{label:t('rt_ollama'),value:S.snap?.ollama_base_url||'-',mono:true,wide:true},{label:t('rt_files'),value:S.snap?.session_files_root||'-',mono:true,wide:true},{label:t('rt_ui_mode'),value:uiState},{label:t('rt_state'),value:S.snap?.running?t('running'):t('idle'),tone:S.snap?.running?'state-running':'state-idle'}];E('status').innerHTML=runtimeItems.map(item=>_runtimePillHtml(item.label,item.value,item)).join('');
47083
+ function renderBoards(){const uiState=S.staticMode?(S.frozen?'static':'live'):'live';const boolWord=v=>t(v?'state_on':'state_off');const activeRole=String(S.snap?.agent_active_role||'').trim();const activeRoleLabel=activeRole?_chatVirtAgentRoleLabel(activeRole):'-';const runtimeItems=[{label:t('rt_session'),value:S.snap?.id||'-',mono:true},{label:t('rt_model'),value:S.snap?.model||'-',mono:true},{label:t('rt_thinking'),value:boolWord(S.snap?.thinking)},{label:t('rt_thinking_stream'),value:boolWord(S.snap?.thinking_stream)},{label:t('rt_mode'),value:S.snap?.execution_mode||S.config?.execution_mode||'sync'},{label:t('rt_active_agent'),value:activeRoleLabel},{label:t('rt_blackboard'),value:S.snap?.blackboard?.status||'-'},{label:t('rt_task'),value:S.snap?.blackboard?.task_profile?.task_type||'-'},{label:t('rt_complexity'),value:S.snap?.blackboard?.task_profile?.complexity||'-'},{label:t('rt_judgement'),value:S.snap?.blackboard?.manager_judgement?.progress||'-'},{label:t('rt_budget'),value:S.snap?.blackboard?.task_profile?.round_budget??'-'},{label:t('rt_remaining'),value:S.snap?.blackboard?.manager_judgement?.remaining_rounds??'-'},{label:t('rt_blackboard_cycles'),value:S.snap?.blackboard?.manager_cycles??'-'},{label:t('rt_round_limit'),value:S.snap?.max_agent_rounds||'-'},{label:t('rt_round'),value:S.snap?.agent_round_index??'-'},{label:t('rt_phase'),value:S.snap?.agent_phase||t('idle')},{label:t('rt_queued_inputs'),value:S.snap?.queued_user_inputs_count??0},{label:t('rt_run_timeout'),value:`${S.snap?.max_run_seconds??'-'}s`},{label:t('rt_ctx_used'),value:S.snap?.context_tokens_estimate??'-'},{label:t('rt_ctx_limit'),value:S.snap?.context_effective_token_limit||S.snap?.context_token_upper_bound||'-'},{label:t('rt_ctx_mode'),value:t(S.snap?.context_token_limit_locked?'rt_manual_lock':'rt_adaptive')},{label:t('rt_ctx_left'),value:formatContextLeft(S.snap)},{label:t('rt_truncation'),value:S.snap?.truncation_count||0},{label:t('rt_trunc_retry'),value:S.snap?.live_truncation_attempts||0},{label:t('rt_trunc_tokens'),value:S.snap?.live_truncation_tokens||0},{label:t('rt_archive'),value:S.snap?.compact_segments_count||0},{label:t('rt_last_compact'),value:S.snap?.last_compact_reason||'-'},{label:t('rt_ollama'),value:S.snap?.ollama_base_url||'-',mono:true,wide:true},{label:t('rt_files'),value:S.snap?.session_files_root||'-',mono:true,wide:true},{label:t('rt_ui_mode'),value:uiState},{label:t('rt_state'),value:S.snap?.running?t('running'):t('idle'),tone:S.snap?.running?'state-running':'state-idle'}];E('status').innerHTML=runtimeItems.map(item=>_runtimePillHtml(item.label,item.value,item)).join('')+agentContextChipsHtml(S.snap);
45426
47084
  renderCtxLive(S.snap);
45427
47085
  const _pmBtn=E('planModeBtn');if(_pmBtn){const _pm=S.snap?.plan_mode_preference||'auto';_pmBtn.textContent='Plan: '+_pm.charAt(0).toUpperCase()+_pm.slice(1)}
45428
47086
  const _lvl=S.snap?.user_task_level||0;updateLevelBtn(_lvl)
@@ -45801,7 +47459,7 @@ window.addEventListener('DOMContentLoaded',async()=>{for(const id of ['chat','se
45801
47459
  APP_TS = """type SessionSummary={id:string;title:string;running:boolean;updated_at:number;message_count:number};
45802
47460
  type Msg={role:string;text:string;thinking?:string;agent_role?:string};
45803
47461
  type UploadMeta={id:string;filename:string;workspace_path:string;kind:string;size:number;uploaded_at:number;preview?:string};
45804
- type Snapshot={id:string;title:string;running:boolean;message_count?:number;model:string;ollama_base_url:string;thinking:boolean;thinking_stream?:boolean;live_thinking?:string;live_truncation_text?:string;live_truncation_kind?:string;live_truncation_tool?:string;live_truncation_active?:boolean;live_truncation_attempts?:number;live_truncation_tokens?:number;live_run_notice_active?:boolean;live_run_notice_label?:string;live_run_notice_started_at?:number;live_run_notice_elapsed?:number;execution_mode?:string;agent_active_role?:string;max_agent_rounds?:number;max_run_seconds?:number;agent_round_index?:number;agent_phase?:string;agent_active_tool?:string;queued_user_inputs_count?:number;context_token_upper_bound?:number;context_token_limit_config?:number;context_token_limit_locked?:boolean;context_tokens_estimate?:number;context_left_tokens?:number;context_left_percent?:number;context_used_percent?:number;truncation_count?:number;compact_segments_count?:number;last_compact_reason?:string;last_compact_ts?:number;last_compact_segment_id?:string;event_seq?:number;render_bridge?:{seq:number;received?:number;last_ts?:number;last_kind?:string;latest?:Record<string,unknown>};blackboard?:{status?:string;original_goal?:string;manager_cycles?:number;active_agent?:string;approval?:Record<string,unknown>;last_delegate?:Record<string,unknown>};messages:Msg[];uploads?:UploadMeta[];llm_model_catalog?:ModelCatalog|null};
47462
+ type Snapshot={id:string;title:string;running:boolean;message_count?:number;model:string;ollama_base_url:string;thinking:boolean;thinking_stream?:boolean;live_thinking?:string;live_truncation_text?:string;live_truncation_kind?:string;live_truncation_tool?:string;live_truncation_active?:boolean;live_truncation_attempts?:number;live_truncation_tokens?:number;live_run_notice_active?:boolean;live_run_notice_label?:string;live_run_notice_started_at?:number;live_run_notice_elapsed?:number;execution_mode?:string;agent_active_role?:string;agent_contexts?:Array<{role:string;label?:string;active?:boolean;used?:number;left?:number;left_percent?:number;effective_limit?:number;tier?:number;message_count?:number;next_call_label?:string}>;max_agent_rounds?:number;max_run_seconds?:number;agent_round_index?:number;agent_phase?:string;agent_active_tool?:string;queued_user_inputs_count?:number;context_token_upper_bound?:number;context_token_limit_config?:number;context_token_limit_locked?:boolean;context_tokens_estimate?:number;context_left_tokens?:number;context_left_percent?:number;context_used_percent?:number;truncation_count?:number;compact_segments_count?:number;last_compact_reason?:string;last_compact_ts?:number;last_compact_segment_id?:string;event_seq?:number;render_bridge?:{seq:number;received?:number;last_ts?:number;last_kind?:string;latest?:Record<string,unknown>};blackboard?:{status?:string;original_goal?:string;manager_cycles?:number;active_agent?:string;approval?:Record<string,unknown>;last_delegate?:Record<string,unknown>};messages:Msg[];uploads?:UploadMeta[];llm_model_catalog?:ModelCatalog|null};
45805
47463
  type SkillMeta={name:string;qualified_name?:string;description:string;provider_id?:string;protocol?:string;meta:Record<string,string>};
45806
47464
  type SkillProvider={provider_id:string;protocol:string;protocol_version:string;skill_count:number;description:string};
45807
47465
  type SkillProtocol={protocol:string;version:string;active_providers:number;active_skills:number;description:string};