clouds-coder 2026.3.8__tar.gz → 2026.3.16__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.
@@ -71,7 +71,8 @@ DEFAULT_TIMEOUT_SECONDS = max(
71
71
  )
72
72
  DEFAULT_REQUEST_TIMEOUT = DEFAULT_TIMEOUT_SECONDS
73
73
  AUTO_CONTINUE_BUDGET_DEFAULT = 30
74
- AGENT_MAX_OUTPUT_TOKENS = 2200
74
+ AGENT_MAX_OUTPUT_TOKENS = 16384
75
+ OLLAMA_THINKING_TOOL_BUFFER = 4096
75
76
  WATCHDOG_INTENT_NO_TOOL_THRESHOLD = 2
76
77
  WATCHDOG_REPEAT_NO_TOOL_THRESHOLD = 2
77
78
  WATCHDOG_STATE_STALL_THRESHOLD = 6
@@ -202,7 +203,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
202
203
  "execution_mode": EXECUTION_MODE_SINGLE,
203
204
  "participants": ["developer"],
204
205
  "assigned_expert": "developer",
205
- "round_budget": 4,
206
+ "round_budget": 2,
206
207
  "requires_user_confirmation": False,
207
208
  "complexity": "simple",
208
209
  },
@@ -211,7 +212,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
211
212
  "execution_mode": EXECUTION_MODE_SINGLE,
212
213
  "participants": ["developer"],
213
214
  "assigned_expert": "developer",
214
- "round_budget": 10,
215
+ "round_budget": 6,
215
216
  "requires_user_confirmation": False,
216
217
  "complexity": "simple",
217
218
  },
@@ -220,7 +221,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
220
221
  "execution_mode": EXECUTION_MODE_SYNC,
221
222
  "participants": ["explorer", "developer"],
222
223
  "assigned_expert": "developer",
223
- "round_budget": 12,
224
+ "round_budget": 10,
224
225
  "requires_user_confirmation": False,
225
226
  "complexity": "simple",
226
227
  },
@@ -229,7 +230,7 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
229
230
  "execution_mode": EXECUTION_MODE_SYNC,
230
231
  "participants": ["explorer", "developer", "reviewer"],
231
232
  "assigned_expert": "developer",
232
- "round_budget": 36,
233
+ "round_budget": 24,
233
234
  "requires_user_confirmation": False,
234
235
  "complexity": "complex",
235
236
  },
@@ -485,6 +486,30 @@ CODE_PREVIEW_EXTS = {
485
486
  ".svelte",
486
487
  ".gradle",
487
488
  ".properties",
489
+ # Fortran
490
+ ".f", ".f90", ".f95", ".f03", ".f08", ".for", ".fpp",
491
+ # Haskell
492
+ ".hs", ".lhs",
493
+ # Erlang / Elixir
494
+ ".erl", ".hrl", ".ex", ".exs",
495
+ # OCaml
496
+ ".ml", ".mli",
497
+ # HDL
498
+ ".vhd", ".vhdl", ".v", ".sv",
499
+ # Assembly
500
+ ".asm", ".s",
501
+ # Infra / Schema
502
+ ".proto", ".tf", ".tfvars", ".prisma", ".graphql", ".gql",
503
+ # Modern systems
504
+ ".zig", ".nim", ".jl", ".cr", ".d",
505
+ # Lisp family
506
+ ".clj", ".cljs", ".cljc", ".lisp", ".cl", ".el", ".rkt",
507
+ # Pascal
508
+ ".pas", ".pp",
509
+ # Shader
510
+ ".wgsl", ".glsl", ".hlsl",
511
+ # Misc
512
+ ".groovy", ".cmake", ".dockerfile",
488
513
  }
489
514
  CODE_PREVIEW_FILENAMES = {
490
515
  "dockerfile",
@@ -1310,6 +1335,117 @@ def trim(text: object, limit: int = MAX_TOOL_OUTPUT) -> str:
1310
1335
  s = str(text)
1311
1336
  return s if len(s) <= limit else s[:limit] + "\n...(truncated)"
1312
1337
 
1338
+
1339
+ def _fmt_export_ts(ts: float | int) -> str:
1340
+ v = float(ts or 0)
1341
+ if v <= 0:
1342
+ return ""
1343
+ try:
1344
+ from datetime import datetime
1345
+ return datetime.fromtimestamp(v).strftime("%Y-%m-%d %H:%M:%S")
1346
+ except Exception:
1347
+ return ""
1348
+
1349
+
1350
+ def _html_esc(text: str) -> str:
1351
+ return html.escape(str(text or ""))
1352
+
1353
+
1354
+ def _text_to_minimal_pdf(text: str) -> bytes:
1355
+ """纯 Python 最小 PDF 生成器,无外部依赖。"""
1356
+ raw = str(text or "")
1357
+ lines = raw.replace("\r\n", "\n").replace("\r", "\n").split("\n")
1358
+ font_size = 10
1359
+ leading = font_size * 1.35
1360
+ margin_top, margin_bottom, margin_left, margin_right = 50, 50, 50, 50
1361
+ page_w, page_h = 595, 842 # A4
1362
+ usable_w = page_w - margin_left - margin_right
1363
+ max_chars_per_line = max(20, int(usable_w / (font_size * 0.52)))
1364
+ usable_h = page_h - margin_top - margin_bottom
1365
+ lines_per_page = max(1, int(usable_h / leading))
1366
+
1367
+ # 折行
1368
+ wrapped: list[str] = []
1369
+ for line in lines:
1370
+ if not line:
1371
+ wrapped.append("")
1372
+ continue
1373
+ while len(line) > max_chars_per_line:
1374
+ wrapped.append(line[:max_chars_per_line])
1375
+ line = line[max_chars_per_line:]
1376
+ wrapped.append(line)
1377
+
1378
+ # 分页
1379
+ pages: list[list[str]] = []
1380
+ for i in range(0, len(wrapped), lines_per_page):
1381
+ pages.append(wrapped[i:i + lines_per_page])
1382
+ if not pages:
1383
+ pages = [[""]]
1384
+
1385
+ def _pdf_escape(s: str) -> str:
1386
+ return s.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
1387
+
1388
+ def _safe_latin1(s: str) -> str:
1389
+ return s.encode("latin-1", errors="replace").decode("latin-1")
1390
+
1391
+ objects: list[bytes] = []
1392
+ offsets: list[int] = []
1393
+ buf = b"%PDF-1.4\n"
1394
+
1395
+ def add_obj(content: str) -> int:
1396
+ nonlocal buf
1397
+ idx = len(objects) + 1
1398
+ offsets.append(len(buf))
1399
+ obj_bytes = f"{idx} 0 obj\n{content}\nendobj\n".encode("latin-1")
1400
+ buf += obj_bytes
1401
+ objects.append(obj_bytes)
1402
+ return idx
1403
+
1404
+ catalog_id = add_obj("<< /Type /Catalog /Pages 2 0 R >>")
1405
+ add_obj("PAGES_PLACEHOLDER") # obj 2: placeholder, replaced after page generation
1406
+ font_id = add_obj("<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>")
1407
+
1408
+ page_ids: list[int] = []
1409
+ for page_lines in pages:
1410
+ stream_lines = [f"BT /F1 {font_size} Tf"]
1411
+ y = page_h - margin_top
1412
+ stream_lines.append(f"{margin_left} {y} Td")
1413
+ for pl in page_lines:
1414
+ safe = _safe_latin1(_pdf_escape(pl))
1415
+ stream_lines.append(f"({safe}) Tj")
1416
+ stream_lines.append(f"0 -{leading:.1f} Td")
1417
+ stream_lines.append("ET")
1418
+ stream = "\n".join(stream_lines)
1419
+ stream_bytes = stream.encode("latin-1")
1420
+ content_id = add_obj(f"<< /Length {len(stream_bytes)} >>\nstream\n{stream}\nendstream")
1421
+ page_id = add_obj(
1422
+ f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {page_w} {page_h}] "
1423
+ f"/Contents {content_id} 0 R /Resources << /Font << /F1 {font_id} 0 R >> >> >>"
1424
+ )
1425
+ page_ids.append(page_id)
1426
+
1427
+ kids = " ".join(f"{pid} 0 R" for pid in page_ids)
1428
+ pages_content = f"<< /Type /Pages /Kids [{kids}] /Count {len(page_ids)} >>"
1429
+ pages_bytes = f"2 0 obj\n{pages_content}\nendobj\n".encode("latin-1")
1430
+ old_pages = objects[1]
1431
+ buf = buf.replace(b"2 0 obj\nPAGES_PLACEHOLDER\nendobj\n", pages_bytes)
1432
+ size_diff = len(pages_bytes) - len(old_pages)
1433
+ for i in range(2, len(offsets)):
1434
+ offsets[i] += size_diff
1435
+
1436
+ xref_offset = len(buf)
1437
+ buf += b"xref\n"
1438
+ buf += f"0 {len(objects) + 1}\n".encode("latin-1")
1439
+ buf += b"0000000000 65535 f \n"
1440
+ for off in offsets:
1441
+ buf += f"{off:010d} 00000 n \n".encode("latin-1")
1442
+ buf += b"trailer\n"
1443
+ buf += f"<< /Size {len(objects) + 1} /Root {catalog_id} 0 R >>\n".encode("latin-1")
1444
+ buf += b"startxref\n"
1445
+ buf += f"{xref_offset}\n".encode("latin-1")
1446
+ buf += b"%%EOF\n"
1447
+ return buf
1448
+
1313
1449
  def compress_text_blob(text: str) -> str:
1314
1450
  src = str(text or "")
1315
1451
  if not src:
@@ -3435,6 +3571,7 @@ def _module_exists(name: str) -> bool:
3435
3571
 
3436
3572
  def detect_upload_parser_capabilities() -> dict:
3437
3573
  return {
3574
+ "pdfminer": _module_exists("pdfminer"),
3438
3575
  "openpyxl": _module_exists("openpyxl"),
3439
3576
  "xlrd": _module_exists("xlrd"),
3440
3577
  "python_docx": _module_exists("docx"),
@@ -3446,6 +3583,7 @@ def detect_upload_parser_capabilities() -> dict:
3446
3583
  "catdoc": bool(shutil.which("catdoc")),
3447
3584
  "catppt": bool(shutil.which("catppt")),
3448
3585
  "textutil": bool(shutil.which("textutil")),
3586
+ "playwright": _module_exists("playwright"),
3449
3587
  }
3450
3588
 
3451
3589
  def _render_cap_markdown(caps: dict) -> str:
@@ -4671,6 +4809,96 @@ SKILL_PROTOCOL_SPECS = [
4671
4809
  },
4672
4810
  ]
4673
4811
 
4812
+ # ---------------------------------------------------------------------------
4813
+ # Built-in skill guides (injected into SkillStore on reload)
4814
+ # ---------------------------------------------------------------------------
4815
+ _BUILTIN_SKILLS: dict[str, dict] = {
4816
+ "workspace-paths": {
4817
+ "description": "File path rules, virtual path mapping, relative path conventions",
4818
+ "body": (
4819
+ "# Workspace Paths Guide\n"
4820
+ "- Always use relative paths (e.g. src/main.py) for file tools.\n"
4821
+ "- Runtime maps relative paths to session absolute root automatically.\n"
4822
+ "- '/workspace/...' is a virtual alias for tool arguments only; never create OS-level /workspace.\n"
4823
+ "- When user references uploaded files, prioritize workspace uploads directory.\n"
4824
+ "- For shell commands, use relative paths or $PWD-based references.\n"
4825
+ ),
4826
+ },
4827
+ "task-management": {
4828
+ "description": "Todo/Task creation, updates, and best practices",
4829
+ "body": (
4830
+ "# Task Management Guide\n"
4831
+ "- For level 1-2 (simple) tasks: skip todo scaffolding, give direct response.\n"
4832
+ "- For level 3+ tasks: call TodoWrite early with 3-7 concise items, one marked in_progress.\n"
4833
+ "- Update todos only when plan or status actually changes. Avoid redundant calls.\n"
4834
+ "- If TodoWrite fails or repeats unchanged, use TodoWriteRescue with simple string items.\n"
4835
+ "- Use task_create/task_update/task_list for multi-step structured work.\n"
4836
+ ),
4837
+ },
4838
+ "tool-best-practices": {
4839
+ "description": "write_file vs edit_file, bash usage, error handling conventions",
4840
+ "body": (
4841
+ "# Tool Best Practices\n"
4842
+ "- Prefer write_file/edit_file for code changes (UI renders line-level diffs).\n"
4843
+ "- If write_file/edit_file fails due to malformed arguments, regenerate complete JSON.\n"
4844
+ "- If output looks truncated, split into smaller subtasks.\n"
4845
+ "- For shell commands: use bash_command for execution, not file tools.\n"
4846
+ "- Always end thinking sections with either a final answer or one tool call.\n"
4847
+ "- Never stop at thinking-only content without an action.\n"
4848
+ ),
4849
+ },
4850
+ "context-management": {
4851
+ "description": "Context compaction triggers, context_recall usage, step sizing",
4852
+ "body": (
4853
+ "# Context Management Guide\n"
4854
+ "- Context has a token upper bound; keep steps compact.\n"
4855
+ "- When <compact-resume> hint appears, inherit pending todos and continue immediately.\n"
4856
+ "- After compaction, use context_recall to fetch archived messages by segment_id/query.\n"
4857
+ "- Do not guess content that was compacted away—recall it first.\n"
4858
+ "- For large tasks, break into subtasks to avoid context overflow.\n"
4859
+ ),
4860
+ },
4861
+ "multi-agent-guide": {
4862
+ "description": "Blackboard read/write norms, ask_colleague usage, handoff format",
4863
+ "body": (
4864
+ "# Multi-Agent Guide\n"
4865
+ "- Use read_from_blackboard to check shared state before acting.\n"
4866
+ "- Use write_to_blackboard to record progress, artifacts, and blockers.\n"
4867
+ "- Use ask_colleague with structured intent for inter-agent communication:\n"
4868
+ " - explorer->developer: 'handoff' with research findings\n"
4869
+ " - developer->reviewer: 'review_request' with changed files list\n"
4870
+ " - reviewer->developer: 'fix_request' with concrete failure evidence\n"
4871
+ "- Handoffs should include enough context for the target agent to act independently.\n"
4872
+ "- Keep blackboard updates atomic and concise.\n"
4873
+ ),
4874
+ },
4875
+ "code-review-checklist": {
4876
+ "description": "Reviewer role checklist for code verification",
4877
+ "body": (
4878
+ "# Code Review Checklist\n"
4879
+ "1. Does the implementation match the original goal?\n"
4880
+ "2. Are there syntax errors? Run linting/type checks if available.\n"
4881
+ "3. Are there obvious logic bugs or edge cases missed?\n"
4882
+ "4. Do tests pass? If no tests exist, verify manually.\n"
4883
+ "5. Are there security issues (injection, XSS, hardcoded secrets)?\n"
4884
+ "6. Write review_feedback to blackboard with pass/fail and evidence.\n"
4885
+ "7. If fail: send fix_request via ask_colleague with specific issues.\n"
4886
+ ),
4887
+ },
4888
+ "finish-protocol": {
4889
+ "description": "When and how to call finish_current_task correctly",
4890
+ "body": (
4891
+ "# Finish Protocol\n"
4892
+ "- Call finish_current_task when all required work is complete.\n"
4893
+ "- Include a concise summary of what was done.\n"
4894
+ "- For multi-agent mode: finish triggers auto-summary from blackboard state.\n"
4895
+ "- Do not finish if there are known failing tests or unresolved blockers.\n"
4896
+ "- If todos have stale pending items but work is done, finish anyway—stale items are cleared automatically.\n"
4897
+ "- Summary format: list modified files, key changes, and validation status.\n"
4898
+ ),
4899
+ },
4900
+ }
4901
+
4674
4902
  class SkillStore:
4675
4903
  def __init__(self, skills_root: Path):
4676
4904
  self.skills_root = skills_root
@@ -5075,9 +5303,34 @@ class SkillStore:
5075
5303
  self._load_local_skills()
5076
5304
  self._load_manifest_providers()
5077
5305
  self._load_clawhub_autodetect()
5306
+ self._inject_builtin_skills()
5078
5307
  self.fingerprint = fp
5079
5308
  self.last_reload_ts = now
5080
5309
 
5310
+ def _inject_builtin_skills(self):
5311
+ """Register built-in skill guides for small-model support."""
5312
+ provider_id = "builtin"
5313
+ if not self._provider_exists(provider_id):
5314
+ self._register_provider(
5315
+ provider_id, "builtin", "1.0",
5316
+ "Built-in skill guides for agent operation",
5317
+ )
5318
+ for skill_name, data in _BUILTIN_SKILLS.items():
5319
+ key = f"{provider_id}:{skill_name}"
5320
+ if key in self.skills:
5321
+ continue
5322
+ self._register_skill(
5323
+ provider_id=provider_id,
5324
+ protocol="builtin",
5325
+ protocol_version="1.0",
5326
+ name=skill_name,
5327
+ description=data["description"],
5328
+ meta={"builtin": True},
5329
+ body=data["body"],
5330
+ skill_path="(builtin)",
5331
+ attachments=[],
5332
+ )
5333
+
5081
5334
  def descriptions(self, max_items: int = SKILL_PROMPT_MAX_ITEMS, max_chars: int = SKILL_PROMPT_MAX_CHARS) -> str:
5082
5335
  if not self.skills:
5083
5336
  return "(no skills)"
@@ -6486,11 +6739,14 @@ class OllamaClient:
6486
6739
  think: bool = False,
6487
6740
  on_thinking_chunk=None,
6488
6741
  ) -> dict:
6742
+ effective_max = max_tokens
6743
+ if tools:
6744
+ effective_max = max(max_tokens, max_tokens + OLLAMA_THINKING_TOOL_BUFFER)
6489
6745
  payload = {
6490
6746
  "model": self.model,
6491
6747
  "messages": req_messages,
6492
6748
  "stream": True,
6493
- "options": {"temperature": temperature, "num_predict": max_tokens},
6749
+ "options": {"temperature": temperature, "num_predict": effective_max},
6494
6750
  }
6495
6751
  if tools:
6496
6752
  payload["tools"] = tools
@@ -6503,6 +6759,7 @@ class OllamaClient:
6503
6759
  full_content: list[str] = []
6504
6760
  full_thinking: list[str] = []
6505
6761
  tool_calls: list[dict] = []
6762
+ done_reason = ""
6506
6763
  try:
6507
6764
  with urlopen(req, timeout=self.timeout) as resp:
6508
6765
  while True:
@@ -6542,6 +6799,7 @@ class OllamaClient:
6542
6799
  if tcs:
6543
6800
  tool_calls = self._normalize_tool_calls(tcs)
6544
6801
  if part.get("done"):
6802
+ done_reason = str(part.get("done_reason") or "").strip().lower()
6545
6803
  break
6546
6804
  except HTTPError as exc:
6547
6805
  text = exc.read().decode("utf-8", errors="replace")
@@ -6554,7 +6812,7 @@ class OllamaClient:
6554
6812
  "content": content,
6555
6813
  "thinking": thinking_content,
6556
6814
  "tool_calls": tool_calls,
6557
- "raw": {"streamed": True},
6815
+ "raw": {"streamed": True, "done_reason": done_reason},
6558
6816
  }
6559
6817
 
6560
6818
  def chat(
@@ -6618,11 +6876,14 @@ class OllamaClient:
6618
6876
  fallback_status = {0, 400, 404, 405, 500, 501, 502, 503, 504}
6619
6877
  if status not in fallback_status:
6620
6878
  raise
6879
+ effective_max_native = max_tokens
6880
+ if tools:
6881
+ effective_max_native = max(max_tokens, max_tokens + OLLAMA_THINKING_TOOL_BUFFER)
6621
6882
  native_payload = {
6622
6883
  "model": self.model,
6623
6884
  "messages": req_messages,
6624
6885
  "stream": False,
6625
- "options": {"temperature": temperature, "num_predict": max_tokens},
6886
+ "options": {"temperature": temperature, "num_predict": effective_max_native},
6626
6887
  }
6627
6888
  if tools:
6628
6889
  native_payload["tools"] = tools
@@ -6918,6 +7179,7 @@ class SessionState:
6918
7179
  arbiter_max_tokens: int = ARBITER_DEFAULT_MAX_TOKENS,
6919
7180
  arbiter_temperature: float = ARBITER_DEFAULT_TEMPERATURE,
6920
7181
  execution_mode: str = EXECUTION_MODE_SYNC,
7182
+ max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
6921
7183
  ui_language: str = DEFAULT_UI_LANGUAGE,
6922
7184
  js_lib_root: Path | None = None,
6923
7185
  owner_user_id: str = "",
@@ -6973,8 +7235,9 @@ class SessionState:
6973
7235
  self.bus = MessageBus(self.root / "team" / "inbox", crypto)
6974
7236
  self.worktrees = WorktreeManager(self.id, self.tasks, self.root, crypto, repo_root)
6975
7237
  self.messages: list[dict] = []
6976
- self.contexts: dict[str, list[dict]] = {role: [] for role in AGENT_ROLES}
6977
- self.manager_context: list[dict] = []
7238
+ self.agent_messages: list[dict] = [] # unified agent context (replaces contexts + manager_context)
7239
+ self.contexts: dict[str, list[dict]] = {role: [] for role in AGENT_ROLES} # kept as view caches
7240
+ self.manager_context: list[dict] = [] # kept as view cache
6978
7241
  self.blackboard: dict = {}
6979
7242
  self.agent_bus_messages: list[dict] = []
6980
7243
  self.manager_routes: list[dict] = []
@@ -7055,6 +7318,7 @@ class SessionState:
7055
7318
  MIN_AGENT_ROUNDS,
7056
7319
  min(MAX_AGENT_ROUNDS_CAP, int(max_rounds or MAX_AGENT_ROUNDS)),
7057
7320
  )
7321
+ self.max_output_tokens = max(256, int(max_output_tokens or AGENT_MAX_OUTPUT_TOKENS))
7058
7322
  self.max_run_seconds = normalize_timeout_seconds(
7059
7323
  max_run_seconds if max_run_seconds is not None else MAX_RUN_SECONDS,
7060
7324
  minimum=MIN_RUN_TIMEOUT_SECONDS,
@@ -8037,6 +8301,13 @@ class SessionState:
8037
8301
  if isinstance(row, dict):
8038
8302
  clean_manager_context.append(dict(row))
8039
8303
  self.manager_context = clean_manager_context[-400:]
8304
+ raw_agent_messages = raw.get("agent_messages", [])
8305
+ if isinstance(raw_agent_messages, list):
8306
+ clean_am: list[dict] = []
8307
+ for row in raw_agent_messages[-1200:]:
8308
+ if isinstance(row, dict):
8309
+ clean_am.append(dict(row))
8310
+ self.agent_messages = clean_am[-800:]
8040
8311
  raw_blackboard = raw.get("blackboard", {})
8041
8312
  self.blackboard = self._normalize_blackboard(raw_blackboard)
8042
8313
  raw_bus = raw.get("agent_bus_messages", [])
@@ -8159,6 +8430,7 @@ class SessionState:
8159
8430
  "active_agent_role": str(self.active_agent_role or ""),
8160
8431
  "contexts": {role: list(self.contexts.get(role, []))[-400:] for role in AGENT_ROLES},
8161
8432
  "manager_context": list(self.manager_context)[-400:],
8433
+ "agent_messages": list(self.agent_messages)[-800:],
8162
8434
  "blackboard": self._normalize_blackboard(self.blackboard),
8163
8435
  "agent_bus_messages": list(self.agent_bus_messages)[-240:],
8164
8436
  "manager_routes": list(self.manager_routes)[-240:],
@@ -8625,75 +8897,25 @@ class SessionState:
8625
8897
  research_hint = self._deep_research_boost_instruction()
8626
8898
  runtime_level = int(self.runtime_task_level or 0)
8627
8899
  runtime_mode = self._effective_execution_mode()
8628
- runtime_assigned = self._sanitize_agent_role(self.runtime_assigned_expert) or "developer"
8629
- runtime_participants = [
8630
- role for role in [self._sanitize_agent_role(x) for x in self.runtime_participants] if role
8631
- ][:3]
8632
- if runtime_level in {1, 2} or runtime_mode == EXECUTION_MODE_SINGLE:
8633
- todo_hint = (
8634
- "Current run is direct single-agent mode. "
8635
- "Do not create Todo/task scaffolding unless user explicitly asks for project management. "
8636
- "Prioritize direct final response or one concrete minimal tool action."
8637
- )
8638
- else:
8639
- todo_hint = (
8640
- "For non-trivial tasks, call TodoWrite early with 3-7 concise items "
8641
- "(exactly one in_progress), then update only when plan/status changes. "
8642
- "Avoid redundant TodoWrite calls. "
8643
- "If TodoWrite fails or repeats with no changes, call TodoWriteRescue with simple string items."
8644
- )
8645
- route_hint = (
8646
- f"Runtime manager classification: level={runtime_level or '-'}, mode={runtime_mode}, "
8647
- f"scale_preference={self.runtime_scale_preference or 'balanced'}, "
8648
- f"assigned_expert={runtime_assigned}, participants={','.join(runtime_participants) or runtime_assigned}. "
8649
- )
8650
- if int(self.runtime_round_budget or 0) <= 0:
8651
- budget_hint = (
8652
- "Runtime budget policy: unlimited tier budget; keep each step compact and action-oriented; "
8653
- "never stall on long planning text."
8654
- )
8655
- else:
8656
- budget_hint = (
8657
- f"Runtime budget policy: internal cadence budget={int(self.runtime_round_budget)} "
8658
- "for compact reasoning and fast handoffs. "
8659
- "Budget controls thought depth only and must not be used as an early-stop user-facing reason."
8660
- )
8900
+ budget = int(self.runtime_round_budget or 0)
8661
8901
  html_block = f"{html_hint}\n\n" if html_hint else ""
8662
8902
  research_block = f"{research_hint}\n\n" if research_hint else ""
8663
8903
  return (
8664
- f"You are a coding agent running in isolated session workspace {self.files_root}. "
8665
- f"Session absolute writable root is {self.files_root}. "
8666
- "For file tools, prefer relative paths like hello.txt; runtime will map them to the absolute session root. "
8667
- "The '/workspace/...' form is only a virtual alias for path arguments; never create OS-level /workspace in shell. "
8904
+ f"You are a coding agent. Workspace: {self.files_root}. "
8905
+ f"Task level={runtime_level}, mode={runtime_mode}, "
8906
+ f"budget={'unlimited' if budget <= 0 else budget}. "
8907
+ f"Context limit ~{self.context_token_upper_bound} tokens. "
8668
8908
  f"{_detect_os_shell_instruction()} "
8669
- "Use tools to inspect files, execute commands, and edit code safely. "
8670
- f"{route_hint}"
8671
- f"{budget_hint} "
8672
- f"{todo_hint} "
8673
- "When all required work is complete, call finish_current_task (or finish_task / mark_done) "
8674
- "to end execution cleanly "
8675
- "(especially when todos may still contain stale pending items). "
8676
- "If you output internal reasoning/thinking, always end that section and then output either "
8677
- "a final answer or one concrete tool call; never stop at thinking-only content. "
8678
- "For multi-step work use task_create/task_update/task_list as needed. "
8679
- "Use load_skill only when needed; use provider:name if skill name is ambiguous. "
8680
- "Loaded skill content is cached per session; do not repeatedly call load_skill for the same skill unless needed. "
8681
- "If execution stalls (no tool calls / repeated failures), load_skill('execution-degradation-recovery') and follow it. "
8682
- "Use list_skill_providers and list_skill_protocols to inspect dynamic backend skill integrations. "
8683
- "If user asks to save generated guidance/workflow as reusable skill, call write_skill to write SKILL.md under global ./skills. "
8684
- "When changing files, prefer write_file/edit_file so the UI can render line-level diffs. "
8685
- "If a write_file/edit_file tool call fails due malformed or truncated arguments, regenerate and resend the complete JSON arguments. "
8686
- "If output or tool arguments look truncated, split work into smaller subtasks and execute one subtask at a time. "
8687
- "If context has been compacted, call context_recall to fetch exact archived messages by segment_id/query before guessing. "
8688
- "When a <compact-resume> hint appears, inherit pending todos/tasks and continue exploration immediately. "
8689
- f"Current context upper bound is ~{self.context_token_upper_bound} tokens; keep steps compact to stay under this limit. "
8690
- "When user asks to modify uploaded content, prioritize files under the uploaded workspace paths.\n\n"
8691
- "If user asks to generate image/audio/video, use generate_media when active model capability supports it.\n\n"
8909
+ "Use tools to inspect, edit, and execute. "
8910
+ "Call finish_current_task when done. "
8911
+ "Use load_skill for guidance on specific topics "
8912
+ "(workspace-paths, task-management, tool-best-practices, context-management). "
8913
+ "If execution stalls, load_skill('execution-degradation-recovery'). "
8692
8914
  f"{html_block}"
8693
8915
  f"{research_block}"
8694
8916
  f"{model_language_instruction(self.ui_language)}\n\n"
8695
- f"Uploaded files context:\n{uploads_ctx}\n\n"
8696
- f"Available skills:\n{self.skills.descriptions()}"
8917
+ f"Uploads:\n{uploads_ctx}\n\n"
8918
+ f"Skills:\n{self.skills.descriptions()}"
8697
8919
  )
8698
8920
 
8699
8921
  def _estimate_tokens(self) -> int:
@@ -9261,7 +9483,7 @@ class SessionState:
9261
9483
  if tool_calls:
9262
9484
  return False
9263
9485
 
9264
- near_limit = output_tokens >= int(AGENT_MAX_OUTPUT_TOKENS * 0.90)
9486
+ near_limit = output_tokens >= int(self.max_output_tokens * 0.90)
9265
9487
  # Mid-size outputs (e.g. planning text ending with a Chinese colon) should not be
9266
9488
  # treated as truncation unless close to max tokens or JSON-like unfinished payload.
9267
9489
  json_like_tail = bool(
@@ -9406,17 +9628,108 @@ class SessionState:
9406
9628
  task_ids = self._create_truncation_subtasks(reason)
9407
9629
  self._inject_truncation_rescue_hint(reason, output_tokens, task_ids)
9408
9630
 
9409
- def _microcompact(self):
9631
+ def _find_tool_name_by_id(self, messages: list[dict], tool_use_id: str) -> str:
9632
+ """Find tool name from preceding assistant message by tool_use_id."""
9633
+ if not tool_use_id:
9634
+ return ""
9635
+ for msg in reversed(messages):
9636
+ if msg.get("role") != "assistant":
9637
+ continue
9638
+ for tc in msg.get("tool_calls", []):
9639
+ if isinstance(tc, dict) and tc.get("id") == tool_use_id:
9640
+ fn = tc.get("function", {})
9641
+ return str(fn.get("name", "")) if isinstance(fn, dict) else ""
9642
+ content = msg.get("content")
9643
+ if isinstance(content, list):
9644
+ for block in content:
9645
+ if isinstance(block, dict) and block.get("type") == "tool_use" and block.get("id") == tool_use_id:
9646
+ return str(block.get("name", ""))
9647
+ return ""
9648
+
9649
+ def _microcompact(self, keep_recent: int = 3):
9650
+ """Replace old tool results with compact placeholders to save tokens.
9651
+
9652
+ Handles both OpenAI-style (role=tool) and Anthropic-style (tool_result blocks)
9653
+ messages. Keeps the most recent `keep_recent` tool interactions intact.
9654
+ """
9655
+ # Phase 1: OpenAI-style role=tool messages
9410
9656
  tool_messages = [m for m in self.messages if m.get("role") == "tool"]
9411
- if len(tool_messages) <= 3:
9657
+ if len(tool_messages) > keep_recent:
9658
+ kept = tool_messages[-keep_recent:]
9659
+ keep_ids = {id(x) for x in kept}
9660
+ for msg in self.messages:
9661
+ if msg.get("role") == "tool" and id(msg) not in keep_ids:
9662
+ content = msg.get("content", "")
9663
+ if isinstance(content, str) and len(content) > 120:
9664
+ msg["content"] = "[cleared by microcompact]"
9665
+
9666
+ # Phase 2: Anthropic-style tool_result blocks in user messages
9667
+ assistant_indices = [i for i, m in enumerate(self.messages) if m.get("role") == "assistant"]
9668
+ if len(assistant_indices) <= keep_recent:
9412
9669
  return
9413
- kept = tool_messages[-3:]
9414
- keep_ids = {id(x) for x in kept}
9415
- for msg in self.messages:
9416
- if msg.get("role") == "tool" and id(msg) not in keep_ids:
9417
- content = msg.get("content", "")
9418
- if isinstance(content, str) and len(content) > 120:
9419
- msg["content"] = "[cleared by microcompact]"
9670
+ cutoff = assistant_indices[-keep_recent]
9671
+ for i in range(cutoff):
9672
+ msg = self.messages[i]
9673
+ if msg.get("role") == "user" and isinstance(msg.get("content"), list):
9674
+ changed = False
9675
+ new_content = []
9676
+ for block in msg["content"]:
9677
+ if isinstance(block, dict) and block.get("type") == "tool_result":
9678
+ block_content = block.get("content", "")
9679
+ if isinstance(block_content, str) and len(block_content) > 120:
9680
+ tool_id = block.get("tool_use_id", "")
9681
+ tool_name = self._find_tool_name_by_id(self.messages[:i], tool_id) or "tool"
9682
+ new_content.append({
9683
+ "type": "tool_result",
9684
+ "tool_use_id": tool_id,
9685
+ "content": f"[Previous: used {tool_name}]",
9686
+ })
9687
+ changed = True
9688
+ else:
9689
+ new_content.append(block)
9690
+ else:
9691
+ new_content.append(block)
9692
+ if changed:
9693
+ msg["content"] = new_content
9694
+
9695
+ def _microcompact_agent_messages(self, messages: list[dict], keep_recent: int = 3):
9696
+ """Apply microcompact to an agent-specific message list (e.g. agent_messages filtered by role)."""
9697
+ tool_messages = [m for m in messages if m.get("role") == "tool"]
9698
+ if len(tool_messages) > keep_recent:
9699
+ kept = tool_messages[-keep_recent:]
9700
+ keep_ids = {id(x) for x in kept}
9701
+ for msg in messages:
9702
+ if msg.get("role") == "tool" and id(msg) not in keep_ids:
9703
+ content = msg.get("content", "")
9704
+ if isinstance(content, str) and len(content) > 120:
9705
+ msg["content"] = "[cleared by microcompact]"
9706
+ assistant_indices = [i for i, m in enumerate(messages) if m.get("role") == "assistant"]
9707
+ if len(assistant_indices) <= keep_recent:
9708
+ return
9709
+ cutoff = assistant_indices[-keep_recent]
9710
+ for i in range(cutoff):
9711
+ msg = messages[i]
9712
+ if msg.get("role") == "user" and isinstance(msg.get("content"), list):
9713
+ changed = False
9714
+ new_content = []
9715
+ for block in msg["content"]:
9716
+ if isinstance(block, dict) and block.get("type") == "tool_result":
9717
+ block_content = block.get("content", "")
9718
+ if isinstance(block_content, str) and len(block_content) > 120:
9719
+ tool_id = block.get("tool_use_id", "")
9720
+ tool_name = self._find_tool_name_by_id(messages[:i], tool_id) or "tool"
9721
+ new_content.append({
9722
+ "type": "tool_result",
9723
+ "tool_use_id": tool_id,
9724
+ "content": f"[Previous: used {tool_name}]",
9725
+ })
9726
+ changed = True
9727
+ else:
9728
+ new_content.append(block)
9729
+ else:
9730
+ new_content.append(block)
9731
+ if changed:
9732
+ msg["content"] = new_content
9420
9733
 
9421
9734
  def _estimate_messages_tokens(self, rows: list[dict]) -> int:
9422
9735
  try:
@@ -10090,6 +10403,17 @@ class SessionState:
10090
10403
  return data.decode("latin-1", errors="ignore")
10091
10404
 
10092
10405
  def _extract_pdf_text(self, pdf_path: Path) -> str:
10406
+ # 优先使用 pdfminer.six(纯 Python,无外部依赖)
10407
+ try:
10408
+ from pdfminer.high_level import extract_text
10409
+ text = extract_text(str(pdf_path))
10410
+ if text and text.strip():
10411
+ return text.strip()
10412
+ except ImportError:
10413
+ pass
10414
+ except Exception:
10415
+ pass
10416
+ # 降级:pdftotext CLI
10093
10417
  tool = shutil.which("pdftotext")
10094
10418
  if tool:
10095
10419
  try:
@@ -10103,6 +10427,7 @@ class SessionState:
10103
10427
  return r.stdout.strip()
10104
10428
  except Exception:
10105
10429
  pass
10430
+ # 最终降级:regex 提取
10106
10431
  try:
10107
10432
  raw = pdf_path.read_bytes()
10108
10433
  text = raw.decode("latin-1", errors="ignore")
@@ -10435,6 +10760,16 @@ class SessionState:
10435
10760
  lines.append(chunk)
10436
10761
  lines.append("</uploaded_excerpt>")
10437
10762
  remaining -= len(chunk)
10763
+ # 提示模型可直接读取 .parsed.md 文件获取完整解析文本
10764
+ item_kind = item.get("kind", "file")
10765
+ if item_kind not in ("text", "code"):
10766
+ wp = item.get("workspace_path", "")
10767
+ if wp:
10768
+ from pathlib import PurePosixPath
10769
+ stem = PurePosixPath(wp).stem
10770
+ parent = str(PurePosixPath(wp).parent)
10771
+ parsed_rel = f"{parent}/{stem}.parsed.md" if parent != "." else f"{stem}.parsed.md"
10772
+ lines.append(f" (parsed text available at: {parsed_rel} — use read_file to access full content)")
10438
10773
  return "\n".join(lines)
10439
10774
 
10440
10775
  def add_upload(self, filename: str, raw: bytes, mime: str = "") -> dict:
@@ -10445,10 +10780,42 @@ class SessionState:
10445
10780
  stored.write_bytes(raw)
10446
10781
  ext = stored.suffix.lower()
10447
10782
  text_like_ext = {
10448
- ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".c", ".cc", ".cpp", ".h", ".hpp",
10449
- ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".sql", ".html",
10450
- ".css", ".json", ".yaml", ".yml", ".xml", ".toml", ".ini", ".cfg", ".md", ".txt",
10451
- ".ipynb", ".vue", ".svelte", ".cs", ".m", ".mm", ".r", ".pl", ".csv",
10783
+ ".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".java", ".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl",
10784
+ ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".kts", ".scala", ".sh", ".bash", ".zsh", ".fish", ".sql", ".html", ".htm",
10785
+ ".css", ".sass", ".scss", ".less", ".styl", ".json", ".jsonc", ".yaml", ".yml", ".xml", ".xsd", ".xsl",
10786
+ ".toml", ".ini", ".cfg", ".conf", ".env", ".properties", ".md", ".mdx", ".txt", ".rst", ".log",
10787
+ ".ipynb", ".vue", ".svelte", ".cs", ".m", ".mm", ".r", ".pl", ".pm", ".csv", ".tsv",
10788
+ # Fortran
10789
+ ".f", ".f90", ".f95", ".f03", ".f08", ".for", ".fpp",
10790
+ # 更多语言
10791
+ ".zig", ".nim", ".v", ".d", ".ada", ".adb", ".ads",
10792
+ ".asm", ".s",
10793
+ ".bas", ".vb", ".vbs", ".vba",
10794
+ ".bat", ".cmd", ".ps1", ".psm1",
10795
+ ".clj", ".cljs", ".cljc", ".edn",
10796
+ ".coffee", ".cr", ".dart",
10797
+ ".dockerfile",
10798
+ ".erl", ".hrl", ".ex", ".exs",
10799
+ ".fs", ".fsi", ".fsx",
10800
+ ".gradle", ".groovy", ".gvy",
10801
+ ".hs", ".lhs",
10802
+ ".jl", ".lua",
10803
+ ".lisp", ".cl", ".el", ".scm", ".rkt",
10804
+ ".mk", ".cmake",
10805
+ ".ml", ".mli", ".nix",
10806
+ ".pas", ".pp", ".inc",
10807
+ ".pde", ".ino",
10808
+ ".proto", ".purs",
10809
+ ".raku", ".p6",
10810
+ ".sol",
10811
+ ".sv", ".svh", ".vh", ".vhd", ".vhdl",
10812
+ ".tcl", ".tk",
10813
+ ".tf", ".tfvars", ".hcl",
10814
+ ".tex", ".bib", ".sty", ".cls", ".typ",
10815
+ ".wat",
10816
+ ".diff", ".patch",
10817
+ ".graphql", ".gql",
10818
+ ".prisma", ".svg",
10452
10819
  }
10453
10820
  parsed_excerpt = ""
10454
10821
  kind = "binary"
@@ -10489,6 +10856,15 @@ class SessionState:
10489
10856
  workspace_target = self._upload_workspace_target(safe_name)
10490
10857
  workspace_target.parent.mkdir(parents=True, exist_ok=True)
10491
10858
  workspace_target.write_bytes(raw)
10859
+ # 当 parsed_excerpt 非空且原始文件不是纯文本时,保存解析结果为 .parsed.md
10860
+ parsed_target: Path | None = None
10861
+ if parsed_excerpt and kind not in ("text", "code"):
10862
+ parsed_target = workspace_target.parent / f"{workspace_target.stem}.parsed.md"
10863
+ try:
10864
+ header = f"# {safe_name}\n\n> Auto-parsed from uploaded {kind} file ({len(raw)} bytes)\n\n"
10865
+ parsed_target.write_text(header + parsed_excerpt, encoding="utf-8")
10866
+ except Exception:
10867
+ parsed_target = None # 解析文件保存失败不影响主流程
10492
10868
  workspace_rel = self._session_rel(workspace_target)
10493
10869
  meta = {
10494
10870
  "id": upload_id,
@@ -10507,6 +10883,9 @@ class SessionState:
10507
10883
  self.uploads = self.uploads[-80:]
10508
10884
  self.updated_at = now_ts()
10509
10885
  self._persist()
10886
+ if parsed_excerpt:
10887
+ bb_content = f"[upload:{safe_name}]\n{trim(parsed_excerpt, BLACKBOARD_MAX_TEXT - 200)}"
10888
+ self._blackboard_append_section("research_notes", "system", bb_content)
10510
10889
  self._emit(
10511
10890
  "upload",
10512
10891
  {
@@ -10514,7 +10893,10 @@ class SessionState:
10514
10893
  "workspace_path": workspace_rel,
10515
10894
  "kind": kind,
10516
10895
  "size": len(raw),
10517
- "summary": f"upload: {safe_name} -> {workspace_rel}",
10896
+ "summary": (
10897
+ f"upload: {safe_name} -> {workspace_rel}"
10898
+ + (f" (parsed text: {self._session_rel(parsed_target)})" if parsed_target else "")
10899
+ ),
10518
10900
  "preview": trim(parsed_excerpt, 500),
10519
10901
  },
10520
10902
  )
@@ -10611,6 +10993,12 @@ class SessionState:
10611
10993
  "completed",
10612
10994
  "finished",
10613
10995
  "all set",
10996
+ # 明确表示拒绝/无法完成也应视为终结
10997
+ "抱歉",
10998
+ "sorry",
10999
+ "无法",
11000
+ "cannot",
11001
+ "unable",
10614
11002
  ]
10615
11003
  if any(x in t for x in done_markers):
10616
11004
  return False
@@ -10692,6 +11080,17 @@ class SessionState:
10692
11080
  "that's all",
10693
11081
  "that is all",
10694
11082
  "as requested",
11083
+ # 明确表示无法完成的标记
11084
+ "抱歉,我无法",
11085
+ "无法直接获取",
11086
+ "无法完成",
11087
+ "cannot be done",
11088
+ "unable to",
11089
+ "not possible",
11090
+ "建议您通过",
11091
+ "建议你通过",
11092
+ "i cannot",
11093
+ "i'm unable",
10695
11094
  ]
10696
11095
  return any(x in t for x in done_markers)
10697
11096
 
@@ -11098,7 +11497,7 @@ class SessionState:
11098
11497
  return False
11099
11498
  if not str(thinking_text or "").strip():
11100
11499
  return False
11101
- threshold = int(max(1, AGENT_MAX_OUTPUT_TOKENS) * float(THINKING_BUDGET_FORCE_RATIO))
11500
+ threshold = int(max(1, self.max_output_tokens) * float(THINKING_BUDGET_FORCE_RATIO))
11102
11501
  return int(output_tokens or 0) >= max(1, threshold)
11103
11502
 
11104
11503
  def _is_thinking_only_dead_turn(self, text: str, thinking_text: str, tool_calls: list | None = None) -> bool:
@@ -12229,12 +12628,48 @@ class SessionState:
12229
12628
  fp = self._session_path(rel)
12230
12629
  content = fp.read_text(encoding="utf-8")
12231
12630
  if old_text not in content:
12232
- return f"Error: text not found in {rel}"
12631
+ diag = self._edit_mismatch_diagnostic(content, old_text)
12632
+ return f"Error: text not found in {rel}. {diag}"
12233
12633
  fp.write_text(content.replace(old_text, new_text, 1), encoding="utf-8")
12234
12634
  return f"Edited {rel}"
12235
12635
  except Exception as exc:
12236
12636
  return f"Error: {type(exc).__name__}: {exc}"
12237
12637
 
12638
+ def _edit_mismatch_diagnostic(self, content: str, old_text: str) -> str:
12639
+ """为 edit_file 匹配失败提供诊断信息"""
12640
+ lines = content.splitlines()
12641
+ first_line = old_text.strip().splitlines()[0].strip() if old_text.strip() else ""
12642
+ if not first_line:
12643
+ return "The old_text is empty or whitespace-only."
12644
+ # 搜索 old_text 的第一行在文件中的位置
12645
+ matches = []
12646
+ for i, line in enumerate(lines, 1):
12647
+ if first_line in line:
12648
+ matches.append(i)
12649
+ if matches:
12650
+ loc = ", ".join(str(m) for m in matches[:5])
12651
+ return (
12652
+ f"The first line of old_text was found at line(s) {loc}, "
12653
+ f"but the full multi-line match failed. "
12654
+ f"Likely cause: whitespace or indentation mismatch. "
12655
+ f"Tip: use read_file to get the exact content, then copy it precisely."
12656
+ )
12657
+ # 尝试空白规范化匹配
12658
+ norm_first = " ".join(first_line.split())
12659
+ for i, line in enumerate(lines, 1):
12660
+ norm_line = " ".join(line.split())
12661
+ if norm_first and norm_first in norm_line:
12662
+ return (
12663
+ f"A whitespace-normalized partial match was found near line {i}. "
12664
+ f"The old_text likely has wrong indentation or extra/missing spaces. "
12665
+ f"Use read_file to get exact content."
12666
+ )
12667
+ return (
12668
+ f"No match found. The file has {len(lines)} lines. "
12669
+ f"The old_text first line '{first_line[:60]}' does not appear in the file. "
12670
+ f"The content may have changed since last read. Use read_file to refresh."
12671
+ )
12672
+
12238
12673
  def _write_global_skill(self, args: dict) -> str:
12239
12674
  rel_raw = str(args.get("path", "") or "").strip().replace("\\", "/")
12240
12675
  if not rel_raw:
@@ -12861,15 +13296,13 @@ class SessionState:
12861
13296
  bb = board if isinstance(board, dict) else self._ensure_blackboard()
12862
13297
  if bool(self.runtime_goal_reset_pending):
12863
13298
  return False, "goal-reset-pending"
12864
- approval = bb.get("approval", {}) if isinstance(bb.get("approval"), dict) else {}
12865
- if not bool(approval.get("approved", False)):
12866
- return False, "approval-false"
12867
- if self._approval_is_stale_for_latest_user(bb, latest_user_ts=latest_user_ts):
12868
- return False, "approval-stale-new-user-input"
12869
- if self._manager_has_error_log(bb):
12870
- return False, "blocking-error-log"
12871
- if not self._reviewer_final_summary_ready(bb):
12872
- return False, "reviewer-summary-missing"
13299
+ # Project todo gate: coding tasks must pass compile + test
13300
+ profile = self._ensure_blackboard_task_profile(bb)
13301
+ task_type = str(profile.get("task_type", "general") or "general")
13302
+ if task_type in ("simple_code", "engineering"):
13303
+ for todo in bb.get("project_todos", []):
13304
+ if todo.get("category") in ("compile_test", "min_test") and todo.get("status") != "completed":
13305
+ return False, f"project-todo-incomplete:{todo.get('category', '')}"
12873
13306
  return True, "ok"
12874
13307
 
12875
13308
  def _invalidate_stale_approval_if_needed(
@@ -13322,9 +13755,6 @@ class SessionState:
13322
13755
  "decomposer_output": trim(raw_text, 2000),
13323
13756
  }
13324
13757
  wd = self._normalize_watchdog_state(board.get("watchdog", {}))
13325
- wd["trigger_count"] = max(0, int(wd.get("trigger_count", 0) or 0)) + 1
13326
- wd["last_trigger_reason"] = trim(str(reason or "").strip(), 200)
13327
- wd["last_trigger_ts"] = float(now_ts())
13328
13758
  wd["intent_no_tool_streak"] = 0
13329
13759
  wd["repeat_no_tool_streak"] = 0
13330
13760
  board["watchdog"] = wd
@@ -13353,6 +13783,39 @@ class SessionState:
13353
13783
  )
13354
13784
  return True
13355
13785
 
13786
+ def _watchdog_escalate_to_single_developer(self, board: dict, *, reason: str = ""):
13787
+ """Watchdog 连续 stall 升级:强制降级到 Single+Developer 模式。"""
13788
+ bb = board if isinstance(board, dict) else self._ensure_blackboard()
13789
+ self.runtime_execution_mode = EXECUTION_MODE_SINGLE
13790
+ self.runtime_participants = ["developer"]
13791
+ self.runtime_assigned_expert = "developer"
13792
+ dq = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", {}))
13793
+ dq["active"] = False
13794
+ bb["decomposition_queue"] = dq
13795
+ profile = self._ensure_blackboard_task_profile(bb)
13796
+ profile["execution_mode"] = EXECUTION_MODE_SINGLE
13797
+ profile["participants"] = ["developer"]
13798
+ profile["assigned_expert"] = "developer"
13799
+ bb["task_profile"] = profile
13800
+ self.blackboard = bb
13801
+ self._blackboard_touch()
13802
+ self._blackboard_history(
13803
+ "manager",
13804
+ trim(
13805
+ f"watchdog escalation: forced Single+Developer (trigger_count={int(bb.get('watchdog', {}).get('trigger_count', 0))}, reason={reason})",
13806
+ 520,
13807
+ ),
13808
+ )
13809
+ self._emit(
13810
+ "status",
13811
+ {
13812
+ "summary": (
13813
+ "watchdog escalation: multi-agent stall detected, "
13814
+ "downgrading to Single+Developer mode"
13815
+ )
13816
+ },
13817
+ )
13818
+
13356
13819
  def _watchdog_pick_executor_route(self, board: dict | None = None) -> tuple[dict, dict] | None:
13357
13820
  bb = board if isinstance(board, dict) else self._ensure_blackboard()
13358
13821
  dq = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", {}))
@@ -13564,13 +14027,22 @@ class SessionState:
13564
14027
  except Exception:
13565
14028
  last_trigger_ts = 0.0
13566
14029
  if now_ts() - last_trigger_ts >= 1.0:
13567
- triggered = self._watchdog_activate_decomposition(
13568
- bb,
13569
- reason=trigger_reason,
13570
- role=role,
13571
- step=step,
13572
- pinned_selection=pinned_selection,
13573
- )
14030
+ wd["trigger_count"] = max(0, int(wd.get("trigger_count", 0) or 0)) + 1
14031
+ wd["last_trigger_reason"] = trim(str(trigger_reason or "").strip(), 200)
14032
+ wd["last_trigger_ts"] = float(now_ts())
14033
+ bb["watchdog"] = wd
14034
+ self.blackboard = bb
14035
+ self._blackboard_touch()
14036
+ if int(wd["trigger_count"]) >= 2:
14037
+ self._watchdog_escalate_to_single_developer(bb, reason=trigger_reason)
14038
+ else:
14039
+ triggered = self._watchdog_activate_decomposition(
14040
+ bb,
14041
+ reason=trigger_reason,
14042
+ role=role,
14043
+ step=step,
14044
+ pinned_selection=pinned_selection,
14045
+ )
13574
14046
  bb = self._ensure_blackboard()
13575
14047
  bb["watchdog"] = self._normalize_watchdog_state(bb.get("watchdog", wd))
13576
14048
  bb["decomposition_queue"] = self._normalize_decomposition_queue_state(bb.get("decomposition_queue", dq))
@@ -13705,6 +14177,7 @@ class SessionState:
13705
14177
  "text": "",
13706
14178
  "ts": 0.0,
13707
14179
  },
14180
+ "project_todos": [],
13708
14181
  "watchdog": self._new_watchdog_state(),
13709
14182
  "decomposition_queue": self._new_decomposition_queue_state(),
13710
14183
  }
@@ -13856,6 +14329,24 @@ class SessionState:
13856
14329
  "change_count": max(1, int(item.get("change_count", 1) or 1)),
13857
14330
  }
13858
14331
  board["code_artifacts"] = artifacts
14332
+ if not isinstance(bb_src_todos := src.get("project_todos"), list):
14333
+ board["project_todos"] = []
14334
+ else:
14335
+ clean_todos = []
14336
+ for pt in bb_src_todos[:20]:
14337
+ if not isinstance(pt, dict):
14338
+ continue
14339
+ clean_todos.append({
14340
+ "id": trim(str(pt.get("id", "") or ""), 20),
14341
+ "content": trim(str(pt.get("content", "") or ""), 400),
14342
+ "status": str(pt.get("status", "pending") or "pending") if str(pt.get("status", "pending") or "pending") in ("pending", "in_progress", "completed") else "pending",
14343
+ "category": trim(str(pt.get("category", "") or ""), 40),
14344
+ "created_at": float(pt.get("created_at", 0.0) or 0.0),
14345
+ "completed_at": float(pt.get("completed_at", 0.0) or 0.0) if pt.get("completed_at") else None,
14346
+ "completed_by": trim(str(pt.get("completed_by", "") or ""), 40),
14347
+ "evidence": trim(str(pt.get("evidence", "") or ""), 200),
14348
+ })
14349
+ board["project_todos"] = clean_todos
13859
14350
  board["watchdog"] = self._normalize_watchdog_state(src.get("watchdog", {}))
13860
14351
  board["decomposition_queue"] = self._normalize_decomposition_queue_state(
13861
14352
  src.get("decomposition_queue", {})
@@ -13876,6 +14367,7 @@ class SessionState:
13876
14367
  def _blackboard_reset_for_goal(self, goal: str):
13877
14368
  self.blackboard = self._new_blackboard(goal)
13878
14369
  self.manager_context = []
14370
+ self.agent_messages = [m for m in self.agent_messages if m.get("agent_role") != "manager"]
13879
14371
  self.manager_routes = []
13880
14372
  self._blackboard_history("manager", f"new goal accepted: {trim(goal, 300)}")
13881
14373
  self._sync_todos_from_blackboard(reason="goal-reset", board=self.blackboard)
@@ -14186,13 +14678,150 @@ class SessionState:
14186
14678
  row["activeForm"] = f"Pending ({label}): {text}"
14187
14679
  return rows
14188
14680
 
14681
+ # ── Project-based todo generation & status tracking ──────────────
14682
+
14683
+ def _generate_project_todos_from_profile(self, board: dict | None = None) -> list[dict]:
14684
+ bb = board if isinstance(board, dict) else self._ensure_blackboard()
14685
+ profile = self._ensure_blackboard_task_profile(bb)
14686
+ task_type = str(profile.get("task_type", "general") or "general")
14687
+ objective = trim(str(profile.get("direct_objective", "") or ""), 200)
14688
+
14689
+ if task_type == "simple_qa":
14690
+ return [{"content": f"回答: {objective}" if objective else "回答用户问题", "category": "implement"}]
14691
+
14692
+ if task_type in ("simple_code", "engineering"):
14693
+ return [
14694
+ {"content": "分析需求和项目结构", "category": "setup"},
14695
+ {"content": f"实现: {objective}" if objective else "实现编码任务", "category": "implement"},
14696
+ {"content": "编译/语法检查", "category": "compile_test"},
14697
+ {"content": "最小功能测试", "category": "min_test"},
14698
+ ]
14699
+
14700
+ if task_type == "research":
14701
+ return [
14702
+ {"content": f"调研: {objective}" if objective else "执行调研任务", "category": "implement"},
14703
+ {"content": "整理调研结果", "category": "review"},
14704
+ ]
14705
+
14706
+ return [{"content": f"执行: {objective}" if objective else "执行任务", "category": "implement"}]
14707
+
14708
+ def _init_project_todos(self, board: dict | None = None):
14709
+ bb = board if isinstance(board, dict) else self._ensure_blackboard()
14710
+ if bb.get("project_todos"):
14711
+ return
14712
+ raw = self._generate_project_todos_from_profile(bb)
14713
+ bb["project_todos"] = [
14714
+ {
14715
+ "id": f"pt:{i:03d}",
14716
+ "content": t["content"],
14717
+ "status": "pending",
14718
+ "category": t["category"],
14719
+ "created_at": float(now_ts()),
14720
+ "completed_at": None,
14721
+ "completed_by": "",
14722
+ "evidence": "",
14723
+ }
14724
+ for i, t in enumerate(raw)
14725
+ ]
14726
+ self.blackboard = bb
14727
+ self._blackboard_touch()
14728
+
14729
+ def _has_compile_pass_evidence(self, board: dict | None = None) -> bool:
14730
+ bb = board if isinstance(board, dict) else self._ensure_blackboard()
14731
+ logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
14732
+ if not logs:
14733
+ return False
14734
+ positive = ("compiled successfully", "build successful", "0 error", "编译成功",
14735
+ "syntax ok", "no errors", "build succeeded", "compilation successful")
14736
+ negative = ("error:", "fatal error", "syntax error", "compile error", "build failed")
14737
+ for entry in reversed(logs[-6:]):
14738
+ txt = str((entry or {}).get("content", "") or "").lower() if isinstance(entry, dict) else str(entry or "").lower()
14739
+ if not txt:
14740
+ continue
14741
+ if any(neg in txt for neg in negative):
14742
+ continue
14743
+ if any(pos in txt for pos in positive):
14744
+ return True
14745
+ return False
14746
+
14747
+ def _has_test_pass_evidence(self, board: dict | None = None) -> bool:
14748
+ bb = board if isinstance(board, dict) else self._ensure_blackboard()
14749
+ logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
14750
+ feedback = bb.get("review_feedback", []) if isinstance(bb.get("review_feedback"), list) else []
14751
+ positive = ("test passed", "tests passed", "测试通过", "运行正常",
14752
+ "all tests pass", "ok", "passed", "test succeeded")
14753
+ negative = ("failed", "error", "failure", "测试失败")
14754
+ combined = list(logs[-6:]) + list(feedback[-4:])
14755
+ for entry in reversed(combined[-8:]):
14756
+ txt = str((entry or {}).get("content", "") or "").lower() if isinstance(entry, dict) else str(entry or "").lower()
14757
+ if not txt:
14758
+ continue
14759
+ if any(neg in txt for neg in negative):
14760
+ continue
14761
+ if any(pos in txt for pos in positive):
14762
+ return True
14763
+ return False
14764
+
14765
+ def _update_project_todo_status(self, board: dict | None = None):
14766
+ bb = board if isinstance(board, dict) else self._ensure_blackboard()
14767
+ todos = bb.get("project_todos", [])
14768
+ if not todos:
14769
+ return
14770
+ code_count = len(bb.get("code_artifacts", {}) or {})
14771
+ research_count = len(bb.get("research_notes", []) or [])
14772
+ feedback_pass = self._manager_feedback_passed_from_blackboard(bb)
14773
+
14774
+ for todo in todos:
14775
+ if todo.get("status") == "completed":
14776
+ continue
14777
+ cat = todo.get("category", "")
14778
+ if cat == "setup" and (research_count > 0 or code_count > 0):
14779
+ todo.update(status="completed", completed_at=float(now_ts()), evidence="结构已分析")
14780
+ elif cat == "implement" and code_count > 0:
14781
+ todo.update(status="completed", completed_at=float(now_ts()),
14782
+ completed_by="developer", evidence=f"{code_count} 文件已产出")
14783
+ elif cat == "compile_test" and self._has_compile_pass_evidence(bb):
14784
+ todo.update(status="completed", completed_at=float(now_ts()), evidence="编译通过")
14785
+ elif cat == "min_test" and self._has_test_pass_evidence(bb):
14786
+ todo.update(status="completed", completed_at=float(now_ts()), evidence="测试通过")
14787
+ elif cat == "review" and feedback_pass:
14788
+ todo.update(status="completed", completed_at=float(now_ts()), evidence="审查通过")
14789
+
14790
+ if not any(t.get("status") == "in_progress" for t in todos):
14791
+ for t in todos:
14792
+ if t.get("status") == "pending":
14793
+ t["status"] = "in_progress"
14794
+ break
14795
+
14796
+ bb["project_todos"] = todos
14797
+ self.blackboard = bb
14798
+
14799
+ def _todo_project_rows_from_blackboard(self, board: dict | None = None) -> list[dict]:
14800
+ bb = board if isinstance(board, dict) else self._ensure_blackboard()
14801
+ todos = bb.get("project_todos", [])
14802
+ if not todos:
14803
+ return self._todo_owner_rows_from_blackboard(bb)
14804
+ rows = []
14805
+ for todo in todos:
14806
+ s = todo.get("status", "pending")
14807
+ c = todo.get("content", "")
14808
+ ev = todo.get("evidence", "")
14809
+ af = {
14810
+ "in_progress": f"Working on: {c}",
14811
+ "completed": f"Done: {c}" + (f" ({ev})" if ev else ""),
14812
+ }.get(s, f"Pending: {c}")
14813
+ rows.append({"key": f"bb:proj:{todo.get('id', '')}", "content": c, "status": s, "activeForm": af})
14814
+ return rows
14815
+
14189
14816
  def _sync_todos_from_blackboard(self, reason: str = "", board: dict | None = None):
14190
14817
  if bool(self.runtime_reclassify_required):
14191
14818
  return
14192
14819
  if not self._is_multi_agent_mode():
14193
14820
  return
14194
14821
  bb = board if isinstance(board, dict) else self._ensure_blackboard()
14195
- system_rows = self._todo_owner_rows_from_blackboard(bb)
14822
+ self._init_project_todos(bb)
14823
+ self._update_project_todo_status(bb)
14824
+ system_rows = self._todo_project_rows_from_blackboard(bb)
14196
14825
  existing = self.todo.snapshot()
14197
14826
  non_system_rows: list[dict] = []
14198
14827
  for row in existing:
@@ -14200,18 +14829,17 @@ class SessionState:
14200
14829
  continue
14201
14830
  key = str(row.get("key", "") or "").strip()
14202
14831
  owner = str(row.get("owner", "") or "").strip().lower()
14203
- if key.startswith("bb:owner:") or key.startswith("bb:node:") or owner in {"manager", "explorer", "developer", "reviewer"}:
14832
+ if key.startswith(("bb:owner:", "bb:node:", "bb:proj:")) or owner in {"manager", "explorer", "developer", "reviewer"}:
14204
14833
  continue
14205
14834
  non_system_rows.append(dict(row))
14206
14835
  remaining_cap = max(0, 20 - len(system_rows))
14207
14836
  merged = list(system_rows) + non_system_rows[:remaining_cap]
14208
14837
  try:
14209
14838
  todo_out = self.todo.update(merged)
14210
- except Exception as exc:
14211
- self._emit("status", {"summary": f"owner todo sync skipped: {trim(str(exc), 180)}"})
14839
+ except Exception:
14212
14840
  return
14213
14841
  if todo_out != "No todo changes." and reason:
14214
- self._emit("status", {"summary": f"owner todos synced ({trim(reason, 120)})"})
14842
+ self._emit("status", {"summary": f"project todos synced ({trim(reason, 120)})"})
14215
14843
 
14216
14844
  def _blackboard_set_status(self, status: str, note: str = ""):
14217
14845
  board = self._ensure_blackboard()
@@ -14233,6 +14861,43 @@ class SessionState:
14233
14861
  self._blackboard_touch()
14234
14862
  self._sync_todos_from_blackboard(reason="approved", board=board)
14235
14863
 
14864
+ def _auto_summary_on_finish(self) -> str:
14865
+ """Generate concise summary from blackboard state when run ends."""
14866
+ bb = self._ensure_blackboard()
14867
+ artifacts = bb.get("code_artifacts", {}) if isinstance(bb.get("code_artifacts"), dict) else {}
14868
+ logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
14869
+ feedback = bb.get("review_feedback", []) if isinstance(bb.get("review_feedback"), list) else []
14870
+ summary_parts = []
14871
+ if artifacts:
14872
+ file_list = ", ".join(list(artifacts.keys())[:10])
14873
+ summary_parts.append(f"Modified files: {file_list}")
14874
+ if feedback:
14875
+ last_fb = feedback[-1] if feedback else {}
14876
+ fb_content = str(last_fb.get("content", "") or "") if isinstance(last_fb, dict) else str(last_fb)
14877
+ if fb_content:
14878
+ summary_parts.append(f"Review: {trim(fb_content, 200)}")
14879
+ if logs:
14880
+ recent_logs = logs[-3:]
14881
+ log_strs = []
14882
+ for log_entry in recent_logs:
14883
+ if isinstance(log_entry, dict):
14884
+ log_strs.append(trim(str(log_entry.get("content", "") or ""), 80))
14885
+ elif isinstance(log_entry, str):
14886
+ log_strs.append(trim(log_entry, 80))
14887
+ if log_strs:
14888
+ summary_parts.append(f"Logs: {'; '.join(log_strs)}")
14889
+ summary = "; ".join(summary_parts) or "Task completed."
14890
+ bb["status"] = "COMPLETED"
14891
+ bb["approval"] = {
14892
+ "approved": True,
14893
+ "by": "auto",
14894
+ "note": summary,
14895
+ "ts": float(now_ts()),
14896
+ }
14897
+ self._blackboard_touch()
14898
+ self._emit("status", {"summary": f"[auto-summary] {trim(summary, 400)}"})
14899
+ return summary
14900
+
14236
14901
  def _blackboard_read_state_markdown(self, max_items: int = 6) -> str:
14237
14902
  board = self._ensure_blackboard()
14238
14903
  profile = self._ensure_blackboard_task_profile(board)
@@ -14347,6 +15012,23 @@ class SessionState:
14347
15012
  lines.append("- (none)")
14348
15013
  _render_tail("Recent Execution Logs", board.get("execution_logs", []))
14349
15014
  _render_tail("Recent Review Feedback", board.get("review_feedback", []))
15015
+
15016
+ proj_todos = board.get("project_todos", [])
15017
+ if proj_todos:
15018
+ lines.append("\n### Project Tasks")
15019
+ for pt in proj_todos:
15020
+ s = pt.get("status", "pending")
15021
+ c = trim(str(pt.get("content", "") or ""), 200)
15022
+ ev = trim(str(pt.get("evidence", "") or ""), 100)
15023
+ if s == "completed":
15024
+ mark = "[x]"
15025
+ elif s == "in_progress":
15026
+ mark = "[>]"
15027
+ else:
15028
+ mark = "[ ]"
15029
+ suffix = f" — {ev}" if ev else ""
15030
+ lines.append(f"- {mark} {c}{suffix}")
15031
+
14350
15032
  return "\n".join(lines)
14351
15033
 
14352
15034
  def _manager_route_tools(self) -> list[dict]:
@@ -14612,7 +15294,7 @@ class SessionState:
14612
15294
  "judgement": trim(str(profile.get("reason", "") or "manager fallback classification"), 200),
14613
15295
  "round_budget": int(policy.get("round_budget", profile.get("round_budget", self.max_agent_rounds)) or 0),
14614
15296
  "direct_objective": trim(str(profile.get("direct_objective", "") or ""), 800),
14615
- "execution_mode": str(policy.get("execution_mode", EXECUTION_MODE_SYNC)),
15297
+ "execution_mode": (EXECUTION_MODE_SINGLE if normalize_execution_mode(self.execution_mode, default="") == EXECUTION_MODE_SINGLE else str(policy.get("execution_mode", EXECUTION_MODE_SYNC))),
14616
15298
  "participants": participants,
14617
15299
  "assigned_expert": assigned,
14618
15300
  "requires_user_confirmation": bool(requires_confirmation),
@@ -14622,7 +15304,7 @@ class SessionState:
14622
15304
  }
14623
15305
 
14624
15306
  def _manager_classification_system_prompt(self) -> str:
14625
- return (
15307
+ base = (
14626
15308
  "You are Manager. Classify the latest user request by semantic intent, not by keyword templates. "
14627
15309
  "Decide whether this latest turn should inherit the previous blackboard/task state. "
14628
15310
  "Set inherit_previous_state=true only for genuine follow-up/continuation/refinement of the same ongoing work; "
@@ -14645,6 +15327,12 @@ class SessionState:
14645
15327
  "Use low confidence only when semantic ambiguity is substantial, then set low_confidence_reason briefly. "
14646
15328
  f"{model_language_instruction(self.ui_language)}"
14647
15329
  )
15330
+ if normalize_execution_mode(self.execution_mode, default="") == EXECUTION_MODE_SINGLE:
15331
+ base += (
15332
+ " NOTE: User has configured Single-agent mode. "
15333
+ "Favor level 1-2 for straightforward tasks; only assign level 3+ when genuine multi-step complexity demands it."
15334
+ )
15335
+ return base
14648
15336
 
14649
15337
  def _apply_runtime_task_decision(self, goal_text: str, decision: dict):
14650
15338
  row = dict(decision or {})
@@ -14666,7 +15354,12 @@ class SessionState:
14666
15354
  if level not in TASK_LEVEL_CHOICES:
14667
15355
  level = 3
14668
15356
  policy = dict(TASK_LEVEL_POLICIES.get(level, TASK_LEVEL_POLICIES[3]))
14669
- mode = str(policy.get("execution_mode", EXECUTION_MODE_SYNC))
15357
+ policy_mode = str(policy.get("execution_mode", EXECUTION_MODE_SYNC))
15358
+ config_mode = normalize_execution_mode(self.execution_mode, default="")
15359
+ if config_mode == EXECUTION_MODE_SINGLE:
15360
+ mode = EXECUTION_MODE_SINGLE
15361
+ else:
15362
+ mode = policy_mode
14670
15363
  assigned = self._sanitize_agent_role(
14671
15364
  row.get("assigned_expert", policy.get("assigned_expert", "developer"))
14672
15365
  ) or self._sanitize_agent_role(policy.get("assigned_expert", "developer")) or "developer"
@@ -14697,6 +15390,9 @@ class SessionState:
14697
15390
  except Exception:
14698
15391
  budget_raw = int(policy.get("round_budget", self.max_agent_rounds) or self.max_agent_rounds)
14699
15392
  round_budget = max(1, min(int(self.max_agent_rounds or MAX_AGENT_ROUNDS), int(budget_raw)))
15393
+ if mode == EXECUTION_MODE_SINGLE and policy_mode != EXECUTION_MODE_SINGLE:
15394
+ policy_budget = int(policy.get("round_budget", 10) or 10)
15395
+ round_budget = min(round_budget, max(4, policy_budget // 2))
14700
15396
  requires_confirmation = bool(row.get("requires_user_confirmation", policy.get("requires_user_confirmation", False)))
14701
15397
  if level == 5 and self._looks_like_positive_confirmation(goal_text):
14702
15398
  requires_confirmation = False
@@ -14875,8 +15571,14 @@ class SessionState:
14875
15571
  "When user preference is clear, prioritize it over your default plan. "
14876
15572
  "Remember: budget controls internal thought depth/round compactness, not early stop messaging. "
14877
15573
  "Also decide inherit_previous_state by semantic continuity with prior blackboard state. "
14878
- "If this is pure continuation, keep current runtime policy unchanged."
15574
+ "If this is pure continuation, keep current runtime policy unchanged.\n"
15575
+ f"User configured execution mode: {self.execution_mode}\n"
14879
15576
  )
15577
+ if normalize_execution_mode(self.execution_mode, default="") == EXECUTION_MODE_SINGLE:
15578
+ prompt += (
15579
+ "IMPORTANT: User has configured Single-agent mode. "
15580
+ "Prefer level 1-2 for simple tasks. Only escalate to level 3+ if truly complex.\n"
15581
+ )
14880
15582
  with self.lock:
14881
15583
  self.current_phase = "manager:classify:model-call"
14882
15584
  self.current_tool_name = ""
@@ -14959,6 +15661,22 @@ class SessionState:
14959
15661
  self._apply_runtime_task_decision(goal, decision)
14960
15662
  return dict(decision or {})
14961
15663
 
15664
+ def _project_todo_hint_for_manager(self) -> str:
15665
+ bb = self._ensure_blackboard()
15666
+ todos = bb.get("project_todos", [])
15667
+ if not todos:
15668
+ return ""
15669
+ pending = [t for t in todos if t.get("status") != "completed"]
15670
+ if not pending:
15671
+ return "All project tasks completed. Route to finish. "
15672
+ cur = pending[0]
15673
+ cat = cur.get("category", "")
15674
+ if cat == "compile_test":
15675
+ return "NEXT: compile/syntax check required. Route to Developer for build. "
15676
+ if cat == "min_test":
15677
+ return "NEXT: minimal test required. Route to Developer to run tests. "
15678
+ return f"NEXT: {trim(str(cur.get('content', '') or ''), 120)}. "
15679
+
14962
15680
  def _manager_system_prompt(self) -> str:
14963
15681
  board = self._ensure_blackboard()
14964
15682
  profile = self._ensure_blackboard_task_profile(board)
@@ -14966,41 +15684,30 @@ class SessionState:
14966
15684
  budget = self._blackboard_round_budget(board)
14967
15685
  level = int(profile.get("task_level", self.runtime_task_level or 0) or 0)
14968
15686
  mode = normalize_execution_mode(profile.get("execution_mode", self._effective_execution_mode()), default=self._effective_execution_mode())
14969
- scale_preference = str(profile.get("scale_preference", self.runtime_scale_preference) or "balanced")
14970
- if scale_preference not in TASK_SCALE_PREFERENCES:
14971
- scale_preference = "balanced"
14972
- participants = profile.get("participants", self.runtime_participants)
14973
- if not isinstance(participants, list):
14974
- participants = []
14975
- participant_text = ",".join(
14976
- [role for role in [self._sanitize_agent_role(x) for x in participants] if role][:3]
14977
- ) or "-"
15687
+ task_type = str(profile.get("task_type", "general") or "general").strip().lower()
15688
+ coding_hint = ""
15689
+ if task_type in ("simple_code", "engineering"):
15690
+ coding_hint = (
15691
+ "CODING TASK: skip lengthy exploration/design phases. "
15692
+ "Route to Developer early for implementation. "
15693
+ "Explorer should only be used for specific file/API lookups, not broad analysis. "
15694
+ )
15695
+ project_todo_hint = self._project_todo_hint_for_manager()
14978
15696
  return (
14979
- "You are Manager in a blackboard-driven multi-agent coding system. "
14980
- "Do not write code and do not call worker tools directly. "
14981
- f"Session absolute writable root is {self.files_root}; instruct workers to use relative paths, "
14982
- "and treat '/workspace/...' only as a virtual alias for tool arguments. "
14983
- "Read blackboard state and delegate one short next timeslice only. "
14984
- "Use route_to_next_agent exactly once each turn. "
14985
- "Before delegating, classify task level/type/complexity by your own judgement, "
14986
- "and include task_level/task_type/complexity/judgement/(optional)round_budget/direct_objective "
14987
- "in route_to_next_agent arguments whenever possible. "
14988
- "When concrete execution is required, set is_mandatory=true so worker must call at least one tool "
14989
- "instead of replying with suggestion-only text. "
14990
- "Round budget is an internal cadence control to reduce overthinking and idle loops. "
14991
- "Never use budget as an early-stop reason shown to user before task completion. "
14992
- "Decision policy: missing facts/API -> explorer; implementation/update -> developer; "
14993
- "verification/gap check -> reviewer; only choose finish when review is approved and no blocking logs remain. "
14994
- "Prefer Manager+AgentBus co-management: when fresh agentbus handoff is available and aligned, "
14995
- "follow that handoff to reduce orchestration latency instead of re-planning from scratch. "
14996
- "If finish is blocked by missing final summary after review approval, instruct Reviewer to hand off Explorer "
14997
- "via agentbus (intent=final_summary_request) instead of silently ending. "
14998
- f"Current task level={level or '-'}, mode={mode}, scale_preference={scale_preference}, participants={participant_text}, "
14999
- f"Current task profile: type={profile.get('task_type','general')}, "
15000
- f"complexity={profile.get('complexity','simple')}, "
15001
- f"progress={progress}, round_budget={'unlimited' if int(budget) <= 0 else int(budget)}, "
15697
+ "You are Manager in a multi-agent coding system. "
15698
+ "Read blackboard, delegate one short timeslice via route_to_next_agent. "
15699
+ "Policy: missing facts->explorer, implementation->developer, verification->reviewer, "
15700
+ "all done->finish. Set is_mandatory=true when concrete execution is required. "
15701
+ "Role capabilities: "
15702
+ "Explorer=read-only (bash/read_file/search/blackboard, NO write_file/edit_file); "
15703
+ "Developer=all tools (write_file/edit_file/bash/read_file/etc); "
15704
+ "Reviewer=read+verify (bash/read_file/finish_task, NO write_file/edit_file). "
15705
+ "NEVER delegate file-writing tasks to Explorer or Reviewer. "
15706
+ f"{coding_hint}"
15707
+ f"{project_todo_hint}"
15708
+ f"Level={level}, mode={mode}, progress={progress}, "
15709
+ f"budget={'unlimited' if int(budget) <= 0 else int(budget)}, "
15002
15710
  f"objective={trim(str(profile.get('direct_objective','') or ''), 220)}. "
15003
- "Avoid assigning the same agent more than two consecutive turns unless strictly required. "
15004
15711
  f"{model_language_instruction(self.ui_language)}"
15005
15712
  )
15006
15713
 
@@ -15243,6 +15950,24 @@ class SessionState:
15243
15950
  "reason": "approval-blocked-by-error",
15244
15951
  "source": "fallback",
15245
15952
  }
15953
+ if finish_gate_reason.startswith("project-todo-incomplete:"):
15954
+ missing_cat = finish_gate_reason.split(":", 1)[-1] if ":" in finish_gate_reason else ""
15955
+ if missing_cat == "compile_test":
15956
+ return {
15957
+ "target": "developer",
15958
+ "instruction": "编译/语法检查尚未完成。请编译项目并确认无错误。",
15959
+ "reason": "project-todo-compile-pending",
15960
+ "source": "fallback",
15961
+ "is_mandatory": True,
15962
+ }
15963
+ if missing_cat == "min_test":
15964
+ return {
15965
+ "target": "developer",
15966
+ "instruction": "最小功能测试尚未完成。请运行基本测试验证核心功能。",
15967
+ "reason": "project-todo-test-pending",
15968
+ "source": "fallback",
15969
+ "is_mandatory": True,
15970
+ }
15246
15971
  if task_type == "simple_qa":
15247
15972
  dev_text = self._latest_agent_assistant_text("developer")
15248
15973
  if dev_text:
@@ -15263,6 +15988,29 @@ class SessionState:
15263
15988
  "reason": "simple-qa-direct-answer",
15264
15989
  "source": "fallback",
15265
15990
  }
15991
+ # ── 通用 endpoint 检测:非 simple_qa 的 developer 结论性回复也能触发 finish ──
15992
+ if task_type != "simple_qa":
15993
+ dev_text = self._latest_agent_assistant_text("developer")
15994
+ if dev_text:
15995
+ done_probe = self._detect_endpoint_intent(dev_text, None)
15996
+ if bool(done_probe.get("matched", False)) and not has_error_log:
15997
+ return {
15998
+ "target": "finish",
15999
+ "instruction": "Developer has provided a conclusive response; stop now.",
16000
+ "reason": "general-endpoint-detected",
16001
+ "source": "fallback",
16002
+ }
16003
+ # 通用检查:如果最近的 assistant 消息是结论性回复,且没有待办事项,直接 finish
16004
+ if not has_error_log:
16005
+ for _role in ("developer", "explorer", "reviewer"):
16006
+ _last = self._latest_agent_assistant_text(_role)
16007
+ if _last and self._looks_like_conclusive_reply(_last) and not self.todo.has_open_items():
16008
+ return {
16009
+ "target": "finish",
16010
+ "instruction": "Agent already provided a conclusive reply with no open tasks; stop now.",
16011
+ "reason": "conclusive-reply-detected",
16012
+ "source": "fallback",
16013
+ }
15266
16014
  if complexity == "simple" and task_type == "simple_code":
15267
16015
  if has_error_log:
15268
16016
  return {
@@ -15398,6 +16146,24 @@ class SessionState:
15398
16146
  if str(row.get("task_type", "") or "").strip().lower() == "simple_qa":
15399
16147
  return row
15400
16148
  target = str(row.get("target", "") or "").strip().lower()
16149
+ task_type_low = str(row.get("task_type", "") or "").strip().lower()
16150
+ if task_type_low in ("simple_code", "engineering") and target == "explorer":
16151
+ board = self._ensure_blackboard()
16152
+ progress = self._manager_progress_state(board)
16153
+ if progress in ("initializing", "in_progress"):
16154
+ explorer_count = sum(
16155
+ 1 for x in self.manager_routes[-8:]
16156
+ if str(x.get("target", "") or "").strip().lower() == "explorer"
16157
+ )
16158
+ if explorer_count >= 2:
16159
+ row["target"] = "developer"
16160
+ row["instruction"] = (
16161
+ "Coding task: Explorer has been used enough. "
16162
+ "Start implementation now using write_file/edit_file."
16163
+ )
16164
+ row["reason"] = f"{row.get('reason', '')}|coding-fast-track->developer"
16165
+ row["source"] = "anti-stall"
16166
+ return row
15401
16167
  if target not in AGENT_ROLES:
15402
16168
  return row
15403
16169
  recent = [str(x.get("target", "") or "").strip().lower() for x in self.manager_routes[-4:]]
@@ -15492,6 +16258,11 @@ class SessionState:
15492
16258
  participants[-1] = target
15493
16259
  else:
15494
16260
  target = participants[0]
16261
+ # ── Single 模式硬约束:无论 executor_mode_flag 如何,只允许 assigned_expert ──
16262
+ if mode == EXECUTION_MODE_SINGLE:
16263
+ participants = [assigned_expert]
16264
+ if target in AGENT_ROLES and target != assigned_expert:
16265
+ target = assigned_expert
15495
16266
  instruction = trim(str(row.get("instruction", "") or "").strip(), 1200)
15496
16267
  if not instruction:
15497
16268
  instruction = "Proceed with one concrete next step and report evidence."
@@ -15548,6 +16319,24 @@ class SessionState:
15548
16319
  feedback_pass = self._manager_feedback_passed_from_blackboard(board)
15549
16320
  summary_attempts = int(board.get("manager_summary_attempts", 0) or 0)
15550
16321
  force_finish_override = False
16322
+ # ── 结论性回复截断:当 Agent 已回复结论且无待办/无错误时,强制 finish ──
16323
+ if target in AGENT_ROLES and target != "finish":
16324
+ for _check_role in ("developer", "explorer", "reviewer"):
16325
+ _last_text = self._latest_agent_assistant_text(_check_role)
16326
+ if (
16327
+ _last_text
16328
+ and self._looks_like_conclusive_reply(_last_text)
16329
+ and not self.todo.has_open_items()
16330
+ and not self._manager_has_error_log(board)
16331
+ ):
16332
+ target = "finish"
16333
+ instruction = (
16334
+ f"Agent '{_check_role}' already provided a conclusive reply. "
16335
+ "No open tasks remain. Finishing now."
16336
+ )
16337
+ row["reason"] = f"conclusive-reply-override:{_check_role}"
16338
+ row["source"] = "policy"
16339
+ break
15551
16340
  if bool((board.get("approval", {}) or {}).get("approved", False)) and can_finish_from_approval:
15552
16341
  target = "finish"
15553
16342
  if not instruction:
@@ -15616,6 +16405,44 @@ class SessionState:
15616
16405
  "Do not finish yet. Latest execution logs still contain blocking errors. "
15617
16406
  "Resolve errors and provide verifiable evidence."
15618
16407
  )
16408
+ elif finish_gate_reason.startswith("project-todo-incomplete:"):
16409
+ missing_cat = finish_gate_reason.split(":", 1)[-1] if ":" in finish_gate_reason else ""
16410
+ target = "developer"
16411
+ if missing_cat == "compile_test":
16412
+ instruction = (
16413
+ "编译/语法检查尚未完成。请编译项目并确认无错误。"
16414
+ "Run build/compile and confirm zero errors before finishing."
16415
+ )
16416
+ elif missing_cat == "min_test":
16417
+ instruction = (
16418
+ "最小功能测试尚未完成。请运行基本测试验证核心功能。"
16419
+ "Run minimal tests to verify core functionality before finishing."
16420
+ )
16421
+ else:
16422
+ instruction = (
16423
+ "Project todo not yet completed. Execute the pending step and report evidence."
16424
+ )
16425
+ # Force-finish fallback: if blocked > 3 cycles, mark as done to avoid deadloop
16426
+ summary_attempts = int(board.get("manager_summary_attempts", 0) or 0)
16427
+ if summary_attempts >= 3:
16428
+ force_finish_override = True
16429
+ target = "finish"
16430
+ for pt in board.get("project_todos", []):
16431
+ if pt.get("category") in ("compile_test", "min_test") and pt.get("status") != "completed":
16432
+ pt.update(status="completed", completed_at=float(now_ts()),
16433
+ evidence="force-finish fallback")
16434
+ self.blackboard = board
16435
+ instruction = (
16436
+ "Compile/test gate exceeded retry limit. Force finishing with available evidence. "
16437
+ "Generate final summary and finish now."
16438
+ )
16439
+ row["reason"] = "finish-blocked-project-todo-force-close"
16440
+ row["source"] = "policy"
16441
+ else:
16442
+ board["manager_summary_attempts"] = summary_attempts + 1
16443
+ self.blackboard = board
16444
+ row["reason"] = f"finish-blocked-{missing_cat}"
16445
+ row["source"] = "policy"
15619
16446
  else:
15620
16447
  has_outputs = bool(code_count > 0 or research_count > 0)
15621
16448
  if board_status == "COMPLETED" and has_outputs:
@@ -15848,7 +16675,7 @@ class SessionState:
15848
16675
  },
15849
16676
  }
15850
16677
  ]
15851
- self.manager_context.append(
16678
+ self._append_manager_context(
15852
16679
  {
15853
16680
  "role": "system",
15854
16681
  "content": (
@@ -15858,7 +16685,6 @@ class SessionState:
15858
16685
  "ts": now_ts(),
15859
16686
  }
15860
16687
  )
15861
- self.manager_context = self.manager_context[-400:]
15862
16688
  self._emit(
15863
16689
  "status",
15864
16690
  {
@@ -15898,7 +16724,7 @@ class SessionState:
15898
16724
  },
15899
16725
  }
15900
16726
  ]
15901
- self.manager_context.append(
16727
+ self._append_manager_context(
15902
16728
  {
15903
16729
  "role": "system",
15904
16730
  "content": (
@@ -15908,7 +16734,6 @@ class SessionState:
15908
16734
  "ts": now_ts(),
15909
16735
  }
15910
16736
  )
15911
- self.manager_context = self.manager_context[-400:]
15912
16737
  self._emit(
15913
16738
  "status",
15914
16739
  {
@@ -15926,8 +16751,8 @@ class SessionState:
15926
16751
  "Return only one route_to_next_agent call.\n\n"
15927
16752
  f"{self._blackboard_read_state_markdown(max_items=6)}"
15928
16753
  )
15929
- self.manager_context.append({"role": "user", "content": prompt, "ts": now_ts()})
15930
- self.manager_context = self.manager_context[-400:]
16754
+ self._append_manager_context({"role": "user", "content": prompt, "ts": now_ts()})
16755
+ self._microcompact_agent_messages(self.manager_context)
15931
16756
  with self.lock:
15932
16757
  self.current_phase = "manager:model-call"
15933
16758
  self.current_tool_name = ""
@@ -15963,8 +16788,7 @@ class SessionState:
15963
16788
  }
15964
16789
  for tc in tool_calls
15965
16790
  ]
15966
- self.manager_context.append(assistant)
15967
- self.manager_context = self.manager_context[-400:]
16791
+ self._append_manager_context(assistant)
15968
16792
  route_only_tool_calls = False
15969
16793
  if isinstance(tool_calls, list) and tool_calls:
15970
16794
  tool_names = [
@@ -16241,6 +17065,9 @@ class SessionState:
16241
17065
  "agent_role": role_key,
16242
17066
  }
16243
17067
  self.contexts[role_key] = [executor_seed]
17068
+ # Also filter old messages for this role from agent_messages and add the seed
17069
+ self.agent_messages = [m for m in self.agent_messages if m.get("agent_role") != role_key]
17070
+ self.agent_messages.append(executor_seed)
16244
17071
  self._emit(
16245
17072
  "status",
16246
17073
  {
@@ -16259,11 +17086,22 @@ class SessionState:
16259
17086
  max_len=1400,
16260
17087
  )
16261
17088
  language_note = embedded_policy or self._agent_language_policy_note()
17089
+ role_capability_note = {
17090
+ "explorer": "YOUR TOOLS: read-only (bash/read_file/search/blackboard). You CANNOT write_file or edit_file.",
17091
+ "developer": "YOUR TOOLS: all tools available (write_file/edit_file/bash/read_file/etc).",
17092
+ "reviewer": "YOUR TOOLS: read+verify (bash/read_file/finish_task). You CANNOT write_file or edit_file.",
17093
+ }.get(role_key, "")
17094
+ if role_key == "explorer":
17095
+ tool_examples = "bash/read_file/read_from_blackboard"
17096
+ elif role_key == "reviewer":
17097
+ tool_examples = "bash/read_file/finish_task"
17098
+ else:
17099
+ tool_examples = "write_file/edit_file/bash/read_file"
16262
17100
  mandatory_note = (
16263
17101
  (
16264
17102
  "MANDATORY EXECUTION: this delegate is hard-push. "
16265
17103
  "You must call at least one concrete tool in this turn "
16266
- "(e.g. write_file/edit_file/bash/read_file) and produce verifiable progress. "
17104
+ f"(e.g. {tool_examples}) and produce verifiable progress. "
16267
17105
  "Suggestion-only text reply is forbidden."
16268
17106
  )
16269
17107
  if bool(is_mandatory)
@@ -16293,6 +17131,7 @@ class SessionState:
16293
17131
  f"{mandatory_note}\n"
16294
17132
  f"{executor_note}\n"
16295
17133
  f"{collaboration_note}\n"
17134
+ f"{role_capability_note}\n"
16296
17135
  "</manager-delegate>\n"
16297
17136
  "<blackboard-state>\n"
16298
17137
  f"{trim(board_md, 6000)}\n"
@@ -16802,10 +17641,17 @@ class SessionState:
16802
17641
  role_key = self._sanitize_agent_role(role)
16803
17642
  if not role_key:
16804
17643
  return self.messages
17644
+ # Build filtered view from unified agent_messages
17645
+ filtered = [
17646
+ m for m in self.agent_messages
17647
+ if m.get("agent_role") == role_key
17648
+ or m.get("agent_role") == "shared"
17649
+ or (m.get("role") == "user" and not m.get("agent_role"))
17650
+ ]
17651
+ # Update legacy cache for backward compatibility (serialization, etc.)
16805
17652
  if not isinstance(self.contexts, dict):
16806
17653
  self.contexts = {r: [] for r in AGENT_ROLES}
16807
- if role_key not in self.contexts or not isinstance(self.contexts.get(role_key), list):
16808
- self.contexts[role_key] = []
17654
+ self.contexts[role_key] = filtered[-400:]
16809
17655
  return self.contexts[role_key]
16810
17656
 
16811
17657
  def _append_agent_context_message(self, role: str, message: dict, *, mirror_to_global: bool = False) -> dict:
@@ -16814,11 +17660,10 @@ class SessionState:
16814
17660
  row["agent_role"] = role_key
16815
17661
  if "ts" not in row:
16816
17662
  row["ts"] = now_ts()
16817
- ctx = self._agent_context(role_key)
16818
- ctx.append(row)
16819
- if len(ctx) > 400:
16820
- self.contexts[role_key] = ctx[-400:]
16821
- ctx = self.contexts[role_key]
17663
+ # Write to unified agent_messages
17664
+ self.agent_messages.append(row)
17665
+ if len(self.agent_messages) > 1200:
17666
+ self.agent_messages = self.agent_messages[-800:]
16822
17667
  if mirror_to_global:
16823
17668
  mirror = dict(row)
16824
17669
  if "tool_calls" in mirror and isinstance(mirror.get("tool_calls"), list):
@@ -16838,6 +17683,19 @@ class SessionState:
16838
17683
  self.messages = self.messages[-400:]
16839
17684
  return row
16840
17685
 
17686
+ def _append_manager_context(self, message: dict):
17687
+ """Append to manager_context and agent_messages in sync."""
17688
+ row = dict(message or {})
17689
+ if "agent_role" not in row:
17690
+ row["agent_role"] = "manager"
17691
+ if "ts" not in row:
17692
+ row["ts"] = now_ts()
17693
+ self.manager_context.append(row)
17694
+ self.manager_context = self.manager_context[-400:]
17695
+ self.agent_messages.append(row)
17696
+ if len(self.agent_messages) > 1200:
17697
+ self.agent_messages = self.agent_messages[-800:]
17698
+
16841
17699
  def _agent_display_name(self, role: str) -> str:
16842
17700
  return AGENT_ROLE_LABELS.get(self._sanitize_agent_role(role), str(role or "").strip().title() or "Agent")
16843
17701
 
@@ -16935,52 +17793,69 @@ class SessionState:
16935
17793
  )
16936
17794
  return envelope
16937
17795
 
17796
+ def _drain_agentbus_fast_route(self) -> dict | None:
17797
+ """Check agent_bus_messages for an unprocessed handoff that can skip manager.
17798
+
17799
+ Returns route dict with 'to' and 'payload' if a valid fast-route is found,
17800
+ otherwise returns None (fall back to manager delegation).
17801
+ """
17802
+ if not self.agent_bus_messages:
17803
+ return None
17804
+ now = now_ts()
17805
+ valid_intents = {
17806
+ "handoff", "review_request", "fix_request",
17807
+ "final_summary_request", "implementation_ready",
17808
+ }
17809
+ for env in reversed(self.agent_bus_messages[-20:]):
17810
+ if not isinstance(env, dict):
17811
+ continue
17812
+ if env.get("_fast_routed"):
17813
+ continue
17814
+ intent = str(env.get("intent", "") or "").strip().lower()
17815
+ if intent not in valid_intents:
17816
+ continue
17817
+ to_role = self._sanitize_agent_role(env.get("to", ""))
17818
+ if not to_role or to_role not in AGENT_ROLES:
17819
+ continue
17820
+ age = max(0.0, now - float(env.get("ts", 0.0) or 0.0))
17821
+ if age > 180.0:
17822
+ continue
17823
+ env["_fast_routed"] = True
17824
+ payload = trim(str(env.get("payload", "") or ""), 1400)
17825
+ from_role = str(env.get("from", "") or "")
17826
+ self._emit(
17827
+ "status",
17828
+ {
17829
+ "summary": (
17830
+ f"agentbus fast-route: {from_role}->{to_role} "
17831
+ f"intent={intent} (skipping manager)"
17832
+ )
17833
+ },
17834
+ )
17835
+ return {
17836
+ "to": to_role,
17837
+ "payload": payload,
17838
+ "intent": intent,
17839
+ "from": from_role,
17840
+ "env_id": env.get("id", ""),
17841
+ }
17842
+ return None
17843
+
16938
17844
  def _agent_role_system_prompt(self, role: str) -> str:
16939
17845
  role_key = self._sanitize_agent_role(role) or "developer"
16940
17846
  base = (
16941
- f"You are {self._agent_display_name(role_key)} in a blackboard-driven multi-agent coding system. "
16942
- f"Workspace root: {self.files_root}. "
16943
- "**You are in a restricted container; your virtual root is /workspace. "
16944
- "Never use or access absolute host paths such as /Users/...** "
16945
- f"Session absolute writable root is {self.files_root}. "
16946
- "Use relative file paths (for example hello.txt); runtime maps them to session absolute paths. "
16947
- "If '/workspace/...' appears, treat it as a virtual alias only; never create OS-level /workspace in shell. "
16948
- f"{_detect_os_shell_instruction()} "
16949
- "You must stay within your role boundary and use only provided tools. "
16950
- "Use read_from_blackboard/write_to_blackboard to keep the shared state accurate. "
16951
- "When communicating with other agents, use ask_colleague with structured intent/content. "
16952
- "Always keep outputs concise and action-oriented. "
17847
+ f"You are {self._agent_display_name(role_key)} in a multi-agent coding system. "
17848
+ f"Workspace: {self.files_root}. Use relative paths. "
17849
+ "Use blackboard for shared state, ask_colleague for inter-agent communication. "
17850
+ "Keep outputs concise and action-oriented. "
17851
+ "Use load_skill for detailed guidance (multi-agent-guide, code-review-checklist, finish-protocol). "
16953
17852
  f"{model_language_instruction(self.ui_language)} "
16954
17853
  )
16955
17854
  if role_key == "explorer":
16956
- return (
16957
- base
16958
- + "Role objective: analyze user goals, inspect codebase, and produce actionable research notes. "
16959
- + "Prefer read/search/check commands; avoid direct large code modifications. "
16960
- + "When new evidence appears, write concise research updates to blackboard and hand off actionable insights. "
16961
- + "Proactively use ask_colleague when your findings can unblock developer/reviewer immediately. "
16962
- + "If reviewer sends final_summary_request, produce final wrap-up summary from blackboard evidence and finish."
16963
- )
17855
+ return base + "Role: analyze goals, inspect codebase, produce research notes. Prefer read/search. "
16964
17856
  if role_key == "reviewer":
16965
- return (
16966
- base
16967
- + "Role objective: verify developer output against goal, run checks/tests, and issue pass/fix decisions. "
16968
- + "If gaps remain, send fix_request to developer with concrete failure evidence and write review_feedback to blackboard. "
16969
- + "If manager requests final summary, first call read_from_blackboard "
16970
- + "(sections: code_artifacts, execution_logs, review_feedback, status), then generate a structured summary "
16971
- + "covering changes, validation evidence, and residual risks/next steps. "
16972
- + "When finishing, pass this summary in finish_task.summary; empty or vague summary is invalid. "
16973
- + "If you cannot produce summary from current evidence, hand off Explorer via ask_colleague "
16974
- + "intent=final_summary_request with explicit missing evidence."
16975
- )
16976
- return (
16977
- base
16978
- + "Role objective: implement code changes based on explorer/reviewer inputs. "
16979
- + "Perform concrete file edits and command execution. "
16980
- + "Continuously record progress and blockers to blackboard. "
16981
- + "When blocked or uncertain, immediately call ask_colleague to explorer/reviewer with focused intent. "
16982
- + "When implementation batch is ready, send review_request to reviewer via ask_colleague."
16983
- )
17857
+ return base + "Role: verify output, run checks, issue pass/fix decisions. Write review_feedback to blackboard. "
17858
+ return base + "Role: implement code changes, execute tools, record progress to blackboard. "
16984
17859
 
16985
17860
  def _seed_multi_agent_contexts_if_needed(self, user_text: str = ""):
16986
17861
  if not self._is_multi_agent_mode():
@@ -17005,16 +17880,17 @@ class SessionState:
17005
17880
  mirror_to_global=False,
17006
17881
  )
17007
17882
  if not self.manager_context:
17008
- self.manager_context = [
17009
- {
17010
- "role": "system",
17011
- "content": (
17012
- "Manager context initialized. Delegate by reading blackboard and assigning short slices.\n"
17013
- f"{language_note}"
17014
- ),
17015
- "ts": now_ts(),
17016
- }
17017
- ]
17883
+ init_msg = {
17884
+ "role": "system",
17885
+ "content": (
17886
+ "Manager context initialized. Delegate by reading blackboard and assigning short slices.\n"
17887
+ f"{language_note}"
17888
+ ),
17889
+ "ts": now_ts(),
17890
+ "agent_role": "manager",
17891
+ }
17892
+ self.manager_context = [init_msg]
17893
+ self.agent_messages.append(init_msg)
17018
17894
  if not self._agent_context("developer"):
17019
17895
  self._append_agent_context_message(
17020
17896
  "developer",
@@ -18340,6 +19216,7 @@ class SessionState:
18340
19216
  ctx = self._agent_context(role_key)
18341
19217
  if not ctx:
18342
19218
  return {"status": "skip", "reason": "empty-context", "role": role_key}
19219
+ self._microcompact_agent_messages(ctx)
18343
19220
  with self.lock:
18344
19221
  self.current_phase = f"agent:{role_key}:model-call"
18345
19222
  self.current_tool_name = ""
@@ -18348,7 +19225,7 @@ class SessionState:
18348
19225
  ctx,
18349
19226
  tools=self._tools_for_agent(role_key),
18350
19227
  system=self._agent_role_system_prompt(role_key),
18351
- max_tokens=AGENT_MAX_OUTPUT_TOKENS,
19228
+ max_tokens=self.max_output_tokens,
18352
19229
  think=False,
18353
19230
  stream_thinking=False,
18354
19231
  on_thinking_chunk=self._append_live_thinking,
@@ -18694,10 +19571,23 @@ class SessionState:
18694
19571
  media_inputs_pool=media_inputs_pool,
18695
19572
  media_seen_ts_by_role=media_seen_ts_by_role,
18696
19573
  )
18697
- route = self._manager_delegate_turn(
18698
- pinned_selection=pinned_selection,
18699
- media_inputs_round=manager_media_inputs,
18700
- )
19574
+ # AgentBus fast-route: skip manager if a valid worker handoff is pending
19575
+ bus_route = self._drain_agentbus_fast_route()
19576
+ if bus_route:
19577
+ target = bus_route["to"]
19578
+ instruction = trim(str(bus_route.get("payload", "") or ""), 1400)
19579
+ route = {
19580
+ "target": target,
19581
+ "instruction": instruction,
19582
+ "source": "agentbus-direct",
19583
+ "is_mandatory": False,
19584
+ "executor_mode": False,
19585
+ }
19586
+ else:
19587
+ route = self._manager_delegate_turn(
19588
+ pinned_selection=pinned_selection,
19589
+ media_inputs_round=manager_media_inputs,
19590
+ )
18701
19591
  target = str(route.get("target", "") or "").strip().lower()
18702
19592
  instruction = trim(str(route.get("instruction", "") or "").strip(), 1400)
18703
19593
  if compact_mode and target in AGENT_ROLES:
@@ -18738,6 +19628,23 @@ class SessionState:
18738
19628
  media_inputs_round=role_media_inputs,
18739
19629
  )
18740
19630
  self._blackboard_update_from_worker_step(role, step)
19631
+ # ── Agent turn 结束后的终止检测:结论性回复 + 无待办 + 无错误 → 自动 finish ──
19632
+ agent_text = self._latest_agent_assistant_text(role)
19633
+ if (
19634
+ agent_text
19635
+ and self._looks_like_conclusive_reply(agent_text)
19636
+ and not self.todo.has_open_items()
19637
+ and not self._manager_has_error_log(self._ensure_blackboard())
19638
+ ):
19639
+ self._blackboard_mark_approved(
19640
+ f"conclusive reply from {role}: auto-finish", role
19641
+ )
19642
+ self._mark_all_done_silently(f"conclusive reply from {role}")
19643
+ self._emit(
19644
+ "status",
19645
+ {"summary": f"agent '{role}' gave conclusive reply; finishing run"},
19646
+ )
19647
+ break
18741
19648
  board_after = self._ensure_blackboard()
18742
19649
  board_after_fp = self._watchdog_state_fingerprint(board_after)
18743
19650
  wd_event = self._watchdog_process_worker_step(
@@ -18786,6 +19693,7 @@ class SessionState:
18786
19693
  },
18787
19694
  )
18788
19695
  continue
19696
+ self._auto_summary_on_finish()
18789
19697
  self._mark_all_done_silently(note)
18790
19698
  self._emit(
18791
19699
  "status",
@@ -18847,7 +19755,9 @@ class SessionState:
18847
19755
  )
18848
19756
  break
18849
19757
  else:
18850
- self._emit("error", {"summary": f"max loop reached ({self.max_agent_rounds})"})
19758
+ summary = self._auto_summary_on_finish()
19759
+ self._mark_all_done_silently(f"budget exhausted: {summary}")
19760
+ self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
18851
19761
 
18852
19762
  def _multi_agent_worker(self, *, pinned_selection: str):
18853
19763
  mode = self._effective_execution_mode()
@@ -18980,7 +19890,9 @@ class SessionState:
18980
19890
  sync_index += 1
18981
19891
  continue
18982
19892
  else:
18983
- self._emit("error", {"summary": f"max loop reached ({self.max_agent_rounds})"})
19893
+ summary = self._auto_summary_on_finish()
19894
+ self._mark_all_done_silently(f"budget exhausted: {summary}")
19895
+ self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
18984
19896
 
18985
19897
  def _agent_worker(self):
18986
19898
  single_role = "developer"
@@ -19205,7 +20117,7 @@ class SessionState:
19205
20117
  self.messages,
19206
20118
  tools=TOOLS,
19207
20119
  system=self._system_prompt(),
19208
- max_tokens=AGENT_MAX_OUTPUT_TOKENS,
20120
+ max_tokens=self.max_output_tokens,
19209
20121
  think=False,
19210
20122
  stream_thinking=False,
19211
20123
  on_thinking_chunk=self._append_live_thinking,
@@ -19579,6 +20491,9 @@ class SessionState:
19579
20491
  },
19580
20492
  )
19581
20493
  continue
20494
+ # 对简单查询(非工程任务)限制自动继续预算
20495
+ if auto_continue_budget > 8 and not self._is_long_running_engineering_context():
20496
+ auto_continue_budget = min(auto_continue_budget, 8)
19582
20497
  can_continue = auto_continue_budget > 0 and (
19583
20498
  todo_blocking or self._looks_like_incomplete_reply(text)
19584
20499
  )
@@ -20080,7 +20995,9 @@ class SessionState:
20080
20995
  if stop_due_to_repeated_tool_loop or stop_due_to_hard_break or stop_due_to_finish_task:
20081
20996
  break
20082
20997
  else:
20083
- self._emit("error", {"summary": f"max loop reached ({self.max_agent_rounds})"})
20998
+ summary = self._auto_summary_on_finish()
20999
+ self._mark_all_done_silently(f"budget exhausted: {summary}")
21000
+ self._emit("status", {"summary": f"Budget exhausted ({self.max_agent_rounds} rounds). {trim(summary, 300)}"})
20084
21001
  except CircuitBreakerTriggered as exc:
20085
21002
  note = trim(str(exc), 320) or "Circuit breaker triggered."
20086
21003
  self._emit("status", {"summary": f"hard-stop: {note}"})
@@ -20446,6 +21363,107 @@ class SessionState:
20446
21363
  bio.seek(0)
20447
21364
  return bio.read()
20448
21365
 
21366
+ def export_conversation_md(self) -> str:
21367
+ snap = self.snapshot()
21368
+ title = snap.get("title") or snap.get("id") or "Session"
21369
+ lines = [
21370
+ f"# {title}",
21371
+ "",
21372
+ f"- Session: `{snap.get('id', '')}`",
21373
+ f"- Model: `{snap.get('model', '')}`",
21374
+ f"- Created: {_fmt_export_ts(snap.get('created_at', 0))}",
21375
+ "",
21376
+ "---",
21377
+ "",
21378
+ ]
21379
+ for row in snap.get("conversation_feed", []):
21380
+ role = str(row.get("role", "system"))
21381
+ ts = row.get("ts", 0)
21382
+ time_str = _fmt_export_ts(ts)
21383
+ text = str(row.get("text", ""))
21384
+ thinking = str(row.get("thinking", "") or "")
21385
+ row_type = str(row.get("type", "message"))
21386
+ agent = str(row.get("agent_role", "") or "")
21387
+ header = f"**[{role}]**"
21388
+ if agent:
21389
+ header += f" _{agent}_"
21390
+ if row_type not in ("message", ""):
21391
+ header += f" `{row_type}`"
21392
+ if time_str:
21393
+ header += f" <sub>{time_str}</sub>"
21394
+ lines.append(header)
21395
+ lines.append("")
21396
+ if thinking:
21397
+ lines.append("<details><summary>thinking</summary>")
21398
+ lines.append("")
21399
+ lines.append(thinking)
21400
+ lines.append("")
21401
+ lines.append("</details>")
21402
+ lines.append("")
21403
+ if text:
21404
+ lines.append(text)
21405
+ lines.append("")
21406
+ lines.append("---")
21407
+ lines.append("")
21408
+ return "\n".join(lines)
21409
+
21410
+ def export_conversation_pdf(self) -> bytes:
21411
+ md_text = self.export_conversation_md()
21412
+ return _text_to_minimal_pdf(md_text)
21413
+
21414
+ def _conversation_to_html(self) -> str:
21415
+ snap = self.snapshot()
21416
+ title = _html_esc(snap.get("title") or snap.get("id") or "Session")
21417
+ model = _html_esc(snap.get("model", ""))
21418
+ rows_html = []
21419
+ for row in snap.get("conversation_feed", []):
21420
+ role = str(row.get("role", "system"))
21421
+ ts = row.get("ts", 0)
21422
+ time_str = _fmt_export_ts(ts)
21423
+ text = str(row.get("text", ""))
21424
+ thinking = str(row.get("thinking", "") or "")
21425
+ bg = "#e8f4fd" if role == "user" else ("#f0f0f0" if role == "assistant" else "#fff9e6")
21426
+ block = f'<div style="background:{bg};border-radius:8px;padding:10px 14px;margin:6px 0">'
21427
+ block += f'<div style="font-weight:bold;font-size:13px;color:#555;margin-bottom:4px">[{_html_esc(role)}] {_html_esc(time_str)}</div>'
21428
+ if thinking:
21429
+ block += f'<details style="margin-bottom:6px"><summary style="color:#888;font-size:12px">thinking</summary><pre style="white-space:pre-wrap;font-size:12px;color:#666">{_html_esc(thinking)}</pre></details>'
21430
+ if text:
21431
+ block += f'<pre style="white-space:pre-wrap;font-size:13px;margin:0">{_html_esc(text)}</pre>'
21432
+ block += '</div>'
21433
+ rows_html.append(block)
21434
+ body = "\n".join(rows_html)
21435
+ return f"""<!DOCTYPE html>
21436
+ <html><head><meta charset="utf-8"><title>{title}</title>
21437
+ <style>body{{font-family:-apple-system,BlinkMacSystemFont,sans-serif;max-width:860px;margin:0 auto;padding:20px;background:#fff}}
21438
+ h1{{font-size:20px;margin-bottom:4px}}
21439
+ .meta{{color:#888;font-size:13px;margin-bottom:16px}}</style></head>
21440
+ <body><h1>{title}</h1><div class="meta">Model: {model}</div>
21441
+ {body}
21442
+ </body></html>"""
21443
+
21444
+ def export_conversation_image(self) -> bytes:
21445
+ html_content = self._conversation_to_html()
21446
+ try:
21447
+ from playwright.sync_api import sync_playwright
21448
+ with sync_playwright() as p:
21449
+ browser = p.chromium.launch()
21450
+ page = browser.new_page(viewport={"width": 860, "height": 800})
21451
+ page.set_content(html_content)
21452
+ page.wait_for_load_state("networkidle")
21453
+ img = page.screenshot(full_page=True, type="png")
21454
+ browser.close()
21455
+ return img
21456
+ except ImportError:
21457
+ pass
21458
+ except Exception:
21459
+ pass
21460
+ try:
21461
+ import imgkit
21462
+ return imgkit.from_string(html_content, False, options={"width": "860", "encoding": "UTF-8"})
21463
+ except ImportError:
21464
+ pass
21465
+ raise RuntimeError("Image export requires playwright or imgkit. Install: pip install playwright && playwright install chromium")
21466
+
20449
21467
  class SessionManager:
20450
21468
  def __init__(
20451
21469
  self,
@@ -20471,6 +21489,7 @@ class SessionManager:
20471
21489
  arbiter_max_tokens: int = ARBITER_DEFAULT_MAX_TOKENS,
20472
21490
  arbiter_temperature: float = ARBITER_DEFAULT_TEMPERATURE,
20473
21491
  execution_mode: str = EXECUTION_MODE_SYNC,
21492
+ max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
20474
21493
  run_finished_callback=None,
20475
21494
  ):
20476
21495
  self.root = root
@@ -20493,6 +21512,7 @@ class SessionManager:
20493
21512
  MIN_AGENT_ROUNDS,
20494
21513
  min(MAX_AGENT_ROUNDS_CAP, int(max_rounds or MAX_AGENT_ROUNDS)),
20495
21514
  )
21515
+ self.max_output_tokens = max(256, int(max_output_tokens or AGENT_MAX_OUTPUT_TOKENS))
20496
21516
  self.max_run_seconds = normalize_timeout_seconds(
20497
21517
  max_run_seconds if max_run_seconds is not None else MAX_RUN_SECONDS,
20498
21518
  minimum=MIN_RUN_TIMEOUT_SECONDS,
@@ -20839,6 +21859,7 @@ class SessionManager:
20839
21859
  arbiter_max_tokens=self.arbiter_max_tokens,
20840
21860
  arbiter_temperature=self.arbiter_temperature,
20841
21861
  execution_mode=self.execution_mode,
21862
+ max_output_tokens=self.max_output_tokens,
20842
21863
  ui_language=self.user_language,
20843
21864
  js_lib_root=self.js_lib_root,
20844
21865
  owner_user_id=self.user_id,
@@ -20879,6 +21900,7 @@ class SessionManager:
20879
21900
  arbiter_max_tokens=self.arbiter_max_tokens,
20880
21901
  arbiter_temperature=self.arbiter_temperature,
20881
21902
  execution_mode=self.execution_mode,
21903
+ max_output_tokens=self.max_output_tokens,
20882
21904
  ui_language=self.user_language,
20883
21905
  js_lib_root=self.js_lib_root,
20884
21906
  owner_user_id=self.user_id,
@@ -21291,7 +22313,7 @@ window.MathJax={
21291
22313
  <button id="applyModelBtn" class="subtle">Apply Model</button>
21292
22314
  <button id="importConfigBtn" class="subtle">Upload LLM.config.json</button>
21293
22315
  <input id="configInput" type="file" accept=".json,application/json" style="display:none">
21294
- <a id="downloadBtn" href="#" target="_blank" rel="noreferrer">Open Skills Studio</a>
22316
+ <a id="downloadBtn" href="#">Open Skills Studio</a>
21295
22317
  </div>
21296
22318
  </header>
21297
22319
  <div class="status-cards" id="topStats"></div>
@@ -21320,7 +22342,15 @@ window.MathJax={
21320
22342
  <button id="interruptBtn" class="subtle">Interrupt</button>
21321
22343
  <button id="compactBtn" class="subtle">Compact</button>
21322
22344
  <button id="refreshBtn" class="subtle">Refresh</button>
21323
- <a id="downloadSessionBtn" class="subtle disabled" href="#">Export Session</a>
22345
+ <div class="export-dropdown" style="position:relative;display:inline-block">
22346
+ <button id="exportMenuBtn" class="subtle">Export ▾</button>
22347
+ <div id="exportMenu" style="display:none;position:absolute;bottom:100%;left:0;background:#fff;border:1px solid var(--line);border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.12);z-index:999;min-width:160px;padding:4px 0;margin-bottom:4px">
22348
+ <a id="downloadSessionBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export ZIP</a>
22349
+ <a id="exportMdBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export Markdown</a>
22350
+ <a id="exportPdfBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export PDF</a>
22351
+ <a id="exportPngBtn" class="export-item" href="#" style="display:block;padding:6px 14px;text-decoration:none;color:#333;font-size:13px">Export Image</a>
22352
+ </div>
22353
+ </div>
21324
22354
  <div id="ctxLive" class="ctx-live" title="Remaining context budget">
21325
22355
  <span class="ctx-live-dot"></span>
21326
22356
  <span id="ctxLiveText" class="mono">ctx_left=-</span>
@@ -21394,6 +22424,7 @@ button,a{border:1px solid var(--line);padding:10px 14px;border-radius:12px;backg
21394
22424
  button:hover,a:hover{transform:translateY(-1px);box-shadow:0 4px 10px rgba(15,27,45,.08)}
21395
22425
  #sendBtn,#newSessionBtn{background:linear-gradient(135deg,var(--brand),var(--brand2));color:#fff;border:0}
21396
22426
  .subtle{background:#f6f8fa}
22427
+ .export-item:hover{background:#f0f4f8}
21397
22428
  .actions select{padding:10px 12px;border-radius:12px;border:1px solid var(--line);background:#fff;min-width:160px}
21398
22429
  .think-switch{display:flex;align-items:center;gap:6px;border:1px solid var(--line);padding:8px 10px;border-radius:12px;background:#fff;font-weight:600}
21399
22430
  .danger{color:var(--warn);border-color:#f3c0c0}
@@ -21508,7 +22539,7 @@ main{display:grid;grid-template-columns:minmax(220px,260px) minmax(520px,920px)
21508
22539
  .msg-md blockquote{margin:.5rem 0;padding:.4rem .6rem;border-left:3px solid #9db8e8;background:#eef4ff;border-radius:6px;color:#27446f}
21509
22540
  .msg-md .md-inline-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.86em;padding:1px 5px;border-radius:5px;background:#e8eef8;border:1px solid #d3dded;color:#1e3a5f;white-space:pre}
21510
22541
  .msg-md .md-code-lang{display:inline-block;margin:.3rem 0 0;padding:2px 8px;border:1px solid #cfd9ea;border-bottom:0;border-radius:8px 8px 0 0;background:#f3f7fd;color:#3b4f6d;font-size:.75rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
21511
- .msg-md .md-code{margin:0 0 .55rem;max-width:100%;overflow:auto;padding:8px;border:1px solid #dfe6ef;border-radius:0 8px 8px 8px;background:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;line-height:1.4;white-space:pre}
22542
+ .msg-md .md-code{margin:0 0 .55rem;max-width:100%;overflow:auto;padding:8px;border:1px solid #dfe6ef;border-radius:0 8px 8px 8px;background:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;line-height:1.4;white-space:pre;overscroll-behavior:contain;scrollbar-gutter:stable}
21512
22543
  .msg-md .md-table{margin:.5rem 0;border-collapse:collapse;max-width:100%;width:100%;display:block;overflow:auto;background:#fff}
21513
22544
  .msg-md .md-table th,.msg-md .md-table td{border:1px solid #d7e0ec;padding:6px 8px;text-align:left;vertical-align:top;white-space:nowrap}
21514
22545
  .msg-md .md-table th{background:#f5f8fc;font-weight:700}
@@ -21618,7 +22649,7 @@ h3{font-size:.96rem;margin:10px 0 6px}
21618
22649
 
21619
22650
  APP_JS = """const S={sessions:[],activeId:null,snap:null,es:null,esId:'',skills:[],tools:[],providers:[],protocols:[],config:null,models:[],modelOptions:[],previewBySession:{},fileExplorerBySession:{},previewNonce:0,refreshTimer:null,refreshInFlight:false,pendingSnapshot:false,pendingFullSnapshot:false,scheduledFullSnapshot:false,sessionPollTimer:null,renderStateInFlight:false,lastRenderStatePullAt:0,lastFeedSig:'',lastBoardsSig:'',lastSessionsSig:'',lastVisibilityState:document.visibilityState||'visible',staticMode:false,frozen:false,bootRendered:false,panelHtml:{},follow:{chat:true,sessionList:false,todos:false,tasks:false,activity:true,commands:true,diffs:true,catalog:true,fileExplorer:false},lastEventSeq:0,lastDeltaTs:0,deltaGapCount:0,deltaWatchdogTimer:null,deltaWatchdogStalls:0,deltaWatchdogSeq:0,deltaRenderRaf:0,deltaRenderChat:false,deltaRenderBoards:false,deltaRenderSessions:false,mathObserver:null,mathRoot:null,mdWorker:null,mdWorkerUrl:'',mdReqSeq:0,mdPending:Object.create(null),diffCenterDisabled:Object.create(null),previewCenterDisabled:Object.create(null),diffCenteredDone:Object.create(null),previewCenteredDone:Object.create(null)};
21620
22651
  const MD_CACHE=new Map();
21621
- const MD_CACHE_MAX=420;
22652
+ const MD_CACHE_MAX=280;
21622
22653
  const STATIC_UI=((new URLSearchParams(location.search)).get('static_ui')==='1');
21623
22654
  const SNAPSHOT_DELAY_VISIBLE_MS=300;
21624
22655
  const SNAPSHOT_DELAY_HIDDEN_MS=2400;
@@ -21633,30 +22664,30 @@ const CHAT_SCROLL_RENDER_THROTTLE_MS=70;
21633
22664
  const CHAT_SCROLL_SYNC_DEBOUNCE_MS=260;
21634
22665
  const CHAT_SCROLL_SETTLE_MS=620;
21635
22666
  const CHAT_SCROLL_SETTLE_EPS_PX=1;
21636
- const DELTA_MAX_FEED=420;
21637
- const DELTA_MAX_MESSAGES=420;
21638
- const DELTA_MAX_ACTIVITY=100;
21639
- const DELTA_MAX_OPERATIONS=220;
22667
+ const DELTA_MAX_FEED=300;
22668
+ const DELTA_MAX_MESSAGES=300;
22669
+ const DELTA_MAX_ACTIVITY=80;
22670
+ const DELTA_MAX_OPERATIONS=160;
21640
22671
  const DELTA_MAX_UPLOADS=40;
21641
22672
  const DELTA_WATCHDOG_INTERVAL_MS=1800;
21642
22673
  const DELTA_WATCHDOG_STALL_MS=9000;
21643
22674
  const MARKDOWN_WORKER_MIN_CHARS=800;
21644
22675
  const MARKDOWN_WORKER_MAX_PENDING=96;
21645
22676
  const MARKDOWN_WORKER_REQ_TTL_MS=45000;
21646
- const CHAT_VIRT={heights:Object.create(null),heightVersion:0,avgHeight:140,overscanPx:400,maxCacheKeys:1200,poolByKind:Object.create(null),poolSize:0,poolMax:180};
22677
+ const CHAT_VIRT={heights:Object.create(null),heightVersion:0,avgHeight:140,overscanPx:400,maxCacheKeys:600,poolByKind:Object.create(null),poolSize:0,poolMax:180};
21647
22678
  const RENDER_EVT_TYPES=new Set(['render_frame','render_bridge']);
21648
- const RENDER_QUEUE_MAX=140;
22679
+ const RENDER_QUEUE_MAX=80;
21649
22680
  const RENDER_META_MIN_INTERVAL_MS=180;
21650
22681
  const RENDER={queue:[],raf:0,canvas:null,ctx:null,lastSeq:0,lastPaintAt:0,lastMetaAt:0,lastSummary:'',hideTimer:0,imgTicket:0};
21651
22682
  const CODE_PREVIEW_VIRT_THRESHOLD=1800;
21652
22683
  const CODE_PREVIEW_VIRT_EST_ROW_PX=24;
21653
22684
  const CODE_PREVIEW_VIRT_OVERSCAN=160;
21654
- const CODE_PREVIEW_EXTS=new Set(['.py','.pyi','.js','.mjs','.cjs','.ts','.tsx','.jsx','.java','.c','.cc','.cpp','.cxx','.h','.hh','.hpp','.hxx','.go','.rs','.rb','.php','.swift','.kt','.kts','.scala','.sh','.bash','.zsh','.fish','.ps1','.bat','.sql','.json','.jsonc','.yaml','.yml','.toml','.ini','.cfg','.conf','.xml','.xsd','.xsl','.cs','.m','.mm','.r','.pl','.lua','.dart','.vue','.svelte','.gradle','.properties']);
22685
+ const CODE_PREVIEW_EXTS=new Set(['.py','.pyi','.js','.mjs','.cjs','.ts','.tsx','.jsx','.java','.c','.cc','.cpp','.cxx','.h','.hh','.hpp','.hxx','.go','.rs','.rb','.php','.swift','.kt','.kts','.scala','.sh','.bash','.zsh','.fish','.ps1','.bat','.sql','.json','.jsonc','.yaml','.yml','.toml','.ini','.cfg','.conf','.xml','.xsd','.xsl','.cs','.m','.mm','.r','.pl','.lua','.dart','.vue','.svelte','.gradle','.properties','.f','.f90','.f95','.f03','.f08','.for','.fpp','.hs','.lhs','.erl','.hrl','.ex','.exs','.ml','.mli','.vhd','.vhdl','.v','.sv','.asm','.s','.proto','.tf','.tfvars','.prisma','.graphql','.gql','.zig','.nim','.jl','.cr','.d','.clj','.cljs','.cljc','.lisp','.cl','.el','.rkt','.pas','.pp','.wgsl','.glsl','.hlsl','.groovy','.cmake','.dockerfile']);
21655
22686
  const CODE_PREVIEW_FILENAMES=new Set(['dockerfile','makefile','cmakelists.txt','justfile','gemfile','rakefile','pipfile','requirements.txt']);
21656
- const CODE_LANG_BY_EXT={'.py':'python','.pyi':'python','.js':'javascript','.mjs':'javascript','.cjs':'javascript','.ts':'typescript','.tsx':'typescript','.jsx':'javascript','.java':'java','.c':'c','.cc':'cpp','.cpp':'cpp','.cxx':'cpp','.h':'c','.hh':'cpp','.hpp':'cpp','.hxx':'cpp','.go':'go','.rs':'rust','.rb':'ruby','.php':'php','.swift':'swift','.kt':'kotlin','.kts':'kotlin','.scala':'scala','.sh':'shell','.bash':'shell','.zsh':'shell','.fish':'shell','.ps1':'shell','.bat':'shell','.sql':'sql','.json':'json','.jsonc':'json','.yaml':'yaml','.yml':'yaml','.toml':'toml','.ini':'ini','.cfg':'ini','.conf':'ini','.xml':'xml','.xsd':'xml','.xsl':'xml','.cs':'csharp','.m':'objectivec','.mm':'objectivec','.r':'r','.pl':'perl','.lua':'lua','.dart':'dart','.vue':'javascript','.svelte':'javascript','.gradle':'groovy','.properties':'ini'};
22687
+ const CODE_LANG_BY_EXT={'.py':'python','.pyi':'python','.js':'javascript','.mjs':'javascript','.cjs':'javascript','.ts':'typescript','.tsx':'typescript','.jsx':'javascript','.java':'java','.c':'c','.cc':'cpp','.cpp':'cpp','.cxx':'cpp','.h':'c','.hh':'cpp','.hpp':'cpp','.hxx':'cpp','.go':'go','.rs':'rust','.rb':'ruby','.php':'php','.swift':'swift','.kt':'kotlin','.kts':'kotlin','.scala':'scala','.sh':'shell','.bash':'shell','.zsh':'shell','.fish':'shell','.ps1':'shell','.bat':'shell','.sql':'sql','.json':'json','.jsonc':'json','.yaml':'yaml','.yml':'yaml','.toml':'toml','.ini':'ini','.cfg':'ini','.conf':'ini','.xml':'xml','.xsd':'xml','.xsl':'xml','.cs':'csharp','.m':'objectivec','.mm':'objectivec','.r':'r','.pl':'perl','.lua':'lua','.dart':'dart','.vue':'javascript','.svelte':'javascript','.gradle':'groovy','.properties':'ini','.f':'fortran','.f90':'fortran','.f95':'fortran','.f03':'fortran','.f08':'fortran','.for':'fortran','.fpp':'fortran','.hs':'haskell','.lhs':'haskell','.erl':'erlang','.hrl':'erlang','.ex':'elixir','.exs':'elixir','.ml':'ocaml','.mli':'ocaml','.vhd':'vhdl','.vhdl':'vhdl','.v':'verilog','.sv':'verilog','.asm':'asm','.s':'asm','.proto':'protobuf','.tf':'hcl','.tfvars':'hcl','.zig':'zig','.nim':'nim','.jl':'julia','.cr':'crystal','.d':'dlang','.clj':'clojure','.cljs':'clojure','.cljc':'clojure','.lisp':'lisp','.cl':'lisp','.el':'lisp','.rkt':'racket','.pas':'pascal','.pp':'pascal','.wgsl':'wgsl','.glsl':'glsl','.hlsl':'hlsl','.groovy':'groovy','.cmake':'cmake','.dockerfile':'shell','.prisma':'prisma','.graphql':'graphql','.gql':'graphql'};
21657
22688
  const CODE_LANG_BY_NAME={'dockerfile':'shell','makefile':'makefile','cmakelists.txt':'cmake','justfile':'makefile','gemfile':'ruby','rakefile':'ruby','pipfile':'ini','requirements.txt':'ini'};
21658
22689
  const CODE_LITERAL_WORDS=new Set(['true','false','null','undefined','none','nil']);
21659
- const CODE_KEYWORDS={default:new Set(['if','else','for','while','switch','case','break','continue','return','function','class','import','export','from','try','catch','finally','throw','new','const','let','var','public','private','protected','static','async','await']),python:new Set(['def','class','if','elif','else','for','while','try','except','finally','raise','return','yield','import','from','as','with','pass','break','continue','lambda','global','nonlocal','assert','del','in','is','not','and','or','async','await']),javascript:new Set(['function','class','if','else','for','while','do','switch','case','break','continue','return','try','catch','finally','throw','new','this','const','let','var','import','export','from','default','extends','super','async','await','typeof','instanceof','in','of']),typescript:new Set(['interface','type','enum','implements','readonly','namespace','declare','keyof','infer','satisfies','as','extends','public','private','protected','abstract','override','function','class','if','else','for','while','switch','case','break','continue','return','try','catch','finally','throw','const','let','var','import','export','from','async','await']),java:new Set(['class','interface','enum','extends','implements','public','private','protected','static','final','abstract','volatile','synchronized','if','else','for','while','switch','case','break','continue','return','try','catch','finally','throw','new','package','import','instanceof','this','super','void']),c:new Set(['if','else','for','while','switch','case','break','continue','return','typedef','struct','union','enum','static','const','volatile','extern','inline','sizeof','#include','#define']),cpp:new Set(['if','else','for','while','switch','case','break','continue','return','class','struct','namespace','template','typename','public','private','protected','virtual','override','const','static','auto','constexpr','using','new','delete','this','throw','try','catch','#include','#define']),go:new Set(['package','import','func','type','struct','interface','map','chan','go','defer','select','if','else','for','switch','case','break','continue','return','fallthrough','range','const','var']),rust:new Set(['fn','let','mut','impl','trait','struct','enum','match','if','else','for','while','loop','break','continue','return','pub','use','mod','crate','self','super','where','async','await','move']),ruby:new Set(['def','class','module','if','elsif','else','unless','case','when','for','while','until','begin','rescue','ensure','return','yield','super','self','require','include','extend','end']),php:new Set(['function','class','interface','trait','public','private','protected','static','if','elseif','else','for','foreach','while','switch','case','break','continue','return','try','catch','finally','throw','namespace','use','new']),swift:new Set(['func','class','struct','enum','protocol','extension','if','else','guard','for','while','switch','case','break','continue','return','defer','do','catch','throw','try','import','let','var']),kotlin:new Set(['fun','class','interface','object','data','sealed','enum','if','else','when','for','while','do','break','continue','return','try','catch','throw','import','package','val','var','companion']),scala:new Set(['def','class','trait','object','case','if','else','for','while','match','break','continue','return','try','catch','throw','import','package','val','var','extends','with']),shell:new Set(['if','then','else','fi','for','do','done','while','case','esac','function','return','break','continue','export','local','readonly','in']),sql:new Set(['select','from','where','group','by','order','insert','into','values','update','set','delete','join','left','right','inner','outer','on','create','alter','drop','table','view','index','and','or','not','as','limit']),json:new Set([]),yaml:new Set([]),toml:new Set([]),ini:new Set([]),xml:new Set([]),csharp:new Set(['namespace','class','interface','struct','enum','public','private','protected','internal','static','readonly','const','if','else','for','foreach','while','switch','case','break','continue','return','using','new','this','base','async','await']),objectivec:new Set(['@interface','@implementation','@property','@synthesize','@end','if','else','for','while','switch','case','break','continue','return','#import']),r:new Set(['if','else','for','while','repeat','break','next','function','return','library']),perl:new Set(['if','elsif','else','for','foreach','while','last','next','sub','my','our','use','package','return']),lua:new Set(['if','then','else','elseif','end','for','while','repeat','until','break','function','local','return']),dart:new Set(['class','enum','extension','if','else','for','while','switch','case','break','continue','return','import','library','part','new','const','final','var','async','await']),groovy:new Set(['class','interface','trait','if','else','for','while','switch','case','break','continue','return','def','import','package','new']),makefile:new Set(['include','ifeq','ifneq','ifdef','ifndef','else','endif']),cmake:new Set(['if','else','elseif','endif','foreach','endforeach','while','endwhile','function','endfunction','macro','endmacro','set','add_executable','add_library'])};
22690
+ const CODE_KEYWORDS={default:new Set(['if','else','for','while','switch','case','break','continue','return','function','class','import','export','from','try','catch','finally','throw','new','const','let','var','public','private','protected','static','async','await']),python:new Set(['def','class','if','elif','else','for','while','try','except','finally','raise','return','yield','import','from','as','with','pass','break','continue','lambda','global','nonlocal','assert','del','in','is','not','and','or','async','await']),javascript:new Set(['function','class','if','else','for','while','do','switch','case','break','continue','return','try','catch','finally','throw','new','this','const','let','var','import','export','from','default','extends','super','async','await','typeof','instanceof','in','of']),typescript:new Set(['interface','type','enum','implements','readonly','namespace','declare','keyof','infer','satisfies','as','extends','public','private','protected','abstract','override','function','class','if','else','for','while','switch','case','break','continue','return','try','catch','finally','throw','const','let','var','import','export','from','async','await']),java:new Set(['class','interface','enum','extends','implements','public','private','protected','static','final','abstract','volatile','synchronized','if','else','for','while','switch','case','break','continue','return','try','catch','finally','throw','new','package','import','instanceof','this','super','void']),c:new Set(['if','else','for','while','switch','case','break','continue','return','typedef','struct','union','enum','static','const','volatile','extern','inline','sizeof','#include','#define']),cpp:new Set(['if','else','for','while','switch','case','break','continue','return','class','struct','namespace','template','typename','public','private','protected','virtual','override','const','static','auto','constexpr','using','new','delete','this','throw','try','catch','#include','#define']),go:new Set(['package','import','func','type','struct','interface','map','chan','go','defer','select','if','else','for','switch','case','break','continue','return','fallthrough','range','const','var']),rust:new Set(['fn','let','mut','impl','trait','struct','enum','match','if','else','for','while','loop','break','continue','return','pub','use','mod','crate','self','super','where','async','await','move']),ruby:new Set(['def','class','module','if','elsif','else','unless','case','when','for','while','until','begin','rescue','ensure','return','yield','super','self','require','include','extend','end']),php:new Set(['function','class','interface','trait','public','private','protected','static','if','elseif','else','for','foreach','while','switch','case','break','continue','return','try','catch','finally','throw','namespace','use','new']),swift:new Set(['func','class','struct','enum','protocol','extension','if','else','guard','for','while','switch','case','break','continue','return','defer','do','catch','throw','try','import','let','var']),kotlin:new Set(['fun','class','interface','object','data','sealed','enum','if','else','when','for','while','do','break','continue','return','try','catch','throw','import','package','val','var','companion']),scala:new Set(['def','class','trait','object','case','if','else','for','while','match','break','continue','return','try','catch','throw','import','package','val','var','extends','with']),shell:new Set(['if','then','else','fi','for','do','done','while','case','esac','function','return','break','continue','export','local','readonly','in']),sql:new Set(['select','from','where','group','by','order','insert','into','values','update','set','delete','join','left','right','inner','outer','on','create','alter','drop','table','view','index','and','or','not','as','limit']),json:new Set([]),yaml:new Set([]),toml:new Set([]),ini:new Set([]),xml:new Set([]),csharp:new Set(['namespace','class','interface','struct','enum','public','private','protected','internal','static','readonly','const','if','else','for','foreach','while','switch','case','break','continue','return','using','new','this','base','async','await']),objectivec:new Set(['@interface','@implementation','@property','@synthesize','@end','if','else','for','while','switch','case','break','continue','return','#import']),r:new Set(['if','else','for','while','repeat','break','next','function','return','library']),perl:new Set(['if','elsif','else','for','foreach','while','last','next','sub','my','our','use','package','return']),lua:new Set(['if','then','else','elseif','end','for','while','repeat','until','break','function','local','return']),dart:new Set(['class','enum','extension','if','else','for','while','switch','case','break','continue','return','import','library','part','new','const','final','var','async','await']),groovy:new Set(['class','interface','trait','if','else','for','while','switch','case','break','continue','return','def','import','package','new']),makefile:new Set(['include','ifeq','ifneq','ifdef','ifndef','else','endif']),cmake:new Set(['if','else','elseif','endif','foreach','endforeach','while','endwhile','function','endfunction','macro','endmacro','set','add_executable','add_library']),fortran:new Set(['program','module','subroutine','function','end','use','implicit','none','integer','real','character','logical','complex','dimension','allocatable','intent','in','out','inout','do','if','then','else','elseif','endif','call','return','write','read','format','type','class','interface','contains','allocate','deallocate']),haskell:new Set(['module','where','import','qualified','as','hiding','data','type','newtype','class','instance','deriving','if','then','else','case','of','let','in','do','return','where','infixl','infixr','infix','forall','default']),erlang:new Set(['module','export','import','if','case','of','end','receive','after','when','fun','try','catch','throw','begin','andalso','orelse','not','band','bor','bxor','bnot','bsl','bsr']),elixir:new Set(['def','defp','defmodule','defmacro','defstruct','defprotocol','defimpl','if','else','unless','case','cond','do','end','fn','when','with','for','raise','rescue','import','use','alias','require']),ocaml:new Set(['let','in','if','then','else','match','with','fun','function','type','module','struct','sig','end','open','val','rec','and','or','not','begin','do','done','for','while','to','downto','mutable','ref']),vhdl:new Set(['library','use','entity','architecture','is','of','begin','end','signal','variable','constant','port','in','out','inout','process','if','then','else','elsif','case','when','for','generate','component','generic','map']),verilog:new Set(['module','endmodule','input','output','inout','wire','reg','assign','always','begin','end','if','else','case','endcase','for','while','parameter','localparam','initial','posedge','negedge','task','function']),asm:new Set([]),protobuf:new Set(['syntax','package','import','option','message','enum','service','rpc','returns','repeated','optional','required','map','oneof','reserved','extend']),hcl:new Set(['resource','data','variable','output','locals','module','provider','terraform','backend','required_providers','for_each','count','depends_on','lifecycle','dynamic','content','block']),zig:new Set(['const','var','fn','pub','return','if','else','for','while','break','continue','switch','struct','enum','union','error','defer','errdefer','try','catch','import','comptime','inline','test','unreachable']),nim:new Set(['proc','func','method','type','var','let','const','if','elif','else','case','of','for','while','break','continue','return','import','include','from','object','ref','ptr','template','macro','iterator','yield','discard']),julia:new Set(['function','end','if','elseif','else','for','while','break','continue','return','module','using','import','export','struct','mutable','abstract','type','const','let','do','begin','try','catch','finally','throw','macro','quote']),crystal:new Set(['def','class','module','struct','enum','if','elsif','else','unless','case','when','while','until','for','do','end','return','yield','begin','rescue','ensure','require','include','extend','abstract','private','protected']),dlang:new Set(['module','import','class','struct','enum','interface','if','else','for','foreach','while','do','switch','case','default','break','continue','return','void','auto','const','immutable','static','public','private','protected','override','template','mixin','alias']),clojure:new Set(['def','defn','defmacro','fn','let','if','cond','case','do','loop','recur','for','doseq','when','when-not','ns','require','use','import','try','catch','throw','finally']),lisp:new Set(['defun','defmacro','defvar','defparameter','defconstant','let','let*','if','cond','case','when','unless','lambda','progn','loop','do','dolist','dotimes','setq','setf','funcall','apply','require','provide']),racket:new Set(['define','lambda','let','let*','letrec','if','cond','case','when','unless','begin','do','for','for/list','for/hash','match','struct','class','require','provide','module','import','export']),pascal:new Set(['program','unit','uses','interface','implementation','begin','end','var','const','type','procedure','function','if','then','else','for','to','downto','while','repeat','until','case','of','with','record','class','array','set','nil']),wgsl:new Set(['fn','var','let','const','struct','if','else','for','while','loop','break','continue','return','switch','case','default','override','enable','type','alias','discard','continuing','fallthrough']),glsl:new Set(['void','float','int','bool','vec2','vec3','vec4','mat2','mat3','mat4','sampler2D','uniform','varying','attribute','in','out','inout','if','else','for','while','do','break','continue','return','struct','const','precision','highp','mediump','lowp']),hlsl:new Set(['float','float2','float3','float4','int','bool','void','struct','cbuffer','Texture2D','SamplerState','if','else','for','while','do','break','continue','return','in','out','inout','uniform','static','const','register','semantic']),prisma:new Set(['model','enum','datasource','generator','type','relation','default','unique','id','map','index','ignore','updatedAt']),graphql:new Set(['type','query','mutation','subscription','input','enum','interface','union','scalar','schema','fragment','on','directive','extend','implements'])};
21660
22691
  S.staticMode=STATIC_UI;
21661
22692
  const COMPACT_AUTO_REFRESH_COUNT=3;
21662
22693
  const COMPACT_AUTO_REFRESH_INTERVAL_MS=260;
@@ -21820,18 +22851,18 @@ function renderCtxLive(snap){const box=E('ctxLive');const textEl=E('ctxLiveText'
21820
22851
  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)}
21821
22852
  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():''}
21822
22853
  function isRenderRuntimeEventType(evtType){return RENDER_EVT_TYPES.has(String(evtType||''))}
21823
- function pullRenderState(id,force=false){const sid=String(id||S.activeId||'').trim();if(!sid||S.renderStateInFlight)return;const now=Date.now();if(!force&&now-Number(S.lastRenderStatePullAt||0)<1200)return;S.lastRenderStatePullAt=now;S.renderStateInFlight=true;api('/api/sessions/'+sid+'/render-state').then(st=>{if(!st||typeof st!=='object')return;const frame=st?.frame;if(frame&&typeof frame==='object'){_renderBridgeEnqueue(frame);return}const seq=Number(st?.seq||0);if(seq<=0)return;const kind=String(st?.last_kind||st?.latest?.kind||'generic');const latest=st?.latest||{};const lines=Number(latest?.lines||0);const points=Number(latest?.points||0);_renderBridgeShow();_renderBridgeUpdateMeta(`render seq=${seq} · kind=${kind} · lines=${lines} · points=${points}`,true);_renderBridgeHideLater(90000)}).catch(()=>{}).finally(()=>{S.renderStateInFlight=false})}
22854
+ function pullRenderState(id,force=false){const sid=String(id||S.activeId||'').trim();if(!sid||S.renderStateInFlight)return;const now=Date.now();if(!force&&now-Number(S.lastRenderStatePullAt||0)<1200)return;S.lastRenderStatePullAt=now;S.renderStateInFlight=true;api('/api/sessions/'+sid+'/render-state').then(st=>{if(!st||typeof st!=='object')return;const frame=st?.frame;if(frame&&typeof frame==='object'){_renderBridgeEnqueue(frame);return}const seq=Number(st?.seq||0);if(seq<=0)return;const kind=String(st?.last_kind||st?.latest?.kind||'generic');const latest=st?.latest||{};const lines=Number(latest?.lines||0);const points=Number(latest?.points||0);_renderBridgeShow();_renderBridgeUpdateMeta(`render seq=${seq} · kind=${kind} · lines=${lines} · points=${points}`,true);_renderBridgeHideLater(30000)}).catch(()=>{}).finally(()=>{S.renderStateInFlight=false})}
21824
22855
  function _renderBridgeShow(){const wrap=E('renderBridge');if(!wrap)return null;wrap.classList.remove('hidden');if(RENDER.hideTimer){clearTimeout(RENDER.hideTimer);RENDER.hideTimer=0}return wrap}
21825
- function _renderBridgeHideLater(ms=90000){if(RENDER.hideTimer){clearTimeout(RENDER.hideTimer);RENDER.hideTimer=0}RENDER.hideTimer=setTimeout(()=>{const wrap=E('renderBridge');if(wrap)wrap.classList.add('hidden')},Math.max(4000,Number(ms)||90000))}
22856
+ function _renderBridgeHideLater(ms=30000){if(RENDER.hideTimer){clearTimeout(RENDER.hideTimer);RENDER.hideTimer=0}RENDER.hideTimer=setTimeout(()=>{const wrap=E('renderBridge');if(wrap)wrap.classList.add('hidden')},Math.max(4000,Number(ms)||30000))}
21826
22857
  function _renderBridgeEnsureCanvas(){const canvas=E('renderCanvas');if(!canvas)return null;const wrap=_renderBridgeShow();if(!wrap)return null;const ctx=(canvas.getContext&&canvas.getContext('2d'))?canvas.getContext('2d'):null;if(!ctx)return null;const dpr=Math.max(1,Math.min(3,window.devicePixelRatio||1));const rect=canvas.getBoundingClientRect();const w=Math.max(260,Math.floor(rect.width||canvas.clientWidth||canvas.width||960));const h=Math.max(120,Math.floor(rect.height||canvas.clientHeight||canvas.height||220));const bw=Math.max(260,Math.floor(w*dpr));const bh=Math.max(120,Math.floor(h*dpr));if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;ctx.setTransform(1,0,0,1,0,0);ctx.scale(dpr,dpr)}RENDER.canvas=canvas;RENDER.ctx=ctx;return{canvas,ctx,w,h}}
21827
22858
  function _renderBridgeSafeNum(v,def=0,min=-1e6,max=1e6){const n=Number(v);if(!Number.isFinite(n))return def;return Math.max(min,Math.min(max,n))}
21828
22859
  function _renderBridgeColor(raw,fallback='#1f6feb'){const s=String(raw||'').trim();if(/^#[0-9a-fA-F]{3,8}$/.test(s))return s;if(/^rgba?\\(([^)]+)\\)$/.test(s))return s;if(/^hsla?\\(([^)]+)\\)$/.test(s))return s;return fallback}
21829
22860
  function _renderBridgeUpdateMeta(text,force=false){const meta=E('renderMeta');if(!meta)return;const now=Date.now();if(!force&&now-Number(RENDER.lastMetaAt||0)<RENDER_META_MIN_INTERVAL_MS)return;const txt=String(text||'').trim();if(!txt)return;RENDER.lastMetaAt=now;RENDER.lastSummary=txt;meta.textContent=txt}
21830
22861
  function _renderBridgeDrawImage(frame,ctx,w,h){const b64=String(frame?.image_b64||'').trim();if(!b64)return;const mime=String(frame?.mime||'image/png').trim()||'image/png';const src=`data:${mime};base64,${b64}`;const ticket=Number(RENDER.imgTicket||0)+1;RENDER.imgTicket=ticket;const img=new Image();img.onload=()=>{if(ticket!==Number(RENDER.imgTicket||0))return;ctx.drawImage(img,0,0,w,h)};img.onerror=()=>{};img.src=src}
21831
- function _renderBridgeDrawFrame(frame){const ready=_renderBridgeEnsureCanvas();if(!ready)return;const {ctx,w,h}=ready;const srcW=Math.max(1,_renderBridgeSafeNum(frame?.width,0,0,8192));const srcH=Math.max(1,_renderBridgeSafeNum(frame?.height,0,0,8192));const sx=(srcW>0)?(w/srcW):1;const sy=(srcH>0)?(h/srcH):1;const clear=!!frame?.clear||!RENDER.lastPaintAt;if(clear){ctx.clearRect(0,0,w,h)}const bg=String(frame?.bg||'').trim();if(bg){ctx.fillStyle=_renderBridgeColor(bg,'#ffffff');ctx.fillRect(0,0,w,h)}if(String(frame?.image_b64||'').trim()){_renderBridgeDrawImage(frame,ctx,w,h)}const lines=Array.isArray(frame?.lines)?frame.lines:[];if(lines.length){for(const line of lines){const pts=Array.isArray(line?.points)?line.points:[];if(pts.length<2)continue;ctx.beginPath();const p0=pts[0];const x0=_renderBridgeSafeNum(Array.isArray(p0)?p0[0]:0,0)*sx;const y0=_renderBridgeSafeNum(Array.isArray(p0)?p0[1]:0,0)*sy;ctx.moveTo(x0,y0);for(let i=1;i<pts.length;i++){const p=pts[i];ctx.lineTo(_renderBridgeSafeNum(Array.isArray(p)?p[0]:0,0)*sx,_renderBridgeSafeNum(Array.isArray(p)?p[1]:0,0)*sy)}ctx.strokeStyle=_renderBridgeColor(line?.color,'#1f6feb');ctx.globalAlpha=_renderBridgeSafeNum(line?.alpha,1,0,1);ctx.lineWidth=_renderBridgeSafeNum(line?.width,1.6,0.1,60);ctx.stroke();ctx.globalAlpha=1}}const points=Array.isArray(frame?.points)?frame.points:[];if(points.length){for(const p of points){ctx.beginPath();ctx.arc(_renderBridgeSafeNum(p?.x,0)*sx,_renderBridgeSafeNum(p?.y,0)*sy,_renderBridgeSafeNum(p?.size,1.6,0.1,40),0,Math.PI*2);ctx.fillStyle=_renderBridgeColor(p?.color,'#1a7f64');ctx.globalAlpha=_renderBridgeSafeNum(p?.alpha,1,0,1);ctx.fill();ctx.globalAlpha=1}}const txt=String(frame?.text||'').trim();if(txt){ctx.fillStyle='#26364d';ctx.font='12px ui-monospace, SFMono-Regular, Menlo, monospace';ctx.fillText(txt.slice(0,240),8,18)}RENDER.lastPaintAt=Date.now();RENDER.lastSeq=Math.max(Number(RENDER.lastSeq||0),Number(frame?.seq||0));const lineCount=Array.isArray(frame?.lines)?frame.lines.length:0;const pointCount=Array.isArray(frame?.points)?frame.points.length:0;const kind=String(frame?.kind||'generic');_renderBridgeUpdateMeta(`render seq=${RENDER.lastSeq} · kind=${kind} · lines=${lineCount} · points=${pointCount}`,true);_renderBridgeHideLater(90000)}
22862
+ function _renderBridgeDrawFrame(frame){const ready=_renderBridgeEnsureCanvas();if(!ready)return;const {ctx,w,h}=ready;const srcW=Math.max(1,_renderBridgeSafeNum(frame?.width,0,0,8192));const srcH=Math.max(1,_renderBridgeSafeNum(frame?.height,0,0,8192));const sx=(srcW>0)?(w/srcW):1;const sy=(srcH>0)?(h/srcH):1;const clear=!!frame?.clear||!RENDER.lastPaintAt;if(clear){ctx.clearRect(0,0,w,h)}const bg=String(frame?.bg||'').trim();if(bg){ctx.fillStyle=_renderBridgeColor(bg,'#ffffff');ctx.fillRect(0,0,w,h)}if(String(frame?.image_b64||'').trim()){_renderBridgeDrawImage(frame,ctx,w,h)}const lines=Array.isArray(frame?.lines)?frame.lines:[];if(lines.length){for(const line of lines){const pts=Array.isArray(line?.points)?line.points:[];if(pts.length<2)continue;ctx.beginPath();const p0=pts[0];const x0=_renderBridgeSafeNum(Array.isArray(p0)?p0[0]:0,0)*sx;const y0=_renderBridgeSafeNum(Array.isArray(p0)?p0[1]:0,0)*sy;ctx.moveTo(x0,y0);for(let i=1;i<pts.length;i++){const p=pts[i];ctx.lineTo(_renderBridgeSafeNum(Array.isArray(p)?p[0]:0,0)*sx,_renderBridgeSafeNum(Array.isArray(p)?p[1]:0,0)*sy)}ctx.strokeStyle=_renderBridgeColor(line?.color,'#1f6feb');ctx.globalAlpha=_renderBridgeSafeNum(line?.alpha,1,0,1);ctx.lineWidth=_renderBridgeSafeNum(line?.width,1.6,0.1,60);ctx.stroke();ctx.globalAlpha=1}}const points=Array.isArray(frame?.points)?frame.points:[];if(points.length){for(const p of points){ctx.beginPath();ctx.arc(_renderBridgeSafeNum(p?.x,0)*sx,_renderBridgeSafeNum(p?.y,0)*sy,_renderBridgeSafeNum(p?.size,1.6,0.1,40),0,Math.PI*2);ctx.fillStyle=_renderBridgeColor(p?.color,'#1a7f64');ctx.globalAlpha=_renderBridgeSafeNum(p?.alpha,1,0,1);ctx.fill();ctx.globalAlpha=1}}const txt=String(frame?.text||'').trim();if(txt){ctx.fillStyle='#26364d';ctx.font='12px ui-monospace, SFMono-Regular, Menlo, monospace';ctx.fillText(txt.slice(0,240),8,18)}RENDER.lastPaintAt=Date.now();RENDER.lastSeq=Math.max(Number(RENDER.lastSeq||0),Number(frame?.seq||0));const lineCount=Array.isArray(frame?.lines)?frame.lines.length:0;const pointCount=Array.isArray(frame?.points)?frame.points.length:0;const kind=String(frame?.kind||'generic');_renderBridgeUpdateMeta(`render seq=${RENDER.lastSeq} · kind=${kind} · lines=${lineCount} · points=${pointCount}`,true);_renderBridgeHideLater(30000)}
21832
22863
  function _renderBridgeDrain(){RENDER.raf=0;if(!Array.isArray(RENDER.queue)||!RENDER.queue.length)return;const latest=RENDER.queue[RENDER.queue.length-1]||{};RENDER.queue.length=0;_renderBridgeDrawFrame(latest);if(RENDER.queue.length){RENDER.raf=requestAnimationFrame(_renderBridgeDrain)}}
21833
22864
  function _renderBridgeEnqueue(frame){if(!frame||typeof frame!=='object')return;_renderBridgeShow();if(!Array.isArray(RENDER.queue))RENDER.queue=[];RENDER.queue.push(frame);if(RENDER.queue.length>RENDER_QUEUE_MAX){RENDER.queue=RENDER.queue.slice(-Math.floor(RENDER_QUEUE_MAX*0.6))}const summary=String(frame?.summary||'').trim();if(summary){_renderBridgeUpdateMeta(summary,false)}if(!RENDER.raf){RENDER.raf=requestAnimationFrame(_renderBridgeDrain)}}
21834
- function _renderBridgeSyncFromSnapshot(snap){const rb=snap?.render_bridge||null;if(!rb||typeof rb!=='object')return;const seq=Number(rb?.seq||0);if(seq<=0)return;_renderBridgeShow();const kind=String(rb?.last_kind||rb?.latest?.kind||'generic');const latest=rb?.latest||{};const lines=Number(latest?.lines||0);const points=Number(latest?.points||0);_renderBridgeUpdateMeta(`render seq=${seq} · kind=${kind} · lines=${lines} · points=${points}`,true);_renderBridgeHideLater(90000)}
22865
+ function _renderBridgeSyncFromSnapshot(snap){const rb=snap?.render_bridge||null;if(!rb||typeof rb!=='object')return;const seq=Number(rb?.seq||0);if(seq<=0)return;_renderBridgeShow();const kind=String(rb?.last_kind||rb?.latest?.kind||'generic');const latest=rb?.latest||{};const lines=Number(latest?.lines||0);const points=Number(latest?.points||0);_renderBridgeUpdateMeta(`render seq=${seq} · kind=${kind} · lines=${lines} · points=${points}`,true);_renderBridgeHideLater(30000)}
21835
22866
  function _deltaEnsureSnapshot(){if(!S.snap||typeof S.snap!=='object')return false;if(!Array.isArray(S.snap.messages))S.snap.messages=[];if(!Array.isArray(S.snap.conversation_feed))S.snap.conversation_feed=[];if(!Array.isArray(S.snap.activity))S.snap.activity=[];if(!Array.isArray(S.snap.operations))S.snap.operations=[];if(!Array.isArray(S.snap.uploads))S.snap.uploads=[];if(!Array.isArray(S.snap.todos))S.snap.todos=[];if(!Array.isArray(S.snap.tasks))S.snap.tasks=[];return true}
21836
22867
  function _deltaPushLimited(arr,row,maxCount){if(!Array.isArray(arr))return;if(!row||typeof row!=='object')return;arr.push(row);const maxN=Math.max(20,Number(maxCount)||120);if(arr.length>maxN){arr.splice(0,arr.length-maxN)}}
21837
22868
  function _deltaAdoptAgentRole(data){if(!_deltaEnsureSnapshot())return'';const role=_chatVirtAgentRoleKey(data?.agent_role);if(!role)return'';S.snap.agent_active_role=role;return role}
@@ -22016,7 +23047,7 @@ function onRuntimeEvent(evt){
22016
23047
  if(seqState.gap)return{handled:true,needsSnapshot:true};
22017
23048
  const typ=String(evt.type||'');
22018
23049
  if(typ==='render_frame'){_renderBridgeEnqueue(evt.data||{});return{handled:true,needsSnapshot:false}}
22019
- if(typ==='render_bridge'){const d=evt.data||{};const summary=String(d?.summary||'').trim();if(summary){_renderBridgeShow();_renderBridgeUpdateMeta(summary,true);_renderBridgeHideLater(90000)}return{handled:true,needsSnapshot:false}}
23050
+ if(typ==='render_bridge'){const d=evt.data||{};const summary=String(d?.summary||'').trim();if(summary){_renderBridgeShow();_renderBridgeUpdateMeta(summary,true);_renderBridgeHideLater(30000)}return{handled:true,needsSnapshot:false}}
22020
23051
  if(typ==='compact'){scheduleCompactRefreshBurst(COMPACT_AUTO_REFRESH_COUNT);const reason=parseCompactReason(evt.data||{});if(reason==='auto'||reason.startsWith('truncation-rescue')){const pct=Number(evt.data?.context_left_percent_before);const left=Number(evt.data?.context_left_before);const limit=Number(evt.data?.context_limit_before);const pctTxt=Number.isFinite(pct)?pct.toFixed(1):'-';const leftTxt=Number.isFinite(left)&&Number.isFinite(limit)?`${left}/${limit}`:'-';showCompactToast(`${t('compact_auto')}:${pctTxt}% left (${leftTxt}) · delta-sync`)}_deltaAppendActivity(typ,evt.data||{},Number(evt?.ts||Date.now()/1000));_deltaScheduleRender({boards:true,sessions:true});return{handled:true,needsSnapshot:false}}
22021
23052
  return _deltaApplyRuntimeEvent(evt);
22022
23053
  }
@@ -22047,7 +23078,7 @@ function _deltaStartWatchdog(){
22047
23078
  };
22048
23079
  S.deltaWatchdogTimer=setTimeout(tick,DELTA_WATCHDOG_INTERVAL_MS);
22049
23080
  }
22050
- 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');if(offline){link.removeAttribute('target');link.removeAttribute('rel')}else{link.setAttribute('target','_blank');link.setAttribute('rel','noreferrer')}}
23081
+ 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')}
22051
23082
  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('|')}
22052
23083
  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}`}
22053
23084
  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('|')}
@@ -22066,11 +23097,12 @@ function _scrollContainerToNodeCenter(container,target){
22066
23097
  }
22067
23098
  function _bindNestedScrollGuards(root){
22068
23099
  if(!root)return;
23100
+ const SEL='.msg-diff-shell,.msg-code-shell,.preview-code-scroll,.md-code';
22069
23101
  const nodes=[];
22070
- if(root.matches&&root.matches('.msg-diff-shell,.msg-code-shell,.preview-code-scroll')){
23102
+ if(root.matches&&root.matches(SEL)){
22071
23103
  nodes.push(root);
22072
23104
  }
22073
- for(const n of root.querySelectorAll('.msg-diff-shell,.msg-code-shell,.preview-code-scroll')){
23105
+ for(const n of root.querySelectorAll(SEL)){
22074
23106
  nodes.push(n);
22075
23107
  }
22076
23108
  const markManualCenterOff=(node)=>{
@@ -22087,6 +23119,17 @@ function _bindNestedScrollGuards(root){
22087
23119
  if(key)S.diffCenterDisabled[key]=1;
22088
23120
  }
22089
23121
  };
23122
+ const markChatUserScrolling=()=>{
23123
+ const chatEl=E('chat');
23124
+ if(!chatEl)return;
23125
+ const now=Date.now();
23126
+ chatEl._virtManualUnlockTs=Math.max(
23127
+ Number(chatEl._virtManualUnlockTs||0),
23128
+ now+CHAT_SCROLL_LOCK_MS
23129
+ );
23130
+ S.follow.chat=false;
23131
+ chatEl._virtAutoFollowPaused=true;
23132
+ };
22090
23133
  for(const node of nodes){
22091
23134
  if(!node||node._nestedScrollGuardBound)continue;
22092
23135
  node._nestedScrollGuardBound=true;
@@ -22097,16 +23140,38 @@ function _bindNestedScrollGuards(root){
22097
23140
  const maxLeft=Math.max(0,Number(node.scrollWidth||0)-Number(node.clientWidth||0));
22098
23141
  const top=Number(node.scrollTop||0);
22099
23142
  const left=Number(node.scrollLeft||0);
22100
- const canY=(dy<0&&top>0)||(dy>0&&top<maxTop);
22101
- const canX=(dx<0&&left>0)||(dx>0&&left<maxLeft);
23143
+ const canY=(dy<0&&top>0.5)||(dy>0&&top<maxTop-0.5);
23144
+ const canX=(dx<0&&left>0.5)||(dx>0&&left<maxLeft-0.5);
22102
23145
  markManualCenterOff(node);
22103
23146
  if(canY||canX){
22104
23147
  ev.stopPropagation();
23148
+ markChatUserScrolling();
22105
23149
  }
23150
+ },{passive:false});
23151
+ node.addEventListener('mousedown',()=>{markManualCenterOff(node);markChatUserScrolling();},{passive:true});
23152
+ node.addEventListener('touchstart',ev=>{
23153
+ markManualCenterOff(node);
23154
+ markChatUserScrolling();
23155
+ node._touchStartY=Number(ev.touches?.[0]?.clientY||0);
23156
+ node._touchStartX=Number(ev.touches?.[0]?.clientX||0);
22106
23157
  },{passive:true});
22107
- node.addEventListener('mousedown',()=>{markManualCenterOff(node);},{passive:true});
22108
- node.addEventListener('touchstart',()=>{markManualCenterOff(node);},{passive:true});
22109
- node.addEventListener('touchmove',ev=>{markManualCenterOff(node);ev.stopPropagation();},{passive:true});
23158
+ node.addEventListener('touchmove',ev=>{
23159
+ markManualCenterOff(node);
23160
+ const curY=Number(ev.touches?.[0]?.clientY||0);
23161
+ const curX=Number(ev.touches?.[0]?.clientX||0);
23162
+ const dy=curY-(node._touchStartY||0);
23163
+ const dx=curX-(node._touchStartX||0);
23164
+ const maxTop=Math.max(0,Number(node.scrollHeight||0)-Number(node.clientHeight||0));
23165
+ const maxLeft=Math.max(0,Number(node.scrollWidth||0)-Number(node.clientWidth||0));
23166
+ const top=Number(node.scrollTop||0);
23167
+ const left=Number(node.scrollLeft||0);
23168
+ const canY=(dy>0&&top>0.5)||(dy<0&&top<maxTop-0.5);
23169
+ const canX=(dx>0&&left>0.5)||(dx<0&&left<maxLeft-0.5);
23170
+ if(canY||canX){
23171
+ ev.stopPropagation();
23172
+ }
23173
+ markChatUserScrolling();
23174
+ },{passive:false});
22110
23175
  }
22111
23176
  }
22112
23177
  function _centerDiffShellToHotspot(root){
@@ -22136,8 +23201,7 @@ function _centerDiffShellToHotspot(root){
22136
23201
  const target=lines[bestCenter];
22137
23202
  if(!target)return;
22138
23203
  if(msgKey)shell.setAttribute('data-centered-key',msgKey);
22139
- const run=()=>{try{_scrollContainerToNodeCenter(shell,target);if(msgKey)S.diffCenteredDone[msgKey]=1;}catch(_){}};
22140
- if(typeof requestAnimationFrame==='function'){requestAnimationFrame(run)}else{run()}
23204
+ try{_scrollContainerToNodeCenter(shell,target);if(msgKey)S.diffCenteredDone[msgKey]=1;}catch(_){}
22141
23205
  }
22142
23206
  function splitTableRow(line){const src=String(line||'').trim().replace(/^\\|/,'').replace(/\\|$/,'');if(!src)return[];return src.split('|').map(x=>String(x||'').trim())}
22143
23207
  function isTableSeparator(line){const cells=splitTableRow(line);if(!cells.length)return false;return cells.every(cell=>/^:?-{3,}:?$/.test(cell))}
@@ -22509,7 +23573,7 @@ function _previewRenderStageSelector(tab,stages,selectedReq,payload=null){
22509
23573
  stat.textContent=`stage ${idx}/${total} · +${add}/-${del}${lineTail}`;
22510
23574
  }
22511
23575
  function _previewLangFromPath(path){const rel=normalizePreviewPath(path).toLowerCase();const name=rel.split('/').pop()||'';const dot=name.lastIndexOf('.');const ext=dot>=0?name.slice(dot):'';return CODE_LANG_BY_EXT[ext]||CODE_LANG_BY_NAME[name]||'default'}
22512
- function _codeLangConfig(lang){const v=String(lang||'default');if(v==='python'||v==='shell'||v==='ruby'||v==='yaml'||v==='toml'||v==='ini'||v==='r'||v==='perl'||v==='makefile'||v==='cmake')return{hashComment:true,slashComment:false,dashComment:false,blockComment:false,xmlComment:false,backtick:false};if(v==='sql')return{hashComment:false,slashComment:false,dashComment:true,blockComment:true,xmlComment:false,backtick:false};if(v==='xml')return{hashComment:false,slashComment:false,dashComment:false,blockComment:false,xmlComment:true,backtick:false};if(v==='json')return{hashComment:false,slashComment:false,dashComment:false,blockComment:false,xmlComment:false,backtick:false};return{hashComment:false,slashComment:true,dashComment:false,blockComment:true,xmlComment:false,backtick:true}}
23576
+ function _codeLangConfig(lang){const v=String(lang||'default');if(v==='python'||v==='shell'||v==='ruby'||v==='yaml'||v==='toml'||v==='ini'||v==='r'||v==='perl'||v==='makefile'||v==='cmake'||v==='nim'||v==='julia'||v==='elixir'||v==='crystal')return{hashComment:true,slashComment:false,dashComment:false,blockComment:false,xmlComment:false,backtick:false};if(v==='sql'||v==='haskell')return{hashComment:false,slashComment:false,dashComment:true,blockComment:true,xmlComment:false,backtick:false};if(v==='xml'||v==='vhdl')return{hashComment:false,slashComment:false,dashComment:true,blockComment:false,xmlComment:true,backtick:false};if(v==='json'||v==='fortran'||v==='asm'||v==='protobuf'||v==='prisma'||v==='graphql'||v==='wgsl')return{hashComment:false,slashComment:false,dashComment:false,blockComment:false,xmlComment:false,backtick:false};if(v==='lisp'||v==='clojure'||v==='racket')return{hashComment:false,slashComment:false,dashComment:false,blockComment:false,xmlComment:false,backtick:false};return{hashComment:false,slashComment:true,dashComment:false,blockComment:true,xmlComment:false,backtick:true}}
22513
23577
  function _codeWordSet(lang){return CODE_KEYWORDS[String(lang||'default')]||CODE_KEYWORDS.default}
22514
23578
  function _isWordStart(ch){return /[A-Za-z_$]/.test(ch)}
22515
23579
  function _isWordChar(ch){return /[A-Za-z0-9_$]/.test(ch)}
@@ -22913,8 +23977,7 @@ function _scrollCodePreviewToAnchor(body,anchorLine){
22913
23977
  target=body.querySelector('.code-row.code-add,.code-row.code-delete')||rows[0];
22914
23978
  }
22915
23979
  if(!target)return;
22916
- const run=()=>{try{_scrollContainerToNodeCenter(scrollWrap,target);if(previewKey)S.previewCenteredDone[previewKey]=1;}catch(_){}};
22917
- if(typeof requestAnimationFrame==='function'){requestAnimationFrame(run)}else{run()}
23980
+ try{_scrollContainerToNodeCenter(scrollWrap,target);if(previewKey)S.previewCenteredDone[previewKey]=1;}catch(_){}
22918
23981
  }
22919
23982
  async function _renderCodePreviewTab(tab,body,forceReload=false){
22920
23983
  const ticket=String(++S.previewNonce);
@@ -23032,7 +24095,7 @@ function _chatVirtRowKey(row,idx){const r=row||{};const txt=String(r.text||'');c
23032
24095
  function _chatVirtFormatElapsed(seconds){const sec=Math.max(0,Math.floor(Number(seconds)||0));const h=Math.floor(sec/3600);const m=Math.floor((sec%3600)/60);const s=sec%60;if(h>0)return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;return `${m}:${String(s).padStart(2,'0')}`}
23033
24096
  function _chatVirtLiveRunText(label,elapsed){return `${t('running')} · ${_chatVirtFormatElapsed(elapsed)}`}
23034
24097
  function _chatVirtStopRunTicker(chatEl){if(!chatEl)return;const timer=Number(chatEl._virtRunTicker||0);if(timer){clearInterval(timer);chatEl._virtRunTicker=0}}
23035
- function _chatVirtTickRunNotice(chatEl){if(!chatEl)return;const runActive=!!(S.snap?.running&&S.snap?.live_run_notice_active);if(!runActive){_chatVirtStopRunTicker(chatEl);return}const nodes=chatEl.querySelectorAll('.msg[data-run-live=\"1\"]');if(!nodes.length){_chatVirtStopRunTicker(chatEl);return}const now=(Date.now()/1000);for(const node of nodes){const pre=node.querySelector('pre');if(!pre)continue;const label=String(node.getAttribute('data-run-label')||'model call');const startedAt=Number(node.getAttribute('data-run-start')||0);const baseElapsed=Math.max(0,Number(node.getAttribute('data-run-elapsed')||0));const anchorTs=Number(node.getAttribute('data-run-anchor')||0);let elapsed=baseElapsed;if(startedAt>0){elapsed=Math.max(baseElapsed,now-startedAt)}else if(anchorTs>0){elapsed=Math.max(0,baseElapsed+(now-anchorTs))}const whole=Math.floor(elapsed);if(String(node.getAttribute('data-run-last-sec')||'')===String(whole))continue;node.setAttribute('data-run-last-sec',String(whole));pre.textContent=_chatVirtLiveRunText(label,elapsed)}}
24098
+ function _chatVirtTickRunNotice(chatEl){if(!chatEl)return;const runActive=!!(S.snap?.running&&S.snap?.live_run_notice_active);if(!runActive){_chatVirtStopRunTicker(chatEl);return}if(!chatEl._virtRunNodes||!chatEl._virtRunNodes.length){chatEl._virtRunNodes=Array.from(chatEl.querySelectorAll('.msg[data-run-live="1"]'))}const nodes=chatEl._virtRunNodes;if(!nodes.length){_chatVirtStopRunTicker(chatEl);return}const now=(Date.now()/1000);for(const node of nodes){const pre=node.querySelector('pre');if(!pre)continue;const label=String(node.getAttribute('data-run-label')||'model call');const startedAt=Number(node.getAttribute('data-run-start')||0);const baseElapsed=Math.max(0,Number(node.getAttribute('data-run-elapsed')||0));const anchorTs=Number(node.getAttribute('data-run-anchor')||0);let elapsed=baseElapsed;if(startedAt>0){elapsed=Math.max(baseElapsed,now-startedAt)}else if(anchorTs>0){elapsed=Math.max(0,baseElapsed+(now-anchorTs))}const whole=Math.floor(elapsed);if(String(node.getAttribute('data-run-last-sec')||'')===String(whole))continue;node.setAttribute('data-run-last-sec',String(whole));pre.textContent=_chatVirtLiveRunText(label,elapsed)}}
23036
24099
  function _chatVirtSyncRunTicker(chatEl){if(!chatEl)return;const hasRun=!!chatEl.querySelector('.msg[data-run-live=\"1\"]');if(!hasRun){_chatVirtStopRunTicker(chatEl);return}_chatVirtTickRunNotice(chatEl);if(!chatEl._virtRunTicker){chatEl._virtRunTicker=setInterval(()=>_chatVirtTickRunNotice(chatEl),1000)}}
23037
24100
  function _chatVirtCollectRows(){
23038
24101
  const feed=Array.isArray(S.snap?.conversation_feed)?S.snap.conversation_feed:(Array.isArray(S.snap?.messages)?S.snap.messages:[]);
@@ -23101,7 +24164,7 @@ function _chatVirtAcquireNode(kind){const key=String(kind||'text');const pool=_c
23101
24164
  function _chatVirtReleaseNode(node){
23102
24165
  if(!node)return;
23103
24166
  const key=String(node.getAttribute('data-pool-kind')||'text');
23104
- if(Number(CHAT_VIRT.poolSize||0)>=Number(CHAT_VIRT.poolMax||420))return;
24167
+ if(Number(CHAT_VIRT.poolSize||0)>=Number(CHAT_VIRT.poolMax||180))return;
23105
24168
  if(S.mathObserver){
23106
24169
  try{S.mathObserver.unobserve(node)}catch(_){}
23107
24170
  }
@@ -23316,6 +24379,70 @@ function _chatVirtBuildMessageNode(m){
23316
24379
  return d;
23317
24380
  }
23318
24381
  function _chatVirtFindWindow(rows,top,bottom){const startTarget=Math.max(0,top-CHAT_VIRT.overscanPx);const endTarget=Math.max(0,bottom+CHAT_VIRT.overscanPx);let start=0;let acc=0;while(start<rows.length){const h=_chatVirtEstimatedHeight(rows[start]);if((acc+h)>=startTarget)break;acc+=h;start+=1}let end=start;let accEnd=acc;while(end<rows.length&&accEnd<=endTarget){accEnd+=_chatVirtEstimatedHeight(rows[end]);end+=1}end=Math.min(rows.length,end+2);return {start,end,topOffset:acc,endOffset:accEnd}}
24382
+ function _chatVirtReuseWindow(chatEl,rows,top,bottom){
24383
+ if(!chatEl||!Array.isArray(rows)||!rows.length)return null;
24384
+ const prevRows=Array.isArray(chatEl._virtLastRows)?chatEl._virtLastRows:[];
24385
+ const prevStart=Number(chatEl._virtLastWinStart||-1);
24386
+ const prevEnd=Number(chatEl._virtLastWinEnd||-1);
24387
+ const prevTopOffset=Number(chatEl._virtLastTopOffset);
24388
+ const prevEndOffset=Number(chatEl._virtLastEndOffset);
24389
+ if(prevStart<0||prevEnd<=prevStart)return null;
24390
+ if(!Number.isFinite(prevTopOffset)||!Number.isFinite(prevEndOffset)||prevEndOffset<=prevTopOffset)return null;
24391
+ if(prevRows.length!==rows.length)return null;
24392
+ const prevFirstKey=String(prevRows[prevStart]?._vk||'');
24393
+ const nextFirstKey=String(rows[prevStart]?._vk||'');
24394
+ const prevLastKey=String(prevRows[Math.max(0,prevEnd-1)]?._vk||'');
24395
+ const nextLastKey=String(rows[Math.max(0,prevEnd-1)]?._vk||'');
24396
+ if(prevFirstKey!==nextFirstKey||prevLastKey!==nextLastKey)return null;
24397
+ const viewport=Math.max(0,bottom-top);
24398
+ const innerPad=Math.max(120,Math.min(Math.round(CHAT_VIRT.overscanPx*0.45),Math.round(viewport*0.35)));
24399
+ if((prevEndOffset-prevTopOffset)<(viewport+(innerPad*2)))return null;
24400
+ const safeTop=prevTopOffset+innerPad;
24401
+ const safeBottom=prevEndOffset-innerPad;
24402
+ if(top>=safeTop&&bottom<=safeBottom){
24403
+ return {start:prevStart,end:prevEnd,topOffset:prevTopOffset,endOffset:prevEndOffset};
24404
+ }
24405
+ return null;
24406
+ }
24407
+ function _chatVirtReleaseRendered(root){if(!root)return;for(const node of root.querySelectorAll('.msg[data-vk]')){_chatVirtReleaseNode(node)}}
24408
+ function _chatVirtFindRenderedNode(chatEl,key){
24409
+ if(!chatEl||!key)return null;
24410
+ for(const node of chatEl.querySelectorAll('.msg[data-vk]')){
24411
+ if(String(node.getAttribute('data-vk')||'')===String(key||''))return node;
24412
+ }
24413
+ return null;
24414
+ }
24415
+ function _chatVirtCaptureAnchor(chatEl){
24416
+ if(!chatEl)return null;
24417
+ const viewportTop=Number(chatEl.getBoundingClientRect().top||0);
24418
+ let fallback=null;
24419
+ for(const node of chatEl.querySelectorAll('.msg[data-vk]')){
24420
+ const key=String(node.getAttribute('data-vk')||'').trim();
24421
+ if(!key)continue;
24422
+ const rect=node.getBoundingClientRect();
24423
+ const top=Number(rect.top||0)-viewportTop;
24424
+ const bottom=Number(rect.bottom||0)-viewportTop;
24425
+ const anchor={key:key,offset:top};
24426
+ if(!fallback)fallback=anchor;
24427
+ if(bottom>1)return anchor;
24428
+ }
24429
+ return fallback;
24430
+ }
24431
+ function _chatVirtRestoreAnchor(chatEl,anchor){
24432
+ if(!chatEl||!anchor||!anchor.key)return false;
24433
+ const node=_chatVirtFindRenderedNode(chatEl,anchor.key);
24434
+ if(!node)return false;
24435
+ const viewportTop=Number(chatEl.getBoundingClientRect().top||0);
24436
+ const rect=node.getBoundingClientRect();
24437
+ const currentOffset=Number(rect.top||0)-viewportTop;
24438
+ const delta=currentOffset-Number(anchor.offset||0);
24439
+ if(Math.abs(delta)<0.75)return true;
24440
+ const maxTop=Math.max(0,Number(chatEl.scrollHeight||0)-Number(chatEl.clientHeight||0));
24441
+ const target=Math.max(0,Math.min(Number(chatEl.scrollTop||0)+delta,maxTop));
24442
+ if(Math.abs(target-Number(chatEl.scrollTop||0))<0.75)return true;
24443
+ chatEl.scrollTop=target;
24444
+ return true;
24445
+ }
23319
24446
  function _chatVirtBindScroll(chatEl){
23320
24447
  if(chatEl._virtBound)return;
23321
24448
  chatEl._virtBound=true;
@@ -23385,28 +24512,11 @@ function _chatVirtBindScroll(chatEl){
23385
24512
  const now=Date.now();
23386
24513
  chatEl._virtLastWheelTs=now;
23387
24514
  chatEl._virtLastWheelDy=dy;
23388
- chatEl._virtInputUnlockTs=Math.max(
23389
- Number(chatEl._virtInputUnlockTs||0),
23390
- now+CHAT_SCROLL_INPUT_LOCK_MS
23391
- );
23392
- const atBottomBefore=nearBottom(chatEl,6);
23393
24515
  if(dy<0){
23394
- markManual(CHAT_SCROLL_LOCK_MS);
23395
- S.follow.chat=false;
23396
- return;
23397
- }
23398
- if(!atBottomBefore){
23399
- markManual(Math.round(CHAT_SCROLL_LOCK_MS*0.45));
23400
24516
  S.follow.chat=false;
23401
24517
  return;
23402
24518
  }
23403
- S.follow.chat=true;
23404
- chatEl._virtManualUnlockTs=0;
23405
- chatEl._virtInputUnlockTs=0;
23406
- chatEl._virtTouchUnlockTs=0;
23407
- if(atBottomBefore){
23408
- chatEl._virtAutoFollowPaused=false;
23409
- }
24519
+ if(nearBottom(chatEl,6))S.follow.chat=true;
23410
24520
  },{passive:true});
23411
24521
  chatEl.addEventListener('mousedown',()=>{markManual(Math.round(CHAT_SCROLL_LOCK_MS*0.9))},{passive:true});
23412
24522
  chatEl.addEventListener('touchstart',()=>{markTouchStart(CHAT_TOUCH_SCROLL_LOCK_MS)},{passive:true});
@@ -23420,9 +24530,7 @@ function _chatVirtBindScroll(chatEl){
23420
24530
  chatEl._virtScrollDirection=(curTop>prevTop)?1:((curTop<prevTop)?-1:0);
23421
24531
  chatEl._virtLastScrollTop=curTop;
23422
24532
  const atBottom=nearBottom(chatEl,6);
23423
- const manualLock=Number(chatEl._virtManualUnlockTs||0)>now;
23424
- const recentUpIntent=(now-Number(chatEl._virtLastWheelTs||0))<220&&Number(chatEl._virtLastWheelDy||0)<0;
23425
- if(atBottom&&!manualLock&&!recentUpIntent){
24533
+ if(atBottom){
23426
24534
  S.follow.chat=true;
23427
24535
  chatEl._virtManualUnlockTs=0;
23428
24536
  chatEl._virtInputUnlockTs=0;
@@ -23433,9 +24541,6 @@ function _chatVirtBindScroll(chatEl){
23433
24541
  if(S.snap?.running){
23434
24542
  chatEl._virtAutoFollowPaused=true;
23435
24543
  }
23436
- if(!atBottom||recentUpIntent){
23437
- chatEl._virtManualUnlockTs=Math.max(Number(chatEl._virtManualUnlockTs||0),now+CHAT_SCROLL_LOCK_MS);
23438
- }
23439
24544
  }
23440
24545
  scheduleScrollRender();
23441
24546
  });
@@ -23453,12 +24558,9 @@ function renderChat(reason='snapshot'){
23453
24558
  if((!S.snap?.running)&&atBottomNow){
23454
24559
  c._virtAutoFollowPaused=false;
23455
24560
  }
23456
- const now=Date.now();
23457
- const manualLock=Number(c._virtManualUnlockTs||0)>now;
23458
- const autoPaused=Boolean(c._virtAutoFollowPaused);
23459
- const scrolling=_chatVirtIsUserScrolling(c);
23460
- const keep=first||(!manualLock&&!autoPaused&&!scrolling&&(atBottomNow||Boolean(S.follow.chat)));
24561
+ const keep=first||Boolean(S.follow.chat)||atBottomNow;
23461
24562
  const oldScrollTop=Number(c.scrollTop||0);
24563
+ const anchor=(!keep&&!first)?_chatVirtCaptureAnchor(c):null;
23462
24564
  const feedSig=String(S.lastFeedSig||feedSignature(S.snap||{}));
23463
24565
  let rows=[];
23464
24566
  if(reason==='scroll'&&Array.isArray(c._virtRowsCacheRows)&&String(c._virtRowsCacheSig||'')===feedSig){
@@ -23474,7 +24576,7 @@ function renderChat(reason='snapshot'){
23474
24576
  const prevWinEnd=Number(c._virtLastWinEnd||-1);
23475
24577
  const top=Math.max(0,c.scrollTop);
23476
24578
  const bottom=top+Math.max(0,c.clientHeight||0);
23477
- const win=_chatVirtFindWindow(rows,top,bottom);
24579
+ const win=((reason==='scroll')?_chatVirtReuseWindow(c,rows,top,bottom):null)||_chatVirtFindWindow(rows,top,bottom);
23478
24580
  const totalKey=`${feedSig}|hv=${Number(CHAT_VIRT.heightVersion||0)}|rows=${rows.length}`;
23479
24581
  let totalEstimated=0;
23480
24582
  if(reason==='scroll'&&String(c._virtTotalKey||'')===totalKey){
@@ -23592,19 +24694,19 @@ function renderChat(reason='snapshot'){
23592
24694
  CHAT_VIRT.heightVersion=Number(CHAT_VIRT.heightVersion||0)+1;
23593
24695
  }
23594
24696
  }
23595
- if(reason!=='scroll'){
23596
- const maxTop=Math.max(0,c.scrollHeight-c.clientHeight);
23597
- if(keep){
23598
- c.scrollTop=maxTop;
23599
- }else{
23600
- c.scrollTop=Math.max(0,Math.min(oldScrollTop,maxTop));
23601
- }
24697
+ const maxTop=Math.max(0,c.scrollHeight-c.clientHeight);
24698
+ if(keep){
24699
+ c.scrollTop=maxTop;
24700
+ }else if(!(anchor&&_chatVirtRestoreAnchor(c,anchor))){
24701
+ c.scrollTop=Math.max(0,Math.min(oldScrollTop,maxTop));
23602
24702
  }
23603
24703
  c._chatHasRendered=true;
23604
24704
  c._virtRendering=false;
23605
24705
  c._virtLastRows=rows;
23606
24706
  c._virtLastWinStart=win.start;
23607
24707
  c._virtLastWinEnd=win.end;
24708
+ c._virtLastTopOffset=Number(win.topOffset||0);
24709
+ c._virtLastEndOffset=Number(win.endOffset||0);
23608
24710
  if(hasHeightChange&&reason!=='scroll'){
23609
24711
  if(c._virtMeasureRaf)cancelAnimationFrame(c._virtMeasureRaf);
23610
24712
  c._virtMeasureRaf=requestAnimationFrame(()=>{c._virtMeasureRaf=0;renderChat('measure')});
@@ -23654,8 +24756,14 @@ refreshFileExplorer(false).catch(()=>{});
23654
24756
  const uploads=(S.snap?.uploads||[]).slice(-8).reverse();
23655
24757
  E('uploadList').innerHTML=uploads.map(u=>`<div>${esc(u.filename)} → ${esc(u.workspace_path||'')} (${esc(u.kind||'')}, ${esc(u.size||0)}B)</div>`).join('')||`<div>${esc(t('no_uploads'))}</div>`;
23656
24758
  const sessionZip=S.activeId?('/api/sessions/'+S.activeId+'/export.zip'):'#';
24759
+ const sessionMd=S.activeId?('/api/sessions/'+S.activeId+'/export.md'):'#';
24760
+ const sessionPdf=S.activeId?('/api/sessions/'+S.activeId+'/export.pdf'):'#';
24761
+ const sessionPng=S.activeId?('/api/sessions/'+S.activeId+'/export.png'):'#';
23657
24762
  const dl1=E('downloadSessionBtn');
23658
- if(S.activeId){dl1.classList.remove('disabled');dl1.href=sessionZip}else{dl1.classList.add('disabled');dl1.href='#'}
24763
+ const dlMd=E('exportMdBtn');
24764
+ const dlPdf=E('exportPdfBtn');
24765
+ const dlPng=E('exportPngBtn');
24766
+ if(S.activeId){dl1.href=sessionZip;if(dlMd)dlMd.href=sessionMd;if(dlPdf)dlPdf.href=sessionPdf;if(dlPng)dlPng.href=sessionPng}else{dl1.href='#';if(dlMd)dlMd.href='#';if(dlPdf)dlPdf.href='#';if(dlPng)dlPng.href='#'}
23659
24767
  renderSkillsEntryLink()}
23660
24768
  function _normalizeModelCatalog(cat){const src=(cat&&typeof cat==='object')?cat:{};const options=Array.isArray(src.options)?src.options.map(it=>{const row=(it&&typeof it==='object')?it:{};const sel=String(row.selection||'').trim();const mdl=String(row.model||'').trim();if(!sel&&!mdl)return null;const profileId=String(row.profile_id||'').trim()||'profile';const selection=sel||`${profileId}::${mdl}`;return{...row,selection,label:String(row.label||selection)}}).filter(Boolean):[];const models=Array.isArray(src.models)?src.models.map(x=>String(x||'').trim()).filter(Boolean):[];const selected=String(src.selected||'').trim();const thinking=('thinking'in src)?!!src.thinking:null;return{options,models,selected,thinking}}
23661
24769
  function _modelNameFromSelection(selection){const raw=String(selection||'').trim();if(!raw)return'';if(raw.includes('::')){const parts=raw.split('::',2);return String(parts[1]||parts[0]||'').trim()}return raw}
@@ -23753,7 +24861,7 @@ function _chatVirtDebounceWhileScrolling(chatEl,timerField,fn,delayMs=CHAT_SCROL
23753
24861
  if(!_chatVirtIsUserScrolling(chatEl))done();
23754
24862
  };
23755
24863
  chatEl[scrollEndField]=onScrollEnd;
23756
- try{chatEl.addEventListener('scrollend',onScrollEnd,{passive:true})}catch(_){}
24864
+ chatEl.addEventListener('scrollend',onScrollEnd,{once:true,passive:true});
23757
24865
  }
23758
24866
  }
23759
24867
  async function refreshSnapshot(opt={}){
@@ -23903,7 +25011,7 @@ async function compactNow(){if(!S.activeId)return;if(S.staticMode&&S.frozen)resu
23903
25011
  async function clearStaleTodos(){if(!S.activeId){showError(t('select_session_first'));return}if(S.staticMode&&S.frozen)resumeAutoUpdates();await api('/api/sessions/'+S.activeId+'/todos/clear-stale',{method:'POST'});S.lastDeltaTs=Date.now();if(!S.es||S.es.readyState===2){scheduleSnapshot({forceFull:false,delayMs:160,allowWhenFrozen:true})}}
23904
25012
  async function refreshAll(forceProbe=false){if(S.staticMode&&S.frozen){S.frozen=false;applyStaticUiClass()}S.config=await api('/api/config');renderLanguageControls();applyMainI18n();S.skills=await api('/api/skills');S.tools=await api('/api/tools');S.providers=await api('/api/skills/providers');S.protocols=await api('/api/skills/protocols');renderSkillsEntryLink();await refreshSessions();const mc=await loadModelCatalog(forceProbe);if(!applyModelCatalog(mc)){renderModelControls()}if(S.activeId)await refreshSnapshot({forceFull:true,allowWhenFrozen:true})}
23905
25013
  function bindClick(id,fn){const el=E(id);if(el)el.onclick=fn}
23906
- window.addEventListener('DOMContentLoaded',async()=>{for(const id of ['chat','sessionList','todos','tasks','activity','commands','diffs','fileExplorer','catalog']){const el=E(id);if(el){if(id==='chat'){continue}if(id==='sessionList'||id==='todos'||id==='tasks'){S.follow[id]=false;const mark=(lockMs=PANEL_SCROLL_ACTIVE_MS)=>{const now=Date.now();el._panelUserScrollTs=now;el._panelUserScrollLockTs=Math.max(Number(el._panelUserScrollLockTs||0),now+Math.max(PANEL_SCROLL_ACTIVE_MS,Number(lockMs)||PANEL_SCROLL_ACTIVE_MS))};el.addEventListener('wheel',()=>mark(PANEL_SCROLL_ACTIVE_MS+260),{passive:true});el.addEventListener('touchstart',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('touchmove',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('mousedown',()=>mark(PANEL_SCROLL_ACTIVE_MS+180),{passive:true});el.addEventListener('scroll',()=>mark(PANEL_SCROLL_ACTIVE_MS),{passive:true});continue}el.addEventListener('scroll',()=>{S.follow[id]=nearBottom(el)})}}const drop=E('uploadDrop');const fileInput=E('uploadInput');if(drop&&fileInput){drop.onclick=()=>fileInput.click();fileInput.onchange=()=>uploadFiles(fileInput.files).then(()=>{fileInput.value=''}).catch(err=>showError(err.message));for(const evt of ['dragenter','dragover']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.add('dragover')})}for(const evt of ['dragleave','dragend']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.remove('dragover')})}drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('dragover');uploadFiles(e.dataTransfer?.files||[]).catch(err=>showError(err.message))})}const configInput=E('configInput');if(configInput){configInput.onchange=()=>uploadLlmConfigFile(configInput.files&&configInput.files[0]).then(()=>{configInput.value=''}).catch(err=>showError(err.message||String(err)))}bindClick('newSessionBtn',createSession);bindClick('renameSessionBtn',renameSession);bindClick('deleteSessionBtn',deleteSession);bindClick('applyModelBtn',applyModel);bindClick('importConfigBtn',importDefaultConfig);bindClick('sendBtn',sendMessage);bindClick('interruptBtn',interruptRun);bindClick('compactBtn',compactNow);bindClick('clearStaleTodosBtn',clearStaleTodos);bindClick('refreshFilesBtn',()=>refreshFileExplorer(true));bindClick('refreshBtn',()=>refreshAll(true));bindClick('previewReloadBtn',()=>renderActivePreview(true));bindClick('previewCopyBtn',()=>copyPreviewCode());const langSel=E('langSelect');if(langSel){langSel.onchange=()=>setLanguage(langSel.value).catch(err=>showError(err.message||String(err)))}const promptEl=E('prompt');if(promptEl){promptEl.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();sendMessage()}})}applyStaticUiClass();applyMainI18n();_bindPreviewCopyGuard();try{await refreshAll(false);if(!S.sessions.length)await createSession()}catch(err){showError(err.message||String(err))}_deltaStartWatchdog();scheduleSessionPoll(false);document.addEventListener('visibilitychange',()=>{const next=document.visibilityState||'visible';if(next===S.lastVisibilityState)return;S.lastVisibilityState=next;if(next==='hidden'){if(S.staticMode)freezeAutoUpdates();return}if(S.staticMode&&S.frozen)resumeAutoUpdates();scheduleSessionPoll(true);scheduleSnapshot({forceFull:false,delayMs:40,allowWhenFrozen:true})})})
25014
+ window.addEventListener('DOMContentLoaded',async()=>{for(const id of ['chat','sessionList','todos','tasks','activity','commands','diffs','fileExplorer','catalog']){const el=E(id);if(el){if(id==='chat'){continue}if(id==='sessionList'||id==='todos'||id==='tasks'){S.follow[id]=false;const mark=(lockMs=PANEL_SCROLL_ACTIVE_MS)=>{const now=Date.now();el._panelUserScrollTs=now;el._panelUserScrollLockTs=Math.max(Number(el._panelUserScrollLockTs||0),now+Math.max(PANEL_SCROLL_ACTIVE_MS,Number(lockMs)||PANEL_SCROLL_ACTIVE_MS))};el.addEventListener('wheel',()=>mark(PANEL_SCROLL_ACTIVE_MS+260),{passive:true});el.addEventListener('touchstart',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('touchmove',()=>mark(PANEL_SCROLL_ACTIVE_MS+520),{passive:true});el.addEventListener('mousedown',()=>mark(PANEL_SCROLL_ACTIVE_MS+180),{passive:true});el.addEventListener('scroll',()=>mark(PANEL_SCROLL_ACTIVE_MS),{passive:true});continue}el.addEventListener('scroll',()=>{S.follow[id]=nearBottom(el)})}}const drop=E('uploadDrop');const fileInput=E('uploadInput');if(drop&&fileInput){drop.onclick=()=>fileInput.click();fileInput.onchange=()=>uploadFiles(fileInput.files).then(()=>{fileInput.value=''}).catch(err=>showError(err.message));for(const evt of ['dragenter','dragover']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.add('dragover')})}for(const evt of ['dragleave','dragend']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.remove('dragover')})}drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('dragover');uploadFiles(e.dataTransfer?.files||[]).catch(err=>showError(err.message))})}const configInput=E('configInput');if(configInput){configInput.onchange=()=>uploadLlmConfigFile(configInput.files&&configInput.files[0]).then(()=>{configInput.value=''}).catch(err=>showError(err.message||String(err)))}bindClick('newSessionBtn',createSession);bindClick('renameSessionBtn',renameSession);bindClick('deleteSessionBtn',deleteSession);bindClick('applyModelBtn',applyModel);bindClick('importConfigBtn',importDefaultConfig);bindClick('sendBtn',sendMessage);bindClick('interruptBtn',interruptRun);bindClick('compactBtn',compactNow);bindClick('clearStaleTodosBtn',clearStaleTodos);bindClick('refreshFilesBtn',()=>refreshFileExplorer(true));bindClick('refreshBtn',()=>refreshAll(true));bindClick('previewReloadBtn',()=>renderActivePreview(true));bindClick('previewCopyBtn',()=>copyPreviewCode());const exportMenuBtn=E('exportMenuBtn');const exportMenu=E('exportMenu');if(exportMenuBtn&&exportMenu){exportMenuBtn.addEventListener('click',e=>{e.stopPropagation();exportMenu.style.display=exportMenu.style.display==='none'?'block':'none'});document.addEventListener('click',()=>{exportMenu.style.display='none'});exportMenu.addEventListener('click',e=>{e.stopPropagation()});for(const a of exportMenu.querySelectorAll('.export-item')){a.addEventListener('click',()=>{exportMenu.style.display='none'})}}const langSel=E('langSelect');if(langSel){langSel.onchange=()=>setLanguage(langSel.value).catch(err=>showError(err.message||String(err)))}const promptEl=E('prompt');if(promptEl){promptEl.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();sendMessage()}})}applyStaticUiClass();applyMainI18n();_bindPreviewCopyGuard();try{await refreshAll(false);if(!S.sessions.length)await createSession()}catch(err){showError(err.message||String(err))}_deltaStartWatchdog();scheduleSessionPoll(false);document.addEventListener('visibilitychange',()=>{const next=document.visibilityState||'visible';if(next===S.lastVisibilityState)return;S.lastVisibilityState=next;if(next==='hidden'){if(S.deltaWatchdogTimer){clearTimeout(S.deltaWatchdogTimer);S.deltaWatchdogTimer=null}if(S.sessionPollTimer){clearTimeout(S.sessionPollTimer);S.sessionPollTimer=null}if(S.staticMode)freezeAutoUpdates();return}if(S.staticMode&&S.frozen)resumeAutoUpdates();_deltaStartWatchdog();scheduleSessionPoll(true);scheduleSnapshot({forceFull:false,delayMs:40,allowWhenFrozen:true})})})
23907
25015
  """
23908
25016
 
23909
25017
  APP_TS = """type SessionSummary={id:string;title:string;running:boolean;updated_at:number;message_count:number};
@@ -23957,7 +25065,7 @@ SKILLS_INDEX_HTML = """<!doctype html>
23957
25065
  <select id="modelSelect"></select>
23958
25066
  <button id="applyModelBtn" class="subtle">Apply Model</button>
23959
25067
  <button id="refreshBtn" class="subtle">Refresh</button>
23960
- <a id="agentLink" href="#" target="_blank" rel="noreferrer">Open Agent UI</a>
25068
+ <a id="agentLink" href="#">Open Agent UI</a>
23961
25069
  </div>
23962
25070
  </header>
23963
25071
  <div class="status-cards" id="topStats"></div>
@@ -24251,9 +25359,9 @@ function pointToSegmentDistance(px,py,x1,y1,x2,y2){const dx=x2-x1,dy=y2-y1;const
24251
25359
  function findNearestEdgeIndexAt(px,py,threshold=14){const byId={};for(const n of (S.flow.nodes||[])){byId[n.id]=n}const z=getFlowZoom();let bestIdx=-1;let best=Number.POSITIVE_INFINITY;for(let i=0;i<(S.flow.edges||[]).length;i++){const e=S.flow.edges[i];const rp=resolveEdgeSidesAndPoints(e,byId);if(!rp)continue;const x1=(Number(rp.p1.x)||0)*z,y1=(Number(rp.p1.y)||0)*z,x2=(Number(rp.p2.x)||0)*z,y2=(Number(rp.p2.y)||0)*z;const dLine=pointToSegmentDistance(px,py,x1,y1,x2,y2);const dArrow=Math.hypot(px-x2,py-y2);const d=Math.min(dLine,dArrow);if(d<best){best=d;bestIdx=i}}if(best<=Math.max(6,Number(threshold)||14))return bestIdx;return -1}
24252
25360
  function beginLinkDrag(nodeId,side,ev){if(ev){ev.preventDefault();ev.stopPropagation()}const canvas=E('flowCanvas');if(!canvas)return;const rect=canvas.getBoundingClientRect();S.drag=null;S.pan=null;S.selectedNodeId=String(nodeId||'');S.linkDrag={fromId:String(nodeId||''),fromSide:normalizeSide(side)||'right',toX:(ev?ev.clientX:rect.left)-rect.left,toY:(ev?ev.clientY:rect.top)-rect.top};renderNodeEditor();renderFlow()}
24253
25361
  function renderFlow(){const canvas=E('flowCanvas');const svg=E('flowSvg');if(!canvas||!svg)return;const z=getFlowZoom();canvas.innerHTML='';svg.innerHTML='';const defs=document.createElementNS('http://www.w3.org/2000/svg','defs');const marker=document.createElementNS('http://www.w3.org/2000/svg','marker');marker.setAttribute('id','arrow');marker.setAttribute('viewBox','0 0 10 10');marker.setAttribute('refX','10');marker.setAttribute('refY','5');marker.setAttribute('markerWidth','7');marker.setAttribute('markerHeight','7');marker.setAttribute('orient','auto-start-reverse');const path=document.createElementNS('http://www.w3.org/2000/svg','path');path.setAttribute('d','M 0 0 L 10 5 L 0 10 z');path.setAttribute('fill','#7f95b8');marker.appendChild(path);defs.appendChild(marker);svg.appendChild(defs);const byId={};let baseW=1300,baseH=900;for(const n of S.flow.nodes){byId[n.id]=n;baseW=Math.max(baseW,(Number(n.x)||0)+230);baseH=Math.max(baseH,(Number(n.y)||0)+190)}const viewW=Math.max(320,Math.floor(baseW*z));const viewH=Math.max(220,Math.floor(baseH*z));canvas.style.minWidth=`${viewW}px`;canvas.style.minHeight=`${viewH}px`;svg.style.minWidth=`${viewW}px`;svg.style.minHeight=`${viewH}px`;svg.setAttribute('width',String(viewW));svg.setAttribute('height',String(viewH));const returnMap={};for(const e of (S.flow.edges||[])){const a=byId[e.from],b=byId[e.to];if(!a||!b)continue;let fs=edgeFromSide(e);let ts=edgeToSide(e);if(!fs||!ts){const autoSides=inferEdgeSides(a,b);if(!fs)fs=autoSides.from;if(!ts)ts=autoSides.to}const pa=nodePortPoint(a,fs);const pb=nodePortPoint(b,ts);const ca={x:pa.x*z,y:pa.y*z};const cb={x:pb.x*z,y:pb.y*z};const bi=edgeBidirectional(e);const line=document.createElementNS('http://www.w3.org/2000/svg','line');line.setAttribute('x1',String(ca.x));line.setAttribute('y1',String(ca.y));line.setAttribute('x2',String(cb.x));line.setAttribute('y2',String(cb.y));line.setAttribute('stroke','#7f95b8');line.setAttribute('stroke-width','1.8');if(bi)line.setAttribute('marker-start','url(#arrow)');line.setAttribute('marker-end','url(#arrow)');svg.appendChild(line);const lbl=document.createElementNS('http://www.w3.org/2000/svg','text');lbl.setAttribute('x',String((ca.x+cb.x)/2));lbl.setAttribute('y',String((ca.y+cb.y)/2-7));lbl.setAttribute('text-anchor','middle');lbl.setAttribute('class','flow-edge-label');lbl.textContent=edgePathLabel(e);svg.appendChild(lbl);if(bi){const n=edgeReturnN(e);const f=String(e.from||'').trim();const t=String(e.to||'').trim();if(f)returnMap[f]=Math.max(Number(returnMap[f]||0),n);if(t)returnMap[t]=Math.max(Number(returnMap[t]||0),n)}}if(S.linkDrag&&byId[S.linkDrag.fromId]){const src=nodePortPoint(byId[S.linkDrag.fromId],S.linkDrag.fromSide);const line=document.createElementNS('http://www.w3.org/2000/svg','line');line.setAttribute('x1',String(src.x*z));line.setAttribute('y1',String(src.y*z));line.setAttribute('x2',String(Number(S.linkDrag.toX)||src.x*z));line.setAttribute('y2',String(Number(S.linkDrag.toY)||src.y*z));line.setAttribute('class','flow-link-preview');svg.appendChild(line)}for(const n of S.flow.nodes){const d=document.createElement('div');d.className='flow-node'+(n.id===S.selectedNodeId?' active':'');d.style.left=((Number(n.x)||0)*z)+'px';d.style.top=((Number(n.y)||0)*z)+'px';d.style.transform=`scale(${z})`;d.style.transformOrigin='top left';d.innerHTML=`<div class=\"k\">${esc(n.type||'node')}</div><div class=\"t\">${esc(n.title||n.id)}</div><div class=\"c\">${esc(n.content||'')}</div>`;d.onmousedown=(ev)=>{ev.preventDefault();S.linkDrag=null;S.selectedNodeId=n.id;const zz=getFlowZoom();S.drag={id:n.id,dx:ev.clientX-((Number(n.x)||0)*zz),dy:ev.clientY-((Number(n.y)||0)*zz)};renderFlow();renderNodeEditor()};d.onclick=()=>{S.selectedNodeId=n.id;renderFlow();renderNodeEditor()};for(const side of FLOW_SIDES){const p=document.createElement('div');p.className='flow-port side-'+side+((S.linkDrag&&S.linkDrag.fromId===n.id&&S.linkDrag.fromSide===side)?' active':'');p.setAttribute('data-node-id',String(n.id));p.setAttribute('data-side',side);p.onmousedown=(ev)=>beginLinkDrag(n.id,side,ev);d.appendChild(p)}const backN=Number(returnMap[String(n.id)]||0);if(backN>0){const badge=document.createElement('div');badge.className='flow-return-badge';badge.textContent='n='+String(backN);d.appendChild(badge)}canvas.appendChild(d)}renderEdgeSelects();scheduleFlowWrapAdjust();updateFlowZoomUI()}
24254
- let flowWrapRaf=0;
25362
+ let flowWrapRaf=0;let flowWrapDebounce=0;
24255
25363
  function adjustFlowWrapHeight(){const wrap=E('flowWrap');const panel=document.querySelector('.skills-panel-center');const stage=wrap&&wrap.closest?wrap.closest('.flow-stage'):null;if(!wrap||!panel||!stage)return;const mobile=(window.matchMedia&&window.matchMedia('(max-width:1180px)').matches);if(mobile){stage.style.height='320px';stage.style.minHeight='320px';wrap.style.height='100%';return}const kids=Array.from(panel.children||[]);let used=0;for(const el of kids){if(el===stage||el===wrap)continue;const st=getComputedStyle(el);used+=el.offsetHeight+(parseFloat(st.marginTop)||0)+(parseFloat(st.marginBottom)||0)}const ps=getComputedStyle(panel);const gap=(parseFloat(ps.rowGap||ps.gap)||0);if(kids.length>1)used+=gap*(kids.length-1);const vh=Math.max(760,window.innerHeight||900);const maxPx=Math.floor(vh*0.58);let available=Math.floor(panel.clientHeight-used);if(!Number.isFinite(available))available=360;available=Math.max(280,Math.min(maxPx,available));stage.style.height=`${available}px`;stage.style.minHeight='260px';wrap.style.height='100%'}
24256
- function scheduleFlowWrapAdjust(){if(flowWrapRaf)cancelAnimationFrame(flowWrapRaf);flowWrapRaf=requestAnimationFrame(()=>{flowWrapRaf=0;adjustFlowWrapHeight()})}
25364
+ function scheduleFlowWrapAdjust(){if(flowWrapRaf)cancelAnimationFrame(flowWrapRaf);if(flowWrapDebounce)clearTimeout(flowWrapDebounce);flowWrapDebounce=setTimeout(()=>{flowWrapDebounce=0;flowWrapRaf=requestAnimationFrame(()=>{flowWrapRaf=0;adjustFlowWrapHeight()})},60)}
24257
25365
  function switchFlowPanel(mode){const m=(String(mode||'').toLowerCase()==='link')?'link':'node';const nodeBtn=E('flowTabNodeBtn');const linkBtn=E('flowTabLinkBtn');const nodePanel=E('flowPanelNode');const linkPanel=E('flowPanelLink');if(nodeBtn)nodeBtn.classList.toggle('active',m==='node');if(linkBtn)linkBtn.classList.toggle('active',m==='link');if(nodePanel)nodePanel.classList.toggle('active',m==='node');if(linkPanel)linkPanel.classList.toggle('active',m==='link');scheduleFlowWrapAdjust()}
24258
25366
  function renderEdgeSelects(){const ids=S.flow.nodes.map(n=>n.id);const render=id=>{const el=E(id);if(!el)return;const cur=el.value;el.innerHTML=ids.map(x=>`<option value=\"${esc(x)}\">${esc(x)}</option>`).join('');if(cur&&ids.includes(cur))el.value=cur};render('edgeFrom');render('edgeTo')}
24259
25367
  function renderNodeEditor(){const n=S.flow.nodes.find(x=>x.id===S.selectedNodeId)||null;if(!n)return;E('nodeTitle').value=n.title||'';E('nodeType').value=n.type||'process';E('nodeContent').value=n.content||''}
@@ -24320,6 +25428,7 @@ class AppContext:
24320
25428
  arbiter_max_tokens: int = ARBITER_DEFAULT_MAX_TOKENS,
24321
25429
  arbiter_temperature: float = ARBITER_DEFAULT_TEMPERATURE,
24322
25430
  execution_mode: str = EXECUTION_MODE_SYNC,
25431
+ max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
24323
25432
  max_user: int = 0,
24324
25433
  max_user_sessions: int = 0,
24325
25434
  ):
@@ -24364,6 +25473,7 @@ class AppContext:
24364
25473
  self.arbiter_max_tokens = max(24, min(256, int(arbiter_max_tokens or ARBITER_DEFAULT_MAX_TOKENS)))
24365
25474
  self.arbiter_temperature = max(0.0, min(1.0, float(arbiter_temperature if arbiter_temperature is not None else ARBITER_DEFAULT_TEMPERATURE)))
24366
25475
  self.execution_mode = normalize_execution_mode(execution_mode, default=EXECUTION_MODE_SYNC)
25476
+ self.max_output_tokens = max(256, int(max_output_tokens or AGENT_MAX_OUTPUT_TOKENS))
24367
25477
  self.skills_root = skills_root
24368
25478
  ensure_runtime_skills(self.skills_root)
24369
25479
  self.skills_store = SkillStore(self.skills_root)
@@ -24832,6 +25942,7 @@ class AppContext:
24832
25942
  self.arbiter_max_tokens,
24833
25943
  self.arbiter_temperature,
24834
25944
  self.execution_mode,
25945
+ self.max_output_tokens,
24835
25946
  run_finished_callback=self._on_session_run_finished,
24836
25947
  )
24837
25948
  self._session_mgrs[user_id] = mgr
@@ -24913,6 +26024,9 @@ class AppContext:
24913
26024
  active = dict(self.global_profiles.get(self.global_active_profile_id, {}))
24914
26025
  self._sync_global_ollama_defaults(active)
24915
26026
  self.thinking = False
26027
+ cfg_max_output_tokens = cfg.get("max_output_tokens")
26028
+ if cfg_max_output_tokens is not None:
26029
+ self.max_output_tokens = max(256, int(cfg_max_output_tokens))
24916
26030
 
24917
26031
  def normalized_profiles() -> tuple[dict[str, dict], str]:
24918
26032
  rows: dict[str, dict] = {}
@@ -26007,6 +27121,33 @@ class Handler(BaseHTTPRequestHandler):
26007
27121
  if not sess:
26008
27122
  return self._send_json({"error": "session not found"}, status=404)
26009
27123
  return self._send_bytes(sess.export_bundle(), "application/zip", f"{sess.id}_session_export.zip")
27124
+ m = re.match(r"^/api/sessions/([^/]+)/export\.md$", path)
27125
+ if m:
27126
+ sess = mgr.get(m.group(1))
27127
+ if not sess:
27128
+ return self._send_json({"error": "session not found"}, status=404)
27129
+ md = sess.export_conversation_md()
27130
+ return self._send_bytes(md.encode("utf-8"), "text/markdown; charset=utf-8", f"{sess.id}_conversation.md")
27131
+ m = re.match(r"^/api/sessions/([^/]+)/export\.pdf$", path)
27132
+ if m:
27133
+ sess = mgr.get(m.group(1))
27134
+ if not sess:
27135
+ return self._send_json({"error": "session not found"}, status=404)
27136
+ try:
27137
+ pdf = sess.export_conversation_pdf()
27138
+ return self._send_bytes(pdf, "application/pdf", f"{sess.id}_conversation.pdf")
27139
+ except Exception as exc:
27140
+ return self._send_json({"error": str(exc)}, status=500)
27141
+ m = re.match(r"^/api/sessions/([^/]+)/export\.png$", path)
27142
+ if m:
27143
+ sess = mgr.get(m.group(1))
27144
+ if not sess:
27145
+ return self._send_json({"error": "session not found"}, status=404)
27146
+ try:
27147
+ img = sess.export_conversation_image()
27148
+ return self._send_bytes(img, "image/png", f"{sess.id}_conversation.png")
27149
+ except Exception as exc:
27150
+ return self._send_json({"error": str(exc)}, status=500)
26010
27151
  return self._send_json({"error": "not found"}, status=404)
26011
27152
 
26012
27153
  def do_POST(self):
@@ -26640,6 +27781,12 @@ def main():
26640
27781
  default="",
26641
27782
  help="Agent execution mode (single|sequential|sync). Empty means read from startup config, then fallback to sync.",
26642
27783
  )
27784
+ parser.add_argument(
27785
+ "--max-output-tokens",
27786
+ default=AGENT_MAX_OUTPUT_TOKENS,
27787
+ type=int,
27788
+ help=f"Max output tokens per agent turn (default: {AGENT_MAX_OUTPUT_TOKENS}). Also configurable via config file key 'max_output_tokens'.",
27789
+ )
26643
27790
  parser.add_argument(
26644
27791
  "--max_user",
26645
27792
  default=None,
@@ -26858,6 +28005,7 @@ def main():
26858
28005
  or ""
26859
28006
  ).strip()
26860
28007
  resolved_execution_mode = normalize_execution_mode(raw_execution_mode, default=EXECUTION_MODE_SYNC)
28008
+ resolved_max_output_tokens = max(256, int(getattr(args, "max_output_tokens", AGENT_MAX_OUTPUT_TOKENS) or AGENT_MAX_OUTPUT_TOKENS))
26861
28009
  if raw_execution_mode:
26862
28010
  normalized_raw = str(raw_execution_mode).strip().lower()
26863
28011
  if normalized_raw != resolved_execution_mode:
@@ -26904,6 +28052,7 @@ def main():
26904
28052
  resolved_arbiter_max_tokens,
26905
28053
  resolved_arbiter_temperature,
26906
28054
  resolved_execution_mode,
28055
+ resolved_max_output_tokens,
26907
28056
  resolved_max_user,
26908
28057
  resolved_max_user_sessions,
26909
28058
  )