clouds-coder 2026.3.31__tar.gz → 2026.4.2__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.
@@ -14,6 +14,7 @@ import hmac
14
14
  import io
15
15
  import importlib.util
16
16
  import json
17
+ import locale
17
18
  import math
18
19
  import multiprocessing
19
20
  import mimetypes
@@ -36,6 +37,7 @@ import uuid
36
37
  import zipfile
37
38
  import zlib
38
39
  import xml.etree.ElementTree as ET
40
+ from datetime import datetime, timedelta
39
41
  from http import HTTPStatus
40
42
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
41
43
  from pathlib import Path, PurePosixPath
@@ -493,7 +495,10 @@ DEVELOPER_EDIT_STALL_THRESHOLD = 3 # consecutive edit_file failures on same fil
493
495
  PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS = 6144
494
496
  PLAN_MODE_MAX_OPTIONS = 3
495
497
  PLAN_FILE_RELATIVE_PATH = ".clouds_coder/plan.md"
496
- PLAN_BUBBLE_MAX_CHARS = 3800 # margin under ASSISTANT_MESSAGE_EVENT_MAX_CHARS (4000)
498
+ PLAN_BUBBLE_MAX_CHARS = 12_000
499
+ PLAN_NOTICE_BODY_MAX_CHARS = 10_000
500
+ PLAN_MESSAGE_EVENT_MAX_CHARS = 12_000
501
+ PLAN_STEP_FULL_CONTENT_MAX_CHARS = 24_000
497
502
  PLAN_MODE_RESEARCH_TOOL_ALLOWLIST = {
498
503
  "bash", "read_file", "context_recall", "task_get", "task_list",
499
504
  "check_background", "read_from_blackboard", "write_to_blackboard",
@@ -1963,6 +1968,58 @@ def extract_js_lib_download_setting(raw: object) -> bool | None:
1963
1968
  return None
1964
1969
 
1965
1970
 
1971
+ def extract_daily_session_limit_setting(raw: object) -> int | None:
1972
+ """Read per-IP daily session creation limit from config dict.
1973
+
1974
+ Accepted keys:
1975
+ - daily_session_limit
1976
+ - daily_sessions_per_ip
1977
+ - max_daily_sessions_per_ip
1978
+ - session_daily_limit
1979
+ Sections searched: top-level, then 'startup' / 'limits' / 'web_ui' / 'ui'.
1980
+ Returns a non-negative integer, or None if no setting is present.
1981
+ """
1982
+ if not isinstance(raw, dict):
1983
+ return None
1984
+
1985
+ def _parse_non_negative_int(value: object) -> int | None:
1986
+ if value is None:
1987
+ return None
1988
+ if isinstance(value, bool):
1989
+ return int(value)
1990
+ try:
1991
+ text = str(value).strip()
1992
+ if not text:
1993
+ return None
1994
+ return max(0, int(float(text)))
1995
+ except Exception:
1996
+ return None
1997
+
1998
+ keys = (
1999
+ "daily_session_limit",
2000
+ "daily_sessions_per_ip",
2001
+ "max_daily_sessions_per_ip",
2002
+ "session_daily_limit",
2003
+ )
2004
+ for key in keys:
2005
+ if key in raw:
2006
+ return _parse_non_negative_int(raw.get(key))
2007
+ for section_key in ("startup", "limits", "web_ui", "ui"):
2008
+ section = raw.get(section_key)
2009
+ if not isinstance(section, dict):
2010
+ continue
2011
+ for key in keys:
2012
+ if key in section:
2013
+ return _parse_non_negative_int(section.get(key))
2014
+ return None
2015
+
2016
+
2017
+ class SessionCreationLimitExceeded(RuntimeError):
2018
+ def __init__(self, status: dict):
2019
+ self.status = dict(status or {})
2020
+ super().__init__(str(self.status.get("message", "daily session limit reached")))
2021
+
2022
+
1966
2023
  def default_multimodal_capabilities() -> dict[str, bool]:
1967
2024
  return {
1968
2025
  "input_image": False,
@@ -6410,6 +6467,520 @@ Return:
6410
6467
  ),
6411
6468
  )
6412
6469
 
6470
+ def ensure_generated_systematic_debugging_skill(skills_root: Path):
6471
+ generated_root = skills_root / "generated"
6472
+ root = generated_root / "systematic-debugging"
6473
+ skill_md = """---
6474
+ name: systematic-debugging
6475
+ description: Adaptive root-cause analysis engine that scales debugging depth to error severity — from quick-fix pattern matching to deep multi-layer causal tracing across Python, JS, Go, Rust, Java, and C/C++.
6476
+ ---
6477
+
6478
+ # Systematic Debugging
6479
+
6480
+ ## Trigger
6481
+ Task involves fixing bugs, resolving errors, diagnosing failures, analyzing stack traces, or investigating unexpected behavior.
6482
+
6483
+ ## Adaptive Depth Selection (decide BEFORE acting)
6484
+
6485
+ Assess the error and pick the matching depth. This is the single most important decision — wrong depth wastes time or misses the cause.
6486
+
6487
+ | Signal | Depth | Budget | Strategy |
6488
+ |--------|-------|--------|----------|
6489
+ | Typo, missing import, syntax error | **Shallow** | 1-2 tool calls | Pattern-match fix directly from error message |
6490
+ | Single clear exception with traceback | **Standard** | 3-6 tool calls | Trace call chain, read crash site ±20 lines, fix + verify |
6491
+ | Intermittent / multi-component / no clear trace | **Deep** | 8-15 tool calls | Hypothesize → isolate → instrument → validate causal chain |
6492
+ | Reproduces only under specific state / concurrency | **Forensic** | 15-25 tool calls | State reconstruction, bisect, invariant analysis |
6493
+
6494
+ **Rule**: Start at the depth the signals suggest. Escalate only when the current depth's budget is exhausted without resolution.
6495
+
6496
+ ## Core Method: Causal Chain Tracing
6497
+
6498
+ Every bug has a causal chain: **trigger → propagation → manifestation**. Most developers only see the manifestation. Your job is to trace backward to the trigger.
6499
+
6500
+ ### Step 1: Read the Error as a Structured Signal
6501
+ - The error message is DATA, not just text. Extract: error type, location (file:line), variable state, and the operation that failed.
6502
+ - Stack traces read BOTTOM-UP: the last frame is where it crashed, but the cause is often 2-5 frames higher.
6503
+ - Compiler errors: the FIRST error is usually the real one; subsequent errors are cascading noise.
6504
+
6505
+ ### Step 2: Form a Hypothesis Before Reading Code
6506
+ - Based on the error signal, form 1-3 hypotheses about the TRIGGER (not the manifestation).
6507
+ - Rank hypotheses by probability. Investigate the most likely first.
6508
+ - **Anti-pattern**: Reading random files hoping to stumble on the cause. Always have a hypothesis.
6509
+
6510
+ ### Step 3: Targeted Investigation
6511
+ - Read ONLY the code that your hypothesis predicts is involved.
6512
+ - Use `read_file` with offset/limit — read the crash site ±20 lines, not the whole file.
6513
+ - If hypothesis is wrong, update it based on what you learned. Don't restart from scratch.
6514
+
6515
+ ### Step 4: Fix at the Trigger, Not the Symptom
6516
+ - **Wrong**: Add a try/catch around the crash site.
6517
+ - **Right**: Fix why the invalid state reached the crash site in the first place.
6518
+ - One fix per root cause. Never bundle unrelated changes.
6519
+
6520
+ ### Step 5: Verify the Causal Chain is Broken
6521
+ - Re-run the exact failing command. Must succeed.
6522
+ - Run the full test suite. No regressions.
6523
+ - If the bug was at a boundary, add a test for that boundary.
6524
+
6525
+ ## Language-Specific Deep Patterns
6526
+
6527
+ ### Python
6528
+ - `Traceback` → Read bottom-up. The real cause is where the wrong VALUE was created, not where the wrong TYPE was detected.
6529
+ - `AttributeError: 'NoneType'` → Trace backward: who returned None? Usually a missing DB record, failed API call, or uninitialized optional.
6530
+ - `ImportError` / `ModuleNotFoundError` → Check: venv active? Package installed in correct env? Relative vs absolute import? sys.path manipulation?
6531
+ - `RecursionError` → Find the cycle: which function calls itself without converging? Often mutual recursion via A→B→A.
6532
+
6533
+ ### JavaScript / TypeScript
6534
+ - `TypeError: Cannot read properties of undefined` → The object is fine, the CHAIN has a null link. Trace: `a.b.c.d` — which of a/b/c is undefined?
6535
+ - `Unhandled Promise rejection` → An async function threw but nobody awaited or caught. Find the un-awaited call.
6536
+ - `ReferenceError` in production but not dev → Hoisting, tree-shaking, or module resolution difference. Check build config.
6537
+ - TS compile errors → Read the EXPECTED type vs ACTUAL type. The fix is usually at the producer, not the consumer.
6538
+
6539
+ ### Go
6540
+ - `panic: runtime error: invalid memory address` → nil pointer dereference. Find which pointer isn't checked. Often from interface method call on nil receiver.
6541
+ - `data race detected` → Two goroutines accessing shared state. The fix is either mutex, channel, or restructure to avoid sharing.
6542
+ - `context deadline exceeded` → Upstream is slow. Check: is the timeout reasonable? Is the upstream healthy? Is there a retry storm?
6543
+
6544
+ ### Rust
6545
+ - `E0382 use of moved value` → Ownership transferred. Fix: clone (if cheap), borrow (&/&mut), or restructure to avoid the double-use.
6546
+ - `E0277 trait bound not satisfied` → The type doesn't implement what the function requires. Check: does the type need a derive? Is a generic constraint missing?
6547
+ - `lifetime errors` → Draw the lifetime diagram: which reference outlives its source? Usually need to restructure borrows or use owned types.
6548
+
6549
+ ### Java
6550
+ - `NullPointerException` → The modern fix is Optional + .orElseThrow with a descriptive message, not null checks everywhere.
6551
+ - `ClassCastException` → Type erasure hiding a wrong type in a collection. Check the generic types at insertion point.
6552
+ - `ConcurrentModificationException` → Iterating and modifying the same collection. Use Iterator.remove(), streams, or concurrent collections.
6553
+
6554
+ ### C / C++
6555
+ - Segfault → Use ASan (`-fsanitize=address`). The FIRST ASan error is the real one. Common: use-after-free, buffer overflow, null deref.
6556
+ - Undefined behavior → The compiler assumes UB doesn't happen and optimizes accordingly. The "bug" may be correct code that relies on UB. Use UBSan.
6557
+ - Memory leak → Valgrind or ASan. Every allocation must have exactly one deallocation on every code path including error paths.
6558
+
6559
+ ## Advanced Strategies (Deep/Forensic Depth Only)
6560
+
6561
+ ### Binary Search Debugging
6562
+ When you can't pinpoint the cause: comment out half the suspicious code, check if error persists. Halve the remaining range. Converges in O(log n) steps.
6563
+
6564
+ ### Differential Diagnosis
6565
+ When multiple hypotheses remain: design a single test that distinguishes between them. Run it. Eliminate hypotheses. Repeat.
6566
+
6567
+ ### State Reconstruction
6568
+ For state-dependent bugs: trace the state of the key variable backward through time. At each mutation point, ask: was this mutation correct given its inputs?
6569
+
6570
+ ### Invariant Analysis
6571
+ Identify what must ALWAYS be true (e.g., "list is sorted", "pointer is non-null", "balance >= 0"). Find where the invariant is violated. That's the bug.
6572
+
6573
+ ## Output Contract
6574
+ 1. Error classification (type + depth selected + reasoning).
6575
+ 2. Causal chain: trigger → propagation → manifestation.
6576
+ 3. Fix applied: exact file, line, change, and WHY this fixes the trigger.
6577
+ 4. Verification: command run, output confirming fix.
6578
+ 5. If blocked: exact missing information and what to try next.
6579
+ """
6580
+ error_ref = """# Debugging Decision Heuristics
6581
+
6582
+ ## When to Escalate Depth
6583
+ - Shallow fix didn't work after 2 attempts → escalate to Standard
6584
+ - Standard investigation found no cause in 5 tool calls → escalate to Deep
6585
+ - Error is intermittent or state-dependent → start at Deep
6586
+ - Multiple components involved → start at Deep
6587
+
6588
+ ## Red Flags (your fix is wrong)
6589
+ - Error moves to a different line → you fixed a symptom
6590
+ - A new error appears → your fix introduced a regression
6591
+ - Error only disappears in some conditions → you're masking, not fixing
6592
+ - You added a try/catch → almost certainly wrong unless it's a boundary
6593
+
6594
+ ## Efficiency Rules
6595
+ - Never read a file you already read in this session
6596
+ - Never read > 50 lines when 20 would suffice
6597
+ - If you can't form a hypothesis after reading the error, re-read the error more carefully before reading any code
6598
+ - The fastest debug session is the one where you fix it from the error message alone
6599
+
6600
+ ## Error Classification Deep Reference
6601
+
6602
+ ### 1. SYNTAX — Code rejected before execution
6603
+
6604
+ **Signals**: `SyntaxError`, `unexpected token`, `parse error`, compiler exits with line/column reference, no runtime stack trace.
6605
+
6606
+ **Sub-types and causal patterns**:
6607
+ - **Lexical**: Invalid character, unterminated string/comment, encoding mismatch (BOM, non-UTF8). Fix: look at the exact byte the compiler points to.
6608
+ - **Grammatical**: Missing bracket/brace/paren, mismatched delimiters, dangling comma. The error line is often AFTER the real mistake — scan backward from the error for the unclosed construct.
6609
+ - **Semantic-syntax**: Valid tokens in invalid order (`return` outside function, `await` outside async, double `mut`). The compiler knows the rule — read its suggestion first.
6610
+ - **Cross-file**: Import/include of a file that itself has syntax errors. The reported file is the victim, not the cause — check the imported file.
6611
+
6612
+ **Common misdiagnosis**: "Syntax error on line 50" when the real problem is an unclosed bracket on line 30. Always scan backward from the error point for unmatched delimiters.
6613
+
6614
+ **Language-specific traps**:
6615
+ - Python: Mixing tabs and spaces (invisible). `IndentationError` is syntax, not runtime.
6616
+ - JS: Missing semicolon after object in `export default { ... }` before another statement.
6617
+ - Go: Unused import/variable is a compile error, not a warning. Fix or remove, don't comment out.
6618
+ - Rust: Lifetime annotations are syntax-level. `expected lifetime parameter` = missing `<'a>`.
6619
+ - C/C++: Missing semicolon after class/struct definition causes cascading errors on the NEXT file.
6620
+
6621
+ ---
6622
+
6623
+ ### 2. RUNTIME — Crash during execution
6624
+
6625
+ **Signals**: Exception with stack trace, panic, segfault, core dump, non-zero exit code with error message.
6626
+
6627
+ **Sub-types and causal patterns**:
6628
+ - **Null/nil dereference**: Variable is None/null/nil when accessed. Root cause is NOT at the access point — trace backward to find WHO produced the null. Common sources: failed DB lookup, missing config, uninitialized optional, API returning null on error.
6629
+ - **Type mismatch**: Value has wrong type at usage point. Root cause: producer created wrong type, or a collection/map holds mixed types. In dynamic languages, trace the value to its origin and check what type the producer guarantees.
6630
+ - **Index/bounds**: Array/slice index out of range. Check: is the array empty when it shouldn't be? Is the index computed from user input without bounds check? Is there an off-by-one in a loop?
6631
+ - **Assertion/contract**: `assert`, `require`, `precondition` failed. This is the most informative runtime error — the developer who wrote it TOLD you what went wrong. Read the assertion message.
6632
+ - **Resource exhaustion**: OOM, too many open files, stack overflow. Not a logic bug — check for leaks (unclosed handles), unbounded recursion, or legitimate scale problems.
6633
+ - **Encoding/serialization**: JSON parse error, UTF-8 decode failure, protobuf mismatch. The data is corrupt or the schema changed. Check: who produced this data? Has the format been updated without updating the consumer?
6634
+
6635
+ **Causal chain discipline**: The stack trace tells you WHERE it crashed, not WHY. Read bottom-up: the crash frame shows the operation, the frames above show who called it with bad arguments. The root cause is usually 2-5 frames up where the bad value was CREATED.
6636
+
6637
+ **Common misdiagnosis**: Adding a null check at the crash point. This turns a crash into silent wrong behavior — the null still exists, you just stopped reporting it. Fix the null at its SOURCE.
6638
+
6639
+ ---
6640
+
6641
+ ### 3. LOGIC — Wrong output, no crash
6642
+
6643
+ **Signals**: Tests fail with wrong value, infinite loop, incorrect behavior reported by user, function returns unexpected result, state machine reaches impossible state.
6644
+
6645
+ **This is the hardest category** because the code runs "successfully" — there's no error message to guide you.
6646
+
6647
+ **Sub-types and causal patterns**:
6648
+ - **Off-by-one**: Loop iterates one too many/few times, array sliced at wrong boundary, fence-post error in pagination. Check: is it `<` or `<=`? Is the index 0-based or 1-based? Is the range inclusive or exclusive?
6649
+ - **Wrong branch**: Conditional goes the wrong way. Check: is the boolean logic correct? Are `&&`/`||` precedence correct? Is the comparison `==` when it should be `===` (JS)? Is a variable being shadowed?
6650
+ - **Stale state**: Cache/memo returns outdated value, event handler references captured variable from closure, state not reset between iterations. Check: when was this value last updated? Is there a cache invalidation path?
6651
+ - **Algorithm error**: Sorting wrong field, aggregating wrong column, applying wrong formula. The code is clean but implements the wrong spec. Re-read the requirement, then re-read the code — find where they diverge.
6652
+ - **Ordering dependency**: Operations executed in wrong order (write before read, commit before validate, render before data load). Trace the execution order and compare to the required order.
6653
+ - **Silent truncation**: Integer overflow wrapping silently, string truncated, floating-point precision loss. The code looks correct but produces wrong results at scale.
6654
+
6655
+ **Diagnostic strategy**: Add assertions at intermediate points. State what the value SHOULD be. The first assertion that fails localizes the bug. This is faster than reading code and trying to reason about what it does.
6656
+
6657
+ **Common misdiagnosis**: Patching the output instead of fixing the logic. If `calculate_total()` returns 99 instead of 100, don't add +1 at the call site. Find WHY it's computing 99.
6658
+
6659
+ ---
6660
+
6661
+ ### 4. INTEGRATION — Failure at component boundaries
6662
+
6663
+ **Signals**: HTTP 4xx/5xx, connection refused, timeout at API boundary, serialization mismatch between services, auth failure, "unexpected response format", database constraint violation.
6664
+
6665
+ **Sub-types and causal patterns**:
6666
+ - **Contract mismatch**: Caller sends field `user_id`, receiver expects `userId`. Or type mismatch: string vs integer. Always verify the contract on BOTH sides independently — don't trust either side's assumption.
6667
+ - **Auth/permission**: Token expired, wrong scope, missing header, CORS blocking. Check: is the auth mechanism correct? Is the token fresh? Does the user/service have the right permissions?
6668
+ - **Version skew**: Client uses v1 API, server upgraded to v2. Or dependency updated with breaking change. Check: when was the last deploy? Did any dependency versions change?
6669
+ - **Network/infra**: DNS resolution, firewall rules, TLS certificate, connection pool exhaustion. Check: can you reach the endpoint with `curl`? Is the service actually running?
6670
+ - **Data contract**: Database schema changed but code not updated. Migration ran partially. Foreign key constraint violated because related record doesn't exist yet (ordering problem).
6671
+ - **Protocol mismatch**: REST vs gRPC, JSON vs form-encoded, websocket upgrade failed. Check Content-Type headers and request format.
6672
+
6673
+ **Diagnostic strategy**: Isolate which SIDE is wrong. Send a known-good request to the receiver. Send the caller's actual request to a mock. The side that fails with known-good input is the one with the bug.
6674
+
6675
+ **Common misdiagnosis**: Blaming the other service. Always verify YOUR side first. Send the request manually and examine the raw response before assuming the other side is broken.
6676
+
6677
+ ---
6678
+
6679
+ ### 5. PERFORMANCE — Slow or resource-hungry
6680
+
6681
+ **Signals**: Timeout, high latency, OOM, CPU spike, slow query log, user reports "it's slow", load test failures.
6682
+
6683
+ **Sub-types and causal patterns**:
6684
+ - **Algorithmic**: O(n²) or worse where O(n) or O(n log n) is possible. Common: nested loops over large collections, repeated linear search, string concatenation in loop. Profile to find the hotspot, then analyze the algorithm.
6685
+ - **N+1 queries**: One query to get a list, then one query per item to get details. The fix is a JOIN or batch query, not caching.
6686
+ - **Missing index**: Database full-table scan on a frequently queried column. Check `EXPLAIN` output. Add index on the filtered/sorted columns.
6687
+ - **Memory leak**: Allocation without deallocation. Growing collections that are never pruned. Event listeners registered but never removed. Closures capturing large objects.
6688
+ - **Unnecessary work**: Re-computing what could be cached, re-fetching what's already in memory, serializing/deserializing on every call when the object hasn't changed.
6689
+ - **Contention**: Lock held too long, all threads waiting for same resource, connection pool too small, single-threaded bottleneck in parallel system.
6690
+
6691
+ **Diagnostic strategy**: ALWAYS profile before optimizing. The bottleneck is almost never where you think it is. Measure first, then fix the ONE thing that dominates the profile. Re-measure after fixing.
6692
+
6693
+ **Common misdiagnosis**: Optimizing code that runs once instead of the code that runs 10,000 times. Or adding caching without understanding why the original is slow (caching a bug makes the bug faster, not fixed).
6694
+
6695
+ ---
6696
+
6697
+ ### 6. CONCURRENCY — Non-deterministic failures
6698
+
6699
+ **Signals**: Test passes sometimes and fails sometimes, data corruption under load, deadlock (program hangs), race condition detected by sanitizer, "impossible" state in logs.
6700
+
6701
+ **This is the second-hardest category** because bugs may not reproduce reliably.
6702
+
6703
+ **Sub-types and causal patterns**:
6704
+ - **Data race**: Two threads/goroutines/coroutines read-write the same memory without synchronization. The fix is either: mutex/lock, atomic operation, channel/message-passing, or restructure to eliminate shared state.
6705
+ - **Deadlock**: Thread A holds lock X, waits for lock Y. Thread B holds lock Y, waits for lock X. Fix: consistent lock ordering (always acquire X before Y), or use try-lock with timeout, or restructure to use fewer locks.
6706
+ - **Lost update**: Read-modify-write without atomicity. Two threads read value=5, both add 1, both write 6 (should be 7). Fix: use atomic operations or wrap in transaction.
6707
+ - **Stale read**: Reading a value that another thread has already invalidated. Common with double-checked locking done wrong, or reading non-volatile fields in Java.
6708
+ - **Ordering violation**: Assuming operation A completes before B starts, but no synchronization enforces this. Fix: explicit synchronization (barrier, semaphore, channel, `await`).
6709
+ - **Resource starvation**: One thread/process monopolizes a resource (CPU, lock, connection), others timeout. Fix: fair scheduling, lock timeout, connection pool limits.
6710
+
6711
+ **Diagnostic strategy**: First, make it reproducible. Run with thread sanitizer (`-fsanitize=thread`, Go race detector `go test -race`). If it can't be reproduced, analyze the code for shared mutable state — every read and write to shared state must be synchronized. No exceptions.
6712
+
6713
+ **Common misdiagnosis**: Adding `sleep()` to "fix" a race condition. Sleep changes timing, doesn't fix the race. The bug will return under different load. Also: adding more locks without understanding the existing lock ordering — this often introduces deadlocks.
6714
+
6715
+ ---
6716
+
6717
+ ### Classification Decision Tree
6718
+
6719
+ When an error doesn't fit one category cleanly:
6720
+
6721
+ 1. Does the code fail to compile/parse? → **Syntax**
6722
+ 2. Does it crash with an exception/trace? → **Runtime**
6723
+ 3. Does it produce wrong results silently? → **Logic**
6724
+ 4. Does it fail at a service/module boundary? → **Integration**
6725
+ 5. Does it work but too slowly or use too many resources? → **Performance**
6726
+ 6. Does it fail non-deterministically under load? → **Concurrency**
6727
+
6728
+ If ambiguous between two categories, investigate as the MORE severe one:
6729
+ - Syntax < Runtime < Logic < Integration < Performance < Concurrency
6730
+
6731
+ The rightward categories are harder to diagnose and their bugs have wider blast radius. Overestimating severity wastes a few tool calls. Underestimating it wastes the entire debug session.
6732
+ """
6733
+ _write_text_if_changed(root / "SKILL.md", skill_md)
6734
+ _write_text_if_changed(root / "references" / "debugging-heuristics.md", error_ref)
6735
+ _write_text_if_changed(
6736
+ generated_root / "systematic-debugging-capabilities.json",
6737
+ json_dumps({
6738
+ "generated_at": int(now_ts()),
6739
+ "skill": "systematic-debugging",
6740
+ "focus": ["causal-chain-tracing", "adaptive-depth", "multi-language-patterns", "invariant-analysis"],
6741
+ }, indent=2),
6742
+ )
6743
+
6744
+ def ensure_generated_code_engineering_mastery_skill(skills_root: Path):
6745
+ generated_root = skills_root / "generated"
6746
+ root = generated_root / "code-engineering-mastery"
6747
+ skill_md = """---
6748
+ name: code-engineering-mastery
6749
+ description: Adaptive software engineering methodology that scales from rapid prototyping to production-grade architecture — covering requirements analysis, design decision-making, implementation strategy, cross-language mastery, and verification-driven development.
6750
+ ---
6751
+
6752
+ # Code Engineering Mastery
6753
+
6754
+ ## Trigger
6755
+ Task involves implementing features, refactoring code, designing architecture, writing APIs, building systems, or any non-trivial coding work.
6756
+
6757
+ ## Adaptive Engineering Budget
6758
+
6759
+ Before writing any code, assess the task and select the engineering level. This determines how much design and verification overhead is warranted.
6760
+
6761
+ | Signal | Level | Design Budget | Verification |
6762
+ |--------|-------|--------------|-------------|
6763
+ | Single function, clear spec, isolated scope | **Tactical** | 0 — implement directly | Run once, spot-check |
6764
+ | New module or feature, touches 2-5 files | **Standard** | Sketch interfaces first, then implement | Unit tests for core logic |
6765
+ | Cross-cutting change, API design, architectural | **Strategic** | Full design: interfaces → data flow → error handling → tests | Integration tests, edge cases |
6766
+ | System-level, distributed, performance-critical | **Architectural** | Design doc: constraints → trade-offs → failure modes → capacity | Load test, chaos scenarios |
6767
+
6768
+ **Rule**: Match the level to the ACTUAL complexity, not the perceived importance. A critical bugfix can be Tactical; a "simple" feature touching auth is Strategic.
6769
+
6770
+ ## Phase 1: Understand Before Building
6771
+
6772
+ ### Codebase Reconnaissance (do ONCE per project)
6773
+ - Identify: entry points, build system, test framework, deployment mechanism.
6774
+ - Map: which directories own which concerns. Where does business logic live vs infra?
6775
+ - Note: code conventions (naming, error handling, logging patterns) — your code must match.
6776
+
6777
+ ### Requirements Decomposition
6778
+ - Separate WHAT (user-facing behavior) from HOW (implementation approach).
6779
+ - For each requirement, ask: what's the simplest implementation that's correct? Start there.
6780
+ - Identify the RISKY parts — things you're unsure about. Prototype those first, not last.
6781
+
6782
+ ## Phase 2: Design Decisions (Standard+ only)
6783
+
6784
+ ### Interface-First Design
6785
+ - Define the public API before writing implementation. What goes in, what comes out, what errors are possible?
6786
+ - Each function should have ONE reason to exist. If you can't name it clearly, the abstraction is wrong.
6787
+ - Prefer narrow interfaces. A function taking 6 parameters probably does too much.
6788
+
6789
+ ### Data Flow Analysis
6790
+ - Trace how data moves: input → validation → transformation → storage → output.
6791
+ - At each boundary, ask: what can go wrong? Invalid data? Timeout? Partial failure?
6792
+ - Design error handling at boundaries, not in the middle of business logic.
6793
+
6794
+ ### Dependency Direction
6795
+ - High-level modules should not depend on low-level details. Both should depend on abstractions.
6796
+ - If module A imports module B, A should be able to work with any implementation of B's interface.
6797
+ - Circular dependencies are ALWAYS a design error. Break the cycle by extracting the shared concept.
6798
+
6799
+ ## Phase 3: Implementation Strategy
6800
+
6801
+ ### The Build Order Principle
6802
+ Implement in the order that lets you VERIFY each piece:
6803
+ 1. Data models and types first (they're the foundation everything else depends on).
6804
+ 2. Core business logic (pure functions, no I/O — easiest to test).
6805
+ 3. Integration layer (connect core to external systems).
6806
+ 4. API/UI surface (thin layer that delegates to core).
6807
+
6808
+ ### Cross-Language Engineering Patterns
6809
+
6810
+ **Python**: Use type hints as executable documentation. `dataclass` for data, `Protocol` for interfaces. `pathlib` not string paths. Context managers for resources. `pytest` with parametrize for coverage.
6811
+
6812
+ **TypeScript**: Strict mode always. Define types/interfaces before implementation. `const` default, `let` only when needed. async/await over callbacks. Discriminated unions for state machines.
6813
+
6814
+ **Go**: Accept interfaces, return structs. Wrap errors with context (`fmt.Errorf("op: %w", err)`). Table-driven tests. Package by feature not by layer. Channels for coordination, mutexes for state protection.
6815
+
6816
+ **Rust**: Model states as types (newtype pattern). `Result<T, E>` for all fallible ops. `?` operator for propagation. Builder pattern for complex construction. Property-based testing with proptest.
6817
+
6818
+ **Java**: Records for immutable data. Optional over null. Sealed interfaces for type-safe hierarchies. JUnit 5 + AssertJ. Stream API for collection transforms.
6819
+
6820
+ **C/C++**: RAII for resource management. Smart pointers (`unique_ptr` default, `shared_ptr` when needed). Sanitizers in CI (`-fsanitize=address,undefined`). CMake + CTest. `string_view` over `const char*`.
6821
+
6822
+ ## Phase 4: Verification Strategy
6823
+
6824
+ ### Test Writing as Design Validation
6825
+ Tests aren't about coverage numbers — they verify that your DESIGN DECISIONS are correct.
6826
+ - Test the BEHAVIOR, not the implementation. "Given X input, expect Y output" — not "function calls Z internally".
6827
+ - Test boundaries: empty input, max size, null/none, concurrent access, timeout.
6828
+ - Test error paths: does the code fail CORRECTLY when things go wrong?
6829
+
6830
+ ### Verification Escalation
6831
+ | Level | What to Verify | How |
6832
+ |-------|---------------|-----|
6833
+ | Tactical | It works for the happy path | Run once manually |
6834
+ | Standard | Core logic + key edge cases | Unit tests |
6835
+ | Strategic | Integration + error paths + regression | Integration tests + CI |
6836
+ | Architectural | Performance + failure modes + recovery | Load test + fault injection |
6837
+
6838
+ ## Phase 5: Code Quality Self-Review
6839
+
6840
+ Before declaring "done", check:
6841
+ - **Correctness**: Does it satisfy ALL requirements, including edge cases?
6842
+ - **Performance**: Any obvious O(n²) where O(n) is possible? Unnecessary allocations in hot paths?
6843
+ - **Security**: User input validated? No injection paths? Secrets not hardcoded?
6844
+ - **Maintainability**: Would a new team member understand this code in 5 minutes?
6845
+ - **Error handling**: Every failure mode has a clear response. No silent swallowing.
6846
+
6847
+ ## Output Contract
6848
+ 1. Engineering level selected with reasoning.
6849
+ 2. Design decisions made (for Standard+).
6850
+ 3. Implementation: files created/modified with clear purpose for each.
6851
+ 4. Verification: tests written and passing, or manual verification documented.
6852
+ 5. If blocked: exact technical constraint and proposed alternatives.
6853
+ """
6854
+ _write_text_if_changed(root / "SKILL.md", skill_md)
6855
+ _write_text_if_changed(
6856
+ generated_root / "code-engineering-mastery-capabilities.json",
6857
+ json_dumps({
6858
+ "generated_at": int(now_ts()),
6859
+ "skill": "code-engineering-mastery",
6860
+ "focus": ["adaptive-engineering", "interface-first-design", "cross-language", "verification-driven"],
6861
+ }, indent=2),
6862
+ )
6863
+
6864
+ def ensure_generated_smart_file_navigation_skill(skills_root: Path):
6865
+ generated_root = skills_root / "generated"
6866
+ root = generated_root / "smart-file-navigation"
6867
+ skill_md = """---
6868
+ name: smart-file-navigation
6869
+ description: Adaptive codebase exploration engine that scales reading strategy to project size and task scope — from surgical line-range reads to systematic dependency-graph traversal, with built-in loop prevention and workspace awareness.
6870
+ ---
6871
+
6872
+ # Smart File Navigation
6873
+
6874
+ ## Trigger
6875
+ Task involves exploring unfamiliar code, tracing dependencies, navigating from errors to root cause, or any work requiring reading multiple files.
6876
+
6877
+ ## Adaptive Reading Budget
6878
+
6879
+ Decide your reading strategy BEFORE opening any file. Wrong strategy wastes your entire tool budget on irrelevant reads.
6880
+
6881
+ | Task Scope | Strategy | Read Budget | Key Principle |
6882
+ |-----------|----------|-------------|---------------|
6883
+ | Fix a specific error with file:line | **Surgical** | 2-4 reads | Read crash site ±20 lines. Follow ONE call chain. |
6884
+ | Implement feature in known area | **Focused** | 5-10 reads | Scan interfaces of affected modules. Read implementations you'll modify. |
6885
+ | Understand unfamiliar module | **Exploratory** | 8-15 reads | Structure scan → entry points → data flow → key abstractions. |
6886
+ | Full codebase assessment | **Systematic** | 15-25 reads | Top-down: build config → architecture → module boundaries → hot paths. |
6887
+
6888
+ **Rule**: Every read must answer a SPECIFIC question. If you can't state the question, don't read the file.
6889
+
6890
+ ## Core Method: Question-Driven Navigation
6891
+
6892
+ ### The Navigation Loop
6893
+ 1. **State your question**: "What does function X do?" / "Where is Y defined?" / "How does data flow from A to B?"
6894
+ 2. **Predict the answer's location**: Based on naming conventions, directory structure, imports.
6895
+ 3. **Read the minimum needed**: Use offset/limit. Never read 500 lines when 30 suffice.
6896
+ 4. **Record the answer**: Note it in your reasoning. Don't re-read to "remember".
6897
+ 5. **Derive the next question**: Each answer either resolves your task or generates a more specific question.
6898
+
6899
+ If step 5 generates the SAME question you already answered → you're in a loop. STOP and act on what you know.
6900
+
6901
+ ## Reading Strategies by File Size
6902
+
6903
+ | File Size | Strategy |
6904
+ |-----------|----------|
6905
+ | < 150 lines | Read entire file — it's cheap |
6906
+ | 150-500 lines | Read first 30 lines (imports, class defs), then jump to target with offset |
6907
+ | 500-2000 lines | Grep for the specific function/class, read 50-line window around match |
6908
+ | 2000+ lines | NEVER read more than 100 lines at a time. Grep → offset → targeted read |
6909
+
6910
+ ## Dependency Tracing
6911
+
6912
+ When you see an import, make a TRIAGE decision:
6913
+
6914
+ | Import Type | Action | Reasoning |
6915
+ |------------|--------|-----------|
6916
+ | Standard library (os, json, http) | **Skip** | You know what it does |
6917
+ | Third-party (requests, numpy, express) | **Skip** unless the bug is in the call | Read the API docs, not the source |
6918
+ | Project internal, irrelevant to your task | **Note path, skip** | You may need it later |
6919
+ | Project internal, relevant to your task | **Queue for reading** | Read after finishing current file |
6920
+
6921
+ ### Import Resolution Quick Reference
6922
+ - Python: `from foo.bar import X` → `foo/bar.py` or `foo/bar/__init__.py`
6923
+ - JS/TS: `import X from './foo'` → `./foo.js`, `./foo.ts`, `./foo/index.js`, `./foo/index.ts`
6924
+ - Go: `import "project/pkg/foo"` → `pkg/foo/*.go`
6925
+ - Rust: `use crate::foo::bar` → `src/foo/bar.rs` or `src/foo/mod.rs`
6926
+ - Java: `import com.example.Foo` → `src/.../com/example/Foo.java`
6927
+ - C/C++: `#include "foo.h"` → search include paths, check CMakeLists.txt
6928
+
6929
+ ## Error-Driven Navigation
6930
+
6931
+ When you have an error with a location:
6932
+ 1. Read `file:line` with offset = line-10, limit = 30. This gives you the crash site with context.
6933
+ 2. Identify the VARIABLE or EXPRESSION that caused the error.
6934
+ 3. Trace BACKWARD: where was that variable last assigned? Read THAT location.
6935
+ 4. If the assignment depends on another function's return value, read THAT function (just the return statements).
6936
+ 5. You now have the causal chain. Fix at the earliest point where the wrong value was introduced.
6937
+
6938
+ ## Loop Prevention (Critical)
6939
+
6940
+ ### Self-Monitoring Rules
6941
+ - **Track what you've read**: After each read, note "file X, lines Y-Z, learned: ...".
6942
+ - **Never re-read the same file range**: If you already read lines 50-100 of foo.py, you have that information. Use it.
6943
+ - **The 3-read limit**: If you've read 3 different files without taking ANY action (write, edit, bash), you're probably lost. Stop reading and:
6944
+ 1. Write down what you know so far.
6945
+ 2. Identify the SPECIFIC gap in your knowledge.
6946
+ 3. Take an action based on what you know (even if imperfect).
6947
+
6948
+ ### Recovery from Navigation Dead Ends
6949
+ If you can't find what you're looking for:
6950
+ - Check the build system (Makefile, package.json, CMakeLists.txt) — it knows where everything is.
6951
+ - Check the test directory — tests often reveal the intended API and file organization.
6952
+ - Use `bash grep -r "function_name" --include="*.py" -l` to locate definitions.
6953
+ - Ask yourself: am I looking for the right thing? Restate your question.
6954
+
6955
+ ## Workspace Awareness
6956
+
6957
+ ### First-Visit Protocol (do ONCE per project)
6958
+ 1. `bash ls -la` the root directory. Note the structure.
6959
+ 2. Read README.md or equivalent (first 50 lines).
6960
+ 3. Read the build config file (package.json / Makefile / Cargo.toml / go.mod).
6961
+ 4. Record on blackboard: project type, language, entry points, test location.
6962
+
6963
+ ### Mental Map Maintenance
6964
+ - After reading each file, update your mental model: "module X is responsible for Y, exports Z".
6965
+ - When navigating, consult your model BEFORE reading. "Based on the structure, the auth logic is probably in src/auth/".
6966
+ - If your prediction is wrong, update the model — don't just read more files hoping to stumble on it.
6967
+
6968
+ ## Output Contract
6969
+ 1. Files read with specific questions answered per file.
6970
+ 2. Key findings relevant to the current task.
6971
+ 3. Updated mental model of project structure (if first visit).
6972
+ 4. If target not found: files checked, hypotheses eliminated, next approach.
6973
+ """
6974
+ _write_text_if_changed(root / "SKILL.md", skill_md)
6975
+ _write_text_if_changed(
6976
+ generated_root / "smart-file-navigation-capabilities.json",
6977
+ json_dumps({
6978
+ "generated_at": int(now_ts()),
6979
+ "skill": "smart-file-navigation",
6980
+ "focus": ["question-driven-navigation", "adaptive-reading", "loop-prevention", "dependency-tracing"],
6981
+ }, indent=2),
6982
+ )
6983
+
6413
6984
  def ensure_generated_html_frontend_report_skills(skills_root: Path):
6414
6985
  generated_root = skills_root / "generated"
6415
6986
  html_root = generated_root / "html-report-engineering"
@@ -8850,6 +9421,9 @@ def ensure_runtime_skills(skills_root: Path):
8850
9421
  ensure_generated_image_coding_feedback_skill(skills_root)
8851
9422
  ensure_generated_skills_gen_skill(skills_root)
8852
9423
  ensure_generated_execution_recovery_skill(skills_root)
9424
+ ensure_generated_systematic_debugging_skill(skills_root)
9425
+ ensure_generated_code_engineering_mastery_skill(skills_root)
9426
+ ensure_generated_smart_file_navigation_skill(skills_root)
8853
9427
  ensure_generated_html_frontend_report_skills(skills_root)
8854
9428
  ensure_generated_deep_research_skills(skills_root)
8855
9429
  ensure_generated_research_scientific_skills(skills_root)
@@ -14426,7 +15000,7 @@ class SessionState:
14426
15000
  pass
14427
15001
  t = threading.Thread(target=_llm_match, daemon=True)
14428
15002
  t.start()
14429
- t.join(timeout=5.0)
15003
+ t.join(timeout=60.0)
14430
15004
  if llm_result:
14431
15005
  matched_names = llm_result
14432
15006
  self._emit("status", {"summary": f"skill discovery (LLM task analysis): {matched_names} ({trigger})"})
@@ -14436,11 +15010,31 @@ class SessionState:
14436
15010
  matched_names = self._keyword_match_skills(goal_low, skill_catalog)
14437
15011
  if matched_names:
14438
15012
  self._emit("status", {"summary": f"skill discovery (keyword fallback): {matched_names} ({trigger})"})
15013
+ debug_goal = any(
15014
+ token in goal_low
15015
+ for token in (
15016
+ "debug", "bug", "fix", "error", "traceback", "loop", "stuck",
15017
+ "卡死", "空循环", "死循环", "恢复", "recovery", "test", "测试",
15018
+ "integration", "集成", "architecture", "架构",
15019
+ )
15020
+ )
15021
+ if debug_goal and not matched_names:
15022
+ recovery_match = next(
15023
+ (
15024
+ str(s.get("qname", "") or s.get("name", "")).strip()
15025
+ for s in skill_catalog
15026
+ if "execution-degradation-recovery" in str(s.get("qname", "") or s.get("name", "")).strip().lower()
15027
+ ),
15028
+ "",
15029
+ )
15030
+ if recovery_match:
15031
+ matched_names = [recovery_match]
15032
+ self._emit("status", {"summary": f"skill discovery (recovery bias): {matched_names} ({trigger})"})
14439
15033
 
14440
15034
  # --- Path 3: Deferred LLM pickup if still running ---
14441
15035
  if not matched_names and t.is_alive():
14442
15036
  def _deferred_llm_pickup():
14443
- t.join(timeout=8.0)
15037
+ t.join(timeout=60.0)
14444
15038
  if llm_result and not self._loaded_skill_rows():
14445
15039
  for name_str in llm_result[:3]:
14446
15040
  try:
@@ -14460,7 +15054,7 @@ class SessionState:
14460
15054
  for name_str in matched_names[:4]:
14461
15055
  name_low = str(name_str or "").strip().lower()
14462
15056
  is_infra = any(pat in name_low for pat in _INFRA_SKILL_PATTERNS)
14463
- if is_infra:
15057
+ if is_infra and not (debug_goal and "execution-degradation-recovery" in name_low):
14464
15058
  infra_skills.append(name_str)
14465
15059
  else:
14466
15060
  task_skills.append(name_str)
@@ -14670,6 +15264,7 @@ class SessionState:
14670
15264
  "Skills are loaded ON-DEMAND — decide when you need one based on the CURRENT step, not upfront. "
14671
15265
  "For specialized output (reports, slides/PPT, deep research, code review, PDF analysis): "
14672
15266
  "call list_skills to discover options, then load_skill to activate the right one. "
15267
+ "For bug-fix, debugging, testing, integration, API, or architecture steps, proactively check for a matching skill instead of waiting until you are stuck. "
14673
15268
  "Load a skill AT THE MOMENT you begin the step that requires it. "
14674
15269
  "Unload it (via unload_skill) when moving to a different step that needs a different skill. "
14675
15270
  "For simple tasks, direct questions, and multimodal analysis, do NOT load skills. "
@@ -14812,7 +15407,7 @@ class SessionState:
14812
15407
  preview += ", ..."
14813
15408
  source_hint = (
14814
15409
  f" Discovered external raw code-corpus roots: {preview}. "
14815
- "Those raw corpora are source trees, not the query index itself, unless they have been imported into the Code Library."
15410
+ "Those raw corpora are source trees, not the query index itself, unless they have been imported into the Code Library."
14816
15411
  )
14817
15412
  return (
14818
15413
  f"{header}:\n"
@@ -14823,6 +15418,27 @@ class SessionState:
14823
15418
  "Use `query_code_library` to check readiness or retrieve grounded code references from the global library."
14824
15419
  )
14825
15420
 
15421
+ def _engineering_execution_boost_instruction(self) -> str:
15422
+ goal = str(self.runtime_reclassify_goal or self._latest_user_goal_text() or "").lower()
15423
+ signals = (
15424
+ "bug", "debug", "fix", "error", "traceback", "loop", "卡死", "空循环", "死循环",
15425
+ "测试", "test", "验证", "verify", "regression", "接口", "api", "架构", "architecture",
15426
+ "编程", "代码", "工程", "integration", "集成", "build", "compile", "lint",
15427
+ )
15428
+ if not any(sig in goal for sig in signals):
15429
+ return ""
15430
+ return (
15431
+ "ENGINEERING EXECUTION DISCIPLINE: "
15432
+ "For coding, bug-fix, architecture, integration, and testing work, proactively use the skill system when a matching skill exists. "
15433
+ "Do not wait for failure before calling list_skills/load_skill for debugging, API, frontend, parser, or recovery workflows. "
15434
+ "Already-loaded skills appear as <loaded-skill> messages — use them directly without reloading. "
15435
+ "Use a root-cause-first loop: inspect the exact error or failing behavior, read the implicated file or path, form one concrete hypothesis, apply one bounded fix, then run at least one fix-and-verify cycle before declaring success. "
15436
+ "If read_file or bash reports a missing path, empty folder, or mismatched filename, stop repeating the same lookup. "
15437
+ "Reconcile the path against uploads, recent file paths, file explorer entries, and close workspace matches; then either open the closest candidate or create the intended target. "
15438
+ "For large helper scripts or unfamiliar tools, prefer black-box usage first: run --help or inspect usage before reading large source files. "
15439
+ "When claiming progress, capture observable evidence such as command exit codes, test summaries, API responses, rendered output, or parsed results; file existence alone is not sufficient evidence."
15440
+ )
15441
+
14826
15442
  def _system_prompt(self) -> str:
14827
15443
  try:
14828
15444
  self._ensure_skills_ready(force=False)
@@ -14833,6 +15449,7 @@ class SessionState:
14833
15449
  research_hint = self._deep_research_boost_instruction()
14834
15450
  knowledge_hint = self._knowledge_library_prompt_block()
14835
15451
  code_hint = self._code_library_prompt_block()
15452
+ engineering_hint = self._engineering_execution_boost_instruction()
14836
15453
  code_ref_block = self._runtime_code_reference_prompt_block()
14837
15454
  runtime_level = int(self.runtime_task_level or 0)
14838
15455
  runtime_mode = self._effective_execution_mode()
@@ -14841,6 +15458,7 @@ class SessionState:
14841
15458
  research_block = f"{research_hint}\n\n" if research_hint else ""
14842
15459
  knowledge_block = f"{knowledge_hint}\n\n" if knowledge_hint else ""
14843
15460
  code_hint_block = f"{code_hint}\n\n" if code_hint else ""
15461
+ engineering_block = f"{engineering_hint}\n\n" if engineering_hint else ""
14844
15462
  code_block = f"{code_ref_block}\n\n" if code_ref_block else ""
14845
15463
  _is_single_no_enhance = (
14846
15464
  runtime_mode == EXECUTION_MODE_SINGLE
@@ -14881,6 +15499,7 @@ class SessionState:
14881
15499
  f"{research_block}"
14882
15500
  f"{knowledge_block}"
14883
15501
  f"{code_hint_block}"
15502
+ f"{engineering_block}"
14884
15503
  f"{code_block}"
14885
15504
  f"{model_language_instruction(self.ui_language)}\n\n"
14886
15505
  f"Uploads:\n{uploads_ctx}\n\n"
@@ -19005,30 +19624,108 @@ body{padding:18px}
19005
19624
  lines = []
19006
19625
  remaining = max_chars
19007
19626
  for item in items:
19627
+ item_kind = str(item.get("kind", "file") or "file")
19628
+ wp = str(item.get("workspace_path", "") or "")
19629
+ filename = str(item.get("filename", "") or "")
19008
19630
  lines.append(
19009
- f"- {item.get('filename','')} => {item.get('workspace_path','')} "
19010
- f"({item.get('kind','file')}, {item.get('size',0)} bytes)"
19631
+ f"- {filename} => {wp} "
19632
+ f"({item_kind}, {item.get('size',0)} bytes)"
19011
19633
  )
19012
19634
  excerpt = str(item.get("parsed_excerpt", "")).strip()
19013
19635
  if not excerpt or remaining < 200:
19636
+ full_ref = ""
19637
+ if wp:
19638
+ if item_kind not in ("text", "code"):
19639
+ from pathlib import PurePosixPath
19640
+ stem = PurePosixPath(wp).stem
19641
+ parent = str(PurePosixPath(wp).parent)
19642
+ full_ref = f"{parent}/{stem}.parsed.md" if parent != "." else f"{stem}.parsed.md"
19643
+ else:
19644
+ full_ref = wp
19645
+ if full_ref:
19646
+ lines.append(f" (full content available at: {full_ref} — use read_file for the complete source/text)")
19647
+ continue
19648
+ chunk_cap = min(2200, remaining)
19649
+ if self._upload_is_code_like(item):
19650
+ chunk_cap = min(1200, remaining)
19651
+ elif item_kind == "text":
19652
+ chunk_cap = min(1600, remaining)
19653
+ chunk = self._prepare_upload_excerpt(
19654
+ filename,
19655
+ wp,
19656
+ item_kind,
19657
+ excerpt,
19658
+ max_chars=chunk_cap,
19659
+ max_lines=36 if self._upload_is_code_like(item) else 72,
19660
+ )
19661
+ if not chunk:
19014
19662
  continue
19015
- chunk = excerpt[: min(len(excerpt), min(3000, remaining))]
19016
19663
  lines.append(f"<uploaded_excerpt path=\"{item.get('workspace_path','')}\">")
19017
19664
  lines.append(chunk)
19018
19665
  lines.append("</uploaded_excerpt>")
19019
19666
  remaining -= len(chunk)
19020
- # 提示模型可直接读取 .parsed.md 文件获取完整解析文本
19021
- item_kind = item.get("kind", "file")
19022
- if item_kind not in ("text", "code"):
19023
- wp = item.get("workspace_path", "")
19024
- if wp:
19667
+ full_ref = ""
19668
+ if wp:
19669
+ if item_kind not in ("text", "code"):
19025
19670
  from pathlib import PurePosixPath
19026
19671
  stem = PurePosixPath(wp).stem
19027
19672
  parent = str(PurePosixPath(wp).parent)
19028
- parsed_rel = f"{parent}/{stem}.parsed.md" if parent != "." else f"{stem}.parsed.md"
19029
- lines.append(f" (parsed text available at: {parsed_rel} — use read_file to access full content)")
19673
+ full_ref = f"{parent}/{stem}.parsed.md" if parent != "." else f"{stem}.parsed.md"
19674
+ else:
19675
+ full_ref = wp
19676
+ if full_ref:
19677
+ lines.append(f" (full content available at: {full_ref} — use read_file for the complete source/text)")
19030
19678
  return "\n".join(lines)
19031
19679
 
19680
+ def _upload_is_code_like(self, item: dict | None = None, *, filename: str = "", workspace_path: str = "", kind: str = "") -> bool:
19681
+ info = item if isinstance(item, dict) else {}
19682
+ name = str(info.get("filename", "") or filename or "").strip().lower()
19683
+ rel = str(info.get("workspace_path", "") or workspace_path or "").strip().lower()
19684
+ kind_value = str(info.get("kind", "") or kind or "").strip().lower()
19685
+ target = rel or name
19686
+ if kind_value == "code":
19687
+ return True
19688
+ code_like_ext = {
19689
+ ".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".java", ".c",
19690
+ ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".go", ".rs",
19691
+ ".rb", ".php", ".swift", ".kt", ".kts", ".scala", ".sh", ".bash", ".zsh",
19692
+ ".fish", ".sql", ".html", ".htm", ".css", ".sass", ".scss", ".less", ".styl",
19693
+ ".json", ".jsonc", ".yaml", ".yml", ".xml", ".toml", ".ini", ".cfg", ".conf",
19694
+ ".properties", ".md", ".mdx", ".rst", ".txt", ".log", ".ipynb", ".vue",
19695
+ ".svelte", ".cs", ".m", ".mm", ".r", ".pl", ".pm", ".f", ".f90", ".f95",
19696
+ ".f03", ".f08", ".for", ".fpp", ".zig", ".nim", ".v", ".d", ".adb", ".ads",
19697
+ ".asm", ".s", ".ps1", ".gradle", ".groovy", ".jl", ".lua", ".mk", ".cmake",
19698
+ ".ml", ".mli", ".nix", ".pas", ".proto", ".sol", ".sv", ".svh", ".vh",
19699
+ ".vhd", ".vhdl", ".tcl", ".tf", ".tfvars", ".hcl", ".tex", ".wat", ".diff",
19700
+ ".patch", ".graphql", ".gql", ".prisma",
19701
+ }
19702
+ special_names = {"dockerfile", "makefile", "cmakelists.txt", "requirements.txt"}
19703
+ if any(target.endswith(ext) for ext in code_like_ext):
19704
+ return True
19705
+ if Path(name or target).name.lower() in special_names:
19706
+ return True
19707
+ return False
19708
+
19709
+ def _prepare_upload_excerpt(
19710
+ self,
19711
+ filename: str,
19712
+ workspace_path: str,
19713
+ kind: str,
19714
+ text: str,
19715
+ *,
19716
+ max_chars: int,
19717
+ max_lines: int,
19718
+ ) -> str:
19719
+ body = str(text or "").strip()
19720
+ if not body:
19721
+ return ""
19722
+ is_code = self._upload_is_code_like(filename=filename, workspace_path=workspace_path, kind=kind)
19723
+ line_cap = min(max(1, int(max_lines or 1)), 40 if is_code else 80)
19724
+ lines = body.replace("\r\n", "\n").split("\n")
19725
+ if len(lines) > line_cap:
19726
+ body = "\n".join(lines[:line_cap])
19727
+ return trim(body, max_chars)
19728
+
19032
19729
  def add_upload(self, filename: str, raw: bytes, mime: str = "") -> dict:
19033
19730
  safe_name = self._safe_upload_name(filename)
19034
19731
  upload_id = make_id("upload")
@@ -19099,7 +19796,8 @@ body{padding:18px}
19099
19796
  parsed_excerpt = ""
19100
19797
  needs_async_parse = False
19101
19798
  if kind == "text":
19102
- parsed_excerpt = trim(self._decode_text_bytes(raw), 24_000)
19799
+ excerpt_cap = 8_000 if self._upload_is_code_like(filename=safe_name, kind=kind) else 12_000
19800
+ parsed_excerpt = trim(self._decode_text_bytes(raw), excerpt_cap)
19103
19801
  elif kind in ("pdf", "csv", "excel", "presentation", "document"):
19104
19802
  needs_async_parse = True
19105
19803
  workspace_target = self._upload_workspace_target(safe_name)
@@ -19126,8 +19824,17 @@ body{padding:18px}
19126
19824
  self.updated_at = now_ts()
19127
19825
  self._persist()
19128
19826
  if parsed_excerpt:
19129
- bb_content = f"[upload:{safe_name}]\n{trim(parsed_excerpt, BLACKBOARD_MAX_TEXT - 200)}"
19130
- self._blackboard_append_section("research_notes", "system", bb_content)
19827
+ bb_excerpt = self._prepare_upload_excerpt(
19828
+ safe_name,
19829
+ workspace_rel,
19830
+ kind,
19831
+ parsed_excerpt,
19832
+ max_chars=min(4000, max(1200, BLACKBOARD_MAX_TEXT - 200)),
19833
+ max_lines=60,
19834
+ )
19835
+ if bb_excerpt:
19836
+ bb_content = f"[upload:{safe_name}]\n{bb_excerpt}"
19837
+ self._blackboard_append_section("research_notes", "system", bb_content)
19131
19838
  if not needs_async_parse:
19132
19839
  self._emit(
19133
19840
  "upload",
@@ -19261,8 +19968,17 @@ body{padding:18px}
19261
19968
  self._persist()
19262
19969
  break
19263
19970
  if parsed_excerpt:
19264
- bb_content = f"[upload:{safe_name}]\n{trim(parsed_excerpt, BLACKBOARD_MAX_TEXT - 200)}"
19265
- self._blackboard_append_section("research_notes", "system", bb_content)
19971
+ bb_excerpt = self._prepare_upload_excerpt(
19972
+ safe_name,
19973
+ self._session_rel(workspace_target),
19974
+ kind,
19975
+ parsed_excerpt,
19976
+ max_chars=min(4000, max(1200, BLACKBOARD_MAX_TEXT - 200)),
19977
+ max_lines=60,
19978
+ )
19979
+ if bb_excerpt:
19980
+ bb_content = f"[upload:{safe_name}]\n{bb_excerpt}"
19981
+ self._blackboard_append_section("research_notes", "system", bb_content)
19266
19982
  # Emit parse completed event
19267
19983
  workspace_rel = self._session_rel(workspace_target)
19268
19984
  self._emit("upload", {
@@ -21491,11 +22207,134 @@ body{padding:18px}
21491
22207
  pass
21492
22208
  return fp
21493
22209
 
22210
+ def _suggest_workspace_paths(self, rel: str, limit: int = 6, max_scan: int = 1800) -> list[str]:
22211
+ target = str(rel or "").strip().replace("\\", "/")
22212
+ if not target:
22213
+ return []
22214
+ wanted = PurePosixPath(target)
22215
+ desired_name = wanted.name.lower()
22216
+ desired_compact = desired_name.replace(" ", "")
22217
+ desired_stem = wanted.stem.lower()
22218
+ parent_hint = str(wanted.parent).strip(". /").lower()
22219
+ if not desired_name and not parent_hint:
22220
+ return []
22221
+ scored: list[tuple[int, str]] = []
22222
+ seen: set[str] = set()
22223
+ scanned = 0
22224
+ skip_dirs = {".git", ".hg", ".svn", "node_modules", "__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache"}
22225
+ for root, dirs, files in os.walk(self.files_root):
22226
+ dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith(".")]
22227
+ entries = list(files) + list(dirs)
22228
+ for name in entries:
22229
+ scanned += 1
22230
+ if scanned > max_scan:
22231
+ break
22232
+ full = Path(root) / name
22233
+ try:
22234
+ rel_path = full.relative_to(self.files_root).as_posix()
22235
+ except Exception:
22236
+ continue
22237
+ if rel_path in seen:
22238
+ continue
22239
+ low = rel_path.lower()
22240
+ base = name.lower()
22241
+ compact = base.replace(" ", "")
22242
+ stem = Path(base).stem
22243
+ score = 0
22244
+ if desired_name and base == desired_name:
22245
+ score += 90
22246
+ if desired_compact and compact == desired_compact:
22247
+ score += 80
22248
+ if desired_stem and stem == desired_stem:
22249
+ score += 65
22250
+ if desired_name and desired_name in low:
22251
+ score += 28
22252
+ if desired_stem and desired_stem in stem:
22253
+ score += 22
22254
+ if parent_hint and parent_hint in low:
22255
+ score += 12
22256
+ if score <= 0:
22257
+ continue
22258
+ seen.add(rel_path)
22259
+ scored.append((score, rel_path))
22260
+ if scanned > max_scan:
22261
+ break
22262
+ scored.sort(key=lambda row: (-row[0], len(row[1]), row[1]))
22263
+ return [path for _, path in scored[: max(1, int(limit or 1))]]
22264
+
22265
+ def _render_directory_read(self, fp: Path, rel: str, limit: int | None = None, offset: int | None = None) -> str:
22266
+ entries = sorted(
22267
+ list(fp.iterdir()),
22268
+ key=lambda p: (0 if p.is_dir() else 1, p.name.lower()),
22269
+ )
22270
+ total = len(entries)
22271
+ if total == 0:
22272
+ return f"[read_file directory path={rel} entries=0]\n(empty directory)"
22273
+ offset_val = max(0, int(offset or 0))
22274
+ requested_limit = max(1, int(limit or 60))
22275
+ if offset_val >= total:
22276
+ return (
22277
+ f"[read_file directory path={rel} entries=0 of {total} offset={offset_val}]\n"
22278
+ "[end_of_directory]"
22279
+ )
22280
+ page = entries[offset_val: offset_val + requested_limit]
22281
+ lines = [
22282
+ f"[read_file directory path={rel} entries={offset_val + 1}-{offset_val + len(page)} of {total} offset={offset_val} limit={requested_limit}]"
22283
+ ]
22284
+ for child in page:
22285
+ kind = "dir" if child.is_dir() else "file"
22286
+ try:
22287
+ size_text = f" ({child.stat().st_size} bytes)" if child.is_file() else ""
22288
+ except Exception:
22289
+ size_text = ""
22290
+ lines.append(f"{kind} {child.name}{size_text}")
22291
+ next_offset = offset_val + len(page)
22292
+ if next_offset < total:
22293
+ lines.append(f"[next_page read_file path=\"{rel}\" offset={next_offset} limit={requested_limit}]")
22294
+ if offset_val > 0:
22295
+ prev_offset = max(0, offset_val - requested_limit)
22296
+ lines.append(f"[prev_page read_file path=\"{rel}\" offset={prev_offset} limit={requested_limit}]")
22297
+ return "\n".join(lines)
22298
+
22299
+ def _read_text_with_fallback(self, fp: Path) -> str:
22300
+ tried: list[str] = []
22301
+ for enc in ("utf-8", "utf-8-sig", locale.getpreferredencoding(False) or "utf-8", "gb18030"):
22302
+ enc_norm = str(enc or "").strip() or "utf-8"
22303
+ if enc_norm in tried:
22304
+ continue
22305
+ tried.append(enc_norm)
22306
+ try:
22307
+ return fp.read_text(encoding=enc_norm)
22308
+ except UnicodeDecodeError:
22309
+ continue
22310
+ return fp.read_text(encoding="utf-8", errors="replace")
22311
+
22312
+ def _render_missing_read_hint(self, rel: str) -> str:
22313
+ suggestions = self._suggest_workspace_paths(rel, limit=6)
22314
+ parent = PurePosixPath(str(rel or "").replace("\\", "/")).parent.as_posix()
22315
+ lines = [f"Error: FileNotFoundError: {rel}"]
22316
+ if suggestions:
22317
+ lines.append("Closest workspace matches:")
22318
+ lines.extend(f"- {cand}" for cand in suggestions)
22319
+ if parent and parent not in {".", ""}:
22320
+ lines.append(
22321
+ f"Path hint: if `{parent}` is the intended folder, read that directory or create `{rel}` with write_file."
22322
+ )
22323
+ lines.append(
22324
+ "Next action: reconcile the path against uploads/recent file paths, open one close match, "
22325
+ "or create the missing target instead of repeating the same failed read."
22326
+ )
22327
+ return "\n".join(lines)
22328
+
21494
22329
  def _run_read(self, path: str, limit: int | None = None, offset: int | None = None) -> str:
21495
22330
  try:
21496
22331
  rel = self._normalize_tool_path_text(path)
21497
22332
  fp = self._fuzzy_resolve_path(self._session_path(rel))
21498
22333
  rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
22334
+ if not fp.exists():
22335
+ return self._render_missing_read_hint(rel)
22336
+ if fp.is_dir():
22337
+ return self._render_directory_read(fp, rel, limit=limit, offset=offset)
21499
22338
  # Multimodal: detect image/audio/video files and handle natively
21500
22339
  ext = fp.suffix.lower() if fp.suffix else ""
21501
22340
  if ext in IMAGE_EXTS:
@@ -21504,7 +22343,7 @@ body{padding:18px}
21504
22343
  return self._run_read_media(fp, rel, "audio")
21505
22344
  if ext in VIDEO_EXTS:
21506
22345
  return self._run_read_media(fp, rel, "video")
21507
- lines = fp.read_text(encoding="utf-8").splitlines()
22346
+ lines = self._read_text_with_fallback(fp).splitlines()
21508
22347
  total_lines = len(lines)
21509
22348
  if total_lines == 0:
21510
22349
  return ""
@@ -23514,8 +24353,8 @@ body{padding:18px}
23514
24353
  for pt in bb_src_todos[:40]:
23515
24354
  if not isinstance(pt, dict):
23516
24355
  continue
23517
- raw_content = trim(str(pt.get("content", "") or ""), 1500)
23518
- raw_full = trim(str(pt.get("full_content", "") or ""), 1500)
24356
+ raw_content = trim(str(pt.get("content", "") or ""), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
24357
+ raw_full = trim(str(pt.get("full_content", "") or ""), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
23519
24358
  # Migration: if full_content is empty but content has sub-steps, auto-split
23520
24359
  if not raw_full and raw_content and pt.get("category") == "plan_step":
23521
24360
  normalized = _mid_re_norm.sub(r"\n\1", raw_content)
@@ -23525,7 +24364,7 @@ body{padding:18px}
23525
24364
  clean_todos.append({
23526
24365
  "id": trim(str(pt.get("id", "") or ""), 20),
23527
24366
  "content": trim(raw_content, 400),
23528
- "full_content": trim(raw_full, 1500),
24367
+ "full_content": trim(raw_full, PLAN_STEP_FULL_CONTENT_MAX_CHARS),
23529
24368
  "status": str(pt.get("status", "pending") or "pending") if str(pt.get("status", "pending") or "pending") in ("pending", "in_progress", "completed") else "pending",
23530
24369
  "category": trim(str(pt.get("category", "") or ""), 40),
23531
24370
  "plan_step_index": int(pt.get("plan_step_index", -1)) if pt.get("plan_step_index") is not None else -1,
@@ -24456,6 +25295,74 @@ body{padding:18px}
24456
25295
  return True
24457
25296
  return False
24458
25297
 
25298
+ def _tool_result_output_excerpt(self, item: dict, max_chars: int = 160) -> str:
25299
+ if not isinstance(item, dict):
25300
+ return ""
25301
+ raw = trim(str(item.get("output", "") or "").strip(), max_chars * 2)
25302
+ if not raw:
25303
+ return ""
25304
+ clean, _ = filter_runtime_noise_lines(raw)
25305
+ text = trim(clean.replace("\r\n", "\n"), max_chars * 2)
25306
+ if not text:
25307
+ return ""
25308
+ lines = [ln.strip() for ln in text.split("\n") if ln.strip()]
25309
+ if not lines:
25310
+ return ""
25311
+ return trim(lines[0], max_chars)
25312
+
25313
+ def _tool_results_have_validation_evidence(self, plan_step: dict, results: list[dict]) -> bool:
25314
+ if not isinstance(plan_step, dict):
25315
+ return False
25316
+ rows = [r for r in (results or []) if isinstance(r, dict) and r.get("ok", False)]
25317
+ if not rows:
25318
+ return False
25319
+ step_text = str(plan_step.get("full_content", "") or plan_step.get("content", "") or "").lower()
25320
+ phase = self._plan_step_phase_hint(step_text)
25321
+ wrote_files = any(str(r.get("name", "")) in ("write_file", "edit_file") for r in rows)
25322
+ read_back = any(
25323
+ str(r.get("name", "")) == "read_file" and bool(self._tool_result_output_excerpt(r, 140))
25324
+ for r in rows
25325
+ )
25326
+ knowledge_signal = any(
25327
+ str(r.get("name", "")) in ("write_to_blackboard", "read_from_blackboard", "query_code_library", "query_knowledge_library")
25328
+ for r in rows
25329
+ )
25330
+ bash_rows = [r for r in rows if str(r.get("name", "")) == "bash"]
25331
+ observed_signal = False
25332
+ compile_signal = False
25333
+ test_signal = False
25334
+ negative_hints = ("error:", "failed", "failure", "traceback", "fatal error", "assertionerror", "exception")
25335
+ compile_hints = ("compiled successfully", "build successful", "build succeeded", "syntax ok", "lint passed", "no issues found", "0 errors")
25336
+ test_hints = ("test passed", "tests passed", "all tests passed", "0 failed", "100%", "ok", "success")
25337
+ validation_cmd_tokens = ("pytest", "test", "unittest", "jest", "vitest", "cargo test", "go test", "build", "compile", "lint", "run")
25338
+ for row in bash_rows:
25339
+ cmd = str(row.get("args", {}).get("command", "") or "").strip().lower()
25340
+ excerpt = self._tool_result_output_excerpt(row, 180)
25341
+ low = excerpt.lower()
25342
+ if excerpt and not any(neg in low for neg in negative_hints):
25343
+ observed_signal = True
25344
+ if any(tok in cmd for tok in validation_cmd_tokens):
25345
+ observed_signal = True
25346
+ if low and any(tok in low for tok in compile_hints) and not any(neg in low for neg in negative_hints):
25347
+ compile_signal = True
25348
+ if low and any(tok in low for tok in test_hints) and not any(neg in low for neg in negative_hints):
25349
+ test_signal = True
25350
+ wants_test = phase in ("test", "review") or any(
25351
+ tok in step_text for tok in ("test", "pytest", "unit", "integration", "验证", "測試", "测试", "回归", "assert")
25352
+ )
25353
+ wants_runtime_validation = wants_test or phase == "implement" or any(
25354
+ tok in step_text for tok in ("verify", "validation", "check", "lint", "build", "compile", "运行", "校验", "檢查")
25355
+ )
25356
+ if wants_test:
25357
+ return test_signal or (bool(bash_rows) and observed_signal)
25358
+ if phase == "implement":
25359
+ return wrote_files and (compile_signal or test_signal or observed_signal or read_back)
25360
+ if phase in ("research", "design"):
25361
+ return knowledge_signal or read_back or observed_signal or wrote_files
25362
+ if wants_runtime_validation:
25363
+ return observed_signal or read_back or wrote_files
25364
+ return wrote_files or read_back or knowledge_signal or observed_signal
25365
+
24459
25366
  def _has_test_pass_evidence(self, board: dict | None = None) -> bool:
24460
25367
  bb = board if isinstance(board, dict) else self._ensure_blackboard()
24461
25368
  logs = bb.get("execution_logs", []) if isinstance(bb.get("execution_logs"), list) else []
@@ -24481,6 +25388,7 @@ body{padding:18px}
24481
25388
  return
24482
25389
  code_count = len(bb.get("code_artifacts", {}) or {})
24483
25390
  research_count = len(bb.get("research_notes", []) or [])
25391
+ exec_count = len(bb.get("execution_logs", []) or [])
24484
25392
  feedback_pass = self._manager_feedback_passed_from_blackboard(bb)
24485
25393
 
24486
25394
  for todo in todos:
@@ -24493,12 +25401,15 @@ body{padding:18px}
24493
25401
  completed_at=float(now_ts()),
24494
25402
  evidence=self._ui_text("evidence_structure_analyzed"),
24495
25403
  )
24496
- elif cat == "implement" and code_count > 0:
25404
+ elif cat == "implement" and code_count > 0 and (exec_count > 0 or feedback_pass):
24497
25405
  todo.update(
24498
25406
  status="completed",
24499
25407
  completed_at=float(now_ts()),
24500
25408
  completed_by="developer",
24501
- evidence=self._ui_text("evidence_files_produced", count=code_count),
25409
+ evidence=trim(
25410
+ f"{self._ui_text('evidence_files_produced', count=code_count)} + observable execution evidence",
25411
+ 200,
25412
+ ),
24502
25413
  )
24503
25414
  elif cat == "compile_test" and self._has_compile_pass_evidence(bb):
24504
25415
  todo.update(
@@ -24724,18 +25635,19 @@ body{padding:18px}
24724
25635
  isinstance(r, dict) and r.get("ok", False) and str(r.get("name", "")) == "bash"
24725
25636
  for r in results
24726
25637
  )
25638
+ validation_ok = self._tool_results_have_validation_evidence(current, results)
24727
25639
  phase_evidence = False
24728
- if phase in ("research", "design") and wrote_files:
25640
+ if phase in ("research", "design") and validation_ok:
24729
25641
  phase_evidence = True
24730
- elif phase == "implement" and wrote_files and ran_bash_ok:
25642
+ elif phase == "implement" and wrote_files and validation_ok:
24731
25643
  phase_evidence = True
24732
- elif phase in ("test", "review") and ran_bash_ok:
25644
+ elif phase in ("test", "review") and ran_bash_ok and validation_ok:
24733
25645
  phase_evidence = True
24734
25646
  # Advance when:
24735
25647
  # - Manager requested AND worker produced output, OR
24736
25648
  # - All subtasks completed AND worker produced output, OR
24737
25649
  # - Phase heuristics confirm (write+bash for implement)
24738
- has_strong_evidence = worker_produced_output and (
25650
+ has_strong_evidence = validation_ok and worker_produced_output and (
24739
25651
  manager_requested or subtasks_all_done or phase_evidence
24740
25652
  )
24741
25653
  if has_strong_evidence:
@@ -24814,10 +25726,15 @@ body{padding:18px}
24814
25726
  parts.append(f"{name}: {path}")
24815
25727
  elif name == "bash":
24816
25728
  cmd = trim(str(r.get("args", {}).get("command", "") or ""), 80)
24817
- parts.append(f"bash: {cmd}")
25729
+ out = self._tool_result_output_excerpt(r, 120)
25730
+ parts.append(f"bash: {cmd}" + (f" => {out}" if out else ""))
24818
25731
  elif name == "read_file":
24819
25732
  path = str(r.get("args", {}).get("path", "") or "")
24820
- parts.append(f"read: {path}")
25733
+ out = self._tool_result_output_excerpt(r, 90)
25734
+ parts.append(f"read: {path}" + (f" => {out}" if out else ""))
25735
+ elif name in ("write_to_blackboard", "query_code_library", "query_knowledge_library"):
25736
+ out = self._tool_result_output_excerpt(r, 100)
25737
+ parts.append(f"{name}" + (f": {out}" if out else ""))
24821
25738
  return trim("; ".join(parts) or "post-execution evidence", 200)
24822
25739
 
24823
25740
  def _get_active_plan_step(self, board: dict | None = None) -> dict | None:
@@ -25009,7 +25926,7 @@ body{padding:18px}
25009
25926
  return self.todo.update(preserved + normalized)
25010
25927
 
25011
25928
  def _append_instruction_bubble(self, content: str, *, target_roles: tuple[str, ...] = (), summary: str = "") -> bool:
25012
- text = trim(str(content or "").strip(), 2200)
25929
+ text = trim(str(content or "").strip(), PLAN_NOTICE_BODY_MAX_CHARS)
25013
25930
  if not text:
25014
25931
  return False
25015
25932
  recent = self.messages[-8:]
@@ -25033,7 +25950,7 @@ body{padding:18px}
25033
25950
  return True
25034
25951
 
25035
25952
  def _build_plan_guidance_notice_data(self, content: str, *, summary: str = "") -> dict:
25036
- text = trim(str(content or "").strip(), 2200)
25953
+ text = trim(str(content or "").strip(), PLAN_NOTICE_BODY_MAX_CHARS)
25037
25954
  if not text:
25038
25955
  return {}
25039
25956
  lang = normalize_ui_language(getattr(self, "ui_language", DEFAULT_UI_LANGUAGE))
@@ -25080,7 +25997,7 @@ body{padding:18px}
25080
25997
  }
25081
25998
 
25082
25999
  def _append_plan_guidance_bubble(self, content: str, *, target_roles: tuple[str, ...] = (), summary: str = "") -> bool:
25083
- text = trim(str(content or "").strip(), 2200)
26000
+ text = trim(str(content or "").strip(), PLAN_NOTICE_BODY_MAX_CHARS)
25084
26001
  if not text:
25085
26002
  return False
25086
26003
  recent = self.messages[-10:]
@@ -25375,22 +26292,20 @@ body{padding:18px}
25375
26292
  str(r.get("name", "")) == "bash" and r.get("ok", False)
25376
26293
  for r in tool_results
25377
26294
  )
26295
+ validation_ok = self._tool_results_have_validation_evidence(current, tool_results)
25378
26296
  # Auto-advance conditions:
25379
26297
  should_advance = False
25380
26298
  # Priority 1: Check if worker subtasks are all completed (most reliable signal)
25381
26299
  subtasks_done = self._step_subtasks_all_completed(current)
25382
- if subtasks_done and (wrote_files or ran_bash_ok):
26300
+ if subtasks_done and validation_ok:
25383
26301
  should_advance = True
25384
- # Priority 2: Phase-based heuristics (strict implement requires BOTH write + bash)
26302
+ # Priority 2: Phase-based heuristics (require observable evidence, not just file creation)
25385
26303
  if not should_advance:
25386
- if phase in ("research", "design") and wrote_files:
26304
+ if phase in ("research", "design") and validation_ok:
25387
26305
  should_advance = True
25388
- elif phase == "implement" and wrote_files and ran_bash_ok:
25389
- # Strict: implement step needs both file writes AND successful bash
26306
+ elif phase == "implement" and wrote_files and validation_ok:
25390
26307
  should_advance = True
25391
- elif phase in ("test", "review") and ran_bash_ok and not any(
25392
- not r.get("ok", False) for r in tool_results if str(r.get("name", "")) == "bash"
25393
- ):
26308
+ elif phase in ("test", "review") and ran_bash_ok and validation_ok:
25394
26309
  should_advance = True
25395
26310
  # Also check if the agent explicitly mentioned step completion
25396
26311
  if not should_advance:
@@ -25402,10 +26317,10 @@ body{padding:18px}
25402
26317
  break
25403
26318
  step_done_signals = ("step completed", "步骤完成", "step done", "完成了", "已完成",
25404
26319
  "next step", "下一步", "proceed to step", "进入下一")
25405
- if any(sig in last_text for sig in step_done_signals):
26320
+ if validation_ok and any(sig in last_text for sig in step_done_signals):
25406
26321
  should_advance = True
25407
26322
  if should_advance:
25408
- evidence = f"single-agent auto-advance: phase={phase}, wrote={wrote_files}, bash_ok={ran_bash_ok}"
26323
+ evidence = self._collect_step_evidence(current, {"tool_results": tool_results})
25409
26324
  self._advance_plan_step(evidence=evidence, actor="single")
25410
26325
  try:
25411
26326
  self._inject_current_plan_step_execution_hints()
@@ -28103,19 +29018,6 @@ body{padding:18px}
28103
29018
  seen.add(low_tail)
28104
29019
  keep_lines.append(tail)
28105
29020
  continue
28106
- if low.startswith("tasks to complete:"):
28107
- continue
28108
- if re.match(r"^\d+(?:\.\d+)*[.)]\s+", s):
28109
- continue
28110
- if re.match(r"^[-*]\s+", s):
28111
- continue
28112
- if re.match(
28113
- r"(?i)^(mkdir\s+-p|run:|create directories:|create project|create directory|initialize project|cmake\b|python\s+-m\s+venv\b|npx\b)",
28114
- s,
28115
- ):
28116
- continue
28117
- if re.match(r"^(创建|初始化|运行|目录结构|项目根目录结构)[::]?", s):
28118
- continue
28119
29021
  norm = re.sub(r"\s+", " ", s).strip().lower()
28120
29022
  if norm and norm not in seen:
28121
29023
  seen.add(norm)
@@ -29587,6 +30489,7 @@ body{padding:18px}
29587
30489
  role_key = self._sanitize_agent_role(role) or "developer"
29588
30490
  skills_block = self._skills_awareness_block(for_role=role_key)
29589
30491
  code_note = self._runtime_code_reference_prompt_block(max_chars=2600)
30492
+ engineering_note = self._engineering_execution_boost_instruction()
29590
30493
  plan_todo_note = self._plan_todo_discipline_prompt(role=role_key)
29591
30494
  base = (
29592
30495
  f"You are {self._agent_display_name(role_key)} in a multi-agent coding system. "
@@ -29598,6 +30501,7 @@ body{padding:18px}
29598
30501
  "Use blackboard for shared state, ask_colleague for inter-agent communication. "
29599
30502
  "Keep outputs concise and action-oriented. "
29600
30503
  f"{code_note + ' ' if code_note else ''}"
30504
+ f"{engineering_note + ' ' if engineering_note else ''}"
29601
30505
  f"{_detect_os_shell_instruction()} "
29602
30506
  f"{model_language_instruction(self.ui_language)} "
29603
30507
  )
@@ -29638,7 +30542,9 @@ body{padding:18px}
29638
30542
  "For runtime errors: identify the traceback, exception type, and the triggering line. "
29639
30543
  "For test failures: identify which test failed, the assertion, expected vs actual. "
29640
30544
  "For lint/type errors: identify the rule violation and exact location. "
29641
- "4) When sending fix_request via ask_colleague, you MUST include: "
30545
+ "4) Do not approve based only on created files. Require observable evidence such as exit codes, test summaries, API responses, screenshots, or parsed results. "
30546
+ "5) Do not declare success until at least one fix-and-verify cycle has completed. "
30547
+ "6) When sending fix_request via ask_colleague, you MUST include: "
29642
30548
  "the exact error output, the file and line number, the root cause analysis, "
29643
30549
  "the error category (compilation/runtime/test/lint/build/deploy), "
29644
30550
  "and the precise fix (what to change FROM and TO). "
@@ -29651,6 +30557,10 @@ body{padding:18px}
29651
30557
  "The skill's workflow, tools, and file structure OVERRIDE the plan's implementation "
29652
30558
  "approach — if the plan says 'use python-pptx' but the skill says 'use PptxGenJS', "
29653
30559
  "use PptxGenJS. The skill defines HOW to implement; the plan defines WHAT to do. "
30560
+ "AUTONOMOUS SKILL LOADING: When starting a coding, debugging, or architecture task, "
30561
+ "call list_skills to discover available skills, then load_skill to activate the most relevant ones. "
30562
+ "Load skills BEFORE you start working, not after you're stuck. "
30563
+ "Already-loaded skills appear as <loaded-skill> messages in your context — use them directly without reloading. "
29654
30564
  "TODO TRACKING (mandatory): "
29655
30565
  "When a plan step is active, follow the current todo subtask order instead of inventing a parallel path. "
29656
30566
  "After completing ONE subtask, call TodoWrite immediately — mark that subtask as 'completed' and move the next one to 'in_progress' before doing more work. "
@@ -29663,15 +30573,17 @@ body{padding:18px}
29663
30573
  "4) If edit_file fails 'text not found': IMMEDIATELY re-read the file, compare whitespace, retry with exact content. "
29664
30574
  "5) If edit_file fails 2+ times on same file: switch to write_file to rewrite entire file. "
29665
30575
  "6) After every successful edit, run build/test to verify. "
29666
- "NEVER loop on read_file without attempting a concrete edit_file or write_file call. "
30576
+ "NEVER loop on read_file without attempting a concrete edit_file, write_file, path reconciliation, or verification call. "
29667
30577
  "PROBLEM-SOLVING (critical): "
29668
30578
  "When you discover missing files, broken imports, or incomplete source code: "
29669
30579
  "A) Think deeply about what the missing content should contain based on ALL available context "
29670
30580
  "(documentation, Makefile, imports, existing code patterns, architecture docs). "
29671
30581
  "B) CREATE the missing files yourself using write_file — do not wait or re-read. "
29672
30582
  "C) If compilation fails due to missing dependencies, write stub implementations. "
29673
- "D) NEVER re-read the same directory/file more than twice after 2 reads, you MUST act. "
29674
- "E) If truly blocked, explain WHY to the user and propose alternatives. "
30583
+ "D) If read_file or bash says a path is missing, empty, or mismatched, reconcile the path against uploads, recent files, and close matches before trying again. "
30584
+ "E) NEVER re-read the same directory/file more than twice after 2 reads, you MUST act. "
30585
+ "F) Do not declare success until at least one fix-and-verify cycle is complete and the evidence is observable. "
30586
+ "G) If truly blocked, explain WHY to the user and propose alternatives. "
29675
30587
  )
29676
30588
 
29677
30589
  def _seed_multi_agent_contexts_if_needed(self, user_text: str = ""):
@@ -31956,7 +32868,7 @@ body{padding:18px}
31956
32868
  else:
31957
32869
  _repeat_delegation_count = 0
31958
32870
  _prev_delegation_hash = _cur_hash
31959
- if _repeat_delegation_count >= 3:
32871
+ if _repeat_delegation_count >= 15:
31960
32872
  self._emit("status", {"summary": f"manager stuck: repeated identical delegation x{_repeat_delegation_count + 1}; forcing advance"})
31961
32873
  _bb_stuck = self._ensure_blackboard()
31962
32874
  _stuck_step = next(
@@ -31990,6 +32902,13 @@ body{padding:18px}
31990
32902
  media_inputs_pool=media_inputs_pool,
31991
32903
  media_seen_ts_by_role=media_seen_ts_by_role,
31992
32904
  )
32905
+ # Sync-mode skill auto-discovery: same mechanism as plan mode's step-completed trigger.
32906
+ # Runs on early rounds for developer/explorer. Uses goal_sig dedup — no re-loading if already loaded.
32907
+ if role in ("developer", "explorer") and rounds_used <= 2:
32908
+ try:
32909
+ self._refresh_loaded_skills_for_execution_focus(trigger=f"sync-worker-pre:{role}")
32910
+ except Exception:
32911
+ pass
31993
32912
  board_before_fp = self._watchdog_state_fingerprint(self._ensure_blackboard())
31994
32913
  step = self._multi_agent_turn(
31995
32914
  role,
@@ -31999,6 +32918,49 @@ body{padding:18px}
31999
32918
  self._blackboard_update_from_worker_step(role, step)
32000
32919
  # Post-execution plan step advancement (replaces pre-execution advancement)
32001
32920
  self._post_execution_plan_step_check(route, step if isinstance(step, dict) else {})
32921
+ # Sync-mode failure recovery: detect all-tools-failed and inject recovery hint + auto-load debugging skill
32922
+ _step_dict = step if isinstance(step, dict) else {}
32923
+ _step_results = _step_dict.get("tool_results", []) or []
32924
+ if _step_results:
32925
+ _sync_err_count = sum(1 for r in _step_results if isinstance(r, dict) and not r.get("ok", False))
32926
+ _sync_ok_count = sum(1 for r in _step_results if isinstance(r, dict) and r.get("ok", False))
32927
+ if _sync_err_count > 0 and _sync_ok_count == 0:
32928
+ # All tool calls failed in this worker turn — inject recovery guidance
32929
+ _failed_tools = [str(r.get("name", "")) for r in _step_results if isinstance(r, dict)][:4]
32930
+ _err_outputs = " | ".join(
32931
+ trim(str(r.get("output", "") or ""), 120)
32932
+ for r in _step_results if isinstance(r, dict) and not r.get("ok", False)
32933
+ )[:400]
32934
+ self._append_agent_context_message(
32935
+ role,
32936
+ {
32937
+ "role": "user",
32938
+ "content": (
32939
+ "<failure-recovery>"
32940
+ f"All tool calls failed in this turn ({', '.join(_failed_tools)}). "
32941
+ f"Errors: {_err_outputs}\n"
32942
+ "Before retrying, STOP and diagnose:\n"
32943
+ "1) If a debugging skill is available, call load_skill('systematic-debugging') and follow its workflow.\n"
32944
+ "2) Read the EXACT error message — identify the root cause, not just the symptom.\n"
32945
+ "3) Form ONE hypothesis about the cause before making any changes.\n"
32946
+ "4) Apply ONE targeted fix, then verify with a test/build command.\n"
32947
+ "5) If still blocked after 2 attempts, report the exact blocker to the user."
32948
+ "</failure-recovery>"
32949
+ ),
32950
+ "ts": now_ts(),
32951
+ "agent_role": role,
32952
+ },
32953
+ mirror_to_global=False,
32954
+ )
32955
+ # Auto-load systematic-debugging if failure involves code errors
32956
+ _code_err_kw = ("bash", "compile", "syntax", "test", "build", "traceback", "error:")
32957
+ if any(kw in _err_outputs.lower() for kw in _code_err_kw):
32958
+ _bb_sk = self._ensure_blackboard().get("loaded_skills", {})
32959
+ if isinstance(_bb_sk, dict) and "systematic-debugging" not in _bb_sk:
32960
+ try:
32961
+ self._load_skill_with_cache("systematic-debugging", load_source="auto:sync-worker-failure")
32962
+ except Exception:
32963
+ pass
32002
32964
  # Fix 6b: Pure sync no-plan — read worker-done signal and notify manager
32003
32965
  _bb_sync = self._ensure_blackboard()
32004
32966
  if _bb_sync.pop("sync_worker_round_done", False):
@@ -32390,7 +33352,7 @@ body{padding:18px}
32390
33352
  })
32391
33353
  self._emit("message", {
32392
33354
  "role": "assistant",
32393
- "text": trim(bubble_text, int(ASSISTANT_MESSAGE_EVENT_MAX_CHARS)),
33355
+ "text": trim(bubble_text, int(PLAN_MESSAGE_EVENT_MAX_CHARS)),
32394
33356
  "summary": "plan-mode proposal",
32395
33357
  "agent_role": "planner",
32396
33358
  })
@@ -32910,6 +33872,9 @@ body{padding:18px}
32910
33872
  f"- When a loaded skill defines a specific workflow, follow that workflow's actual tools and scripts.\n"
32911
33873
  f"- For complex tasks, produce 8-15 detailed steps, not 3-5 vague ones\n"
32912
33874
  f"- Each step should be completable in 1-3 tool calls\n"
33875
+ f"- Every major step must include an explicit acceptance signal: how to know the step is done.\n"
33876
+ f"- Acceptance signals must use observable evidence such as exit code, test summary, API response, rendered output, parsed rows, numerical thresholds, or screenshot/result inspection.\n"
33877
+ f"- File creation alone is NOT valid acceptance evidence.\n"
32913
33878
  f"\nSTEP STRUCTURE — MAJOR STEPS WITH SUB-STEPS:\n"
32914
33879
  f"Organize steps into MAJOR numbered groups. Each major step has:\n"
32915
33880
  f" 1) A summary title line: \"N. Summary Title\" (e.g., \"1. Project Initialization\")\n"
@@ -32949,6 +33914,8 @@ body{padding:18px}
32949
33914
  "- Include compile/build/lint verification steps after implementation steps.\n"
32950
33915
  "- Include a dedicated testing step with SPECIFIC run commands (e.g. `python -m pytest`, `npm test`) before final review.\n"
32951
33916
  "- Testing step sub-steps must end with: actually RUNNING the tests and checking exit code, not just writing test files.\n"
33917
+ "- Testing steps must include expected results or pass criteria (for example: exit code 0, `3 passed`, HTTP 200 with required fields, rendered page shows target widget).\n"
33918
+ "- Non-test implementation steps must also state the validation artifact to inspect before the step can be treated as done.\n"
32952
33919
  "- For large plans (10+ steps), insert intermediate test checkpoints.\n"
32953
33920
  "- If the task modifies existing code, include a regression test step.\n"
32954
33921
  "- The LAST step must include a sub-step: 'Generate delivery report: summarize what was built, how to run it, and key outputs.'\n"
@@ -33104,7 +34071,7 @@ body{padding:18px}
33104
34071
  grouped_steps = self._group_plan_steps(raw_steps if isinstance(raw_steps, list) else [])
33105
34072
  plan_todos: list[dict] = []
33106
34073
  for i, step in enumerate(grouped_steps[:max(1, int(limit))]):
33107
- step_text = trim(str(step or "").strip(), 1500)
34074
+ step_text = trim(str(step or "").strip(), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
33108
34075
  if not step_text:
33109
34076
  continue
33110
34077
  step_lines = step_text.split("\n")
@@ -33186,8 +34153,8 @@ body{padding:18px}
33186
34153
  bb = self._ensure_blackboard()
33187
34154
  plan = bb.get("plan", {}) if isinstance(bb.get("plan"), dict) else {}
33188
34155
  plan_choice = str(plan.get("chosen", "") or choice_id).strip() or choice_id
33189
- title = trim(str(plan.get("title", "") or "").strip(), 240)
33190
- summary = trim(str(plan.get("summary", "") or "").strip(), 1200)
34156
+ title = trim(str(plan.get("title", "") or "").strip(), 800)
34157
+ summary = trim(str(plan.get("summary", "") or "").strip(), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
33191
34158
  if not title or not summary:
33192
34159
  proposal = self.runtime_plan_proposal or {}
33193
34160
  chosen = next(
@@ -33198,9 +34165,9 @@ body{padding:18px}
33198
34165
  None,
33199
34166
  )
33200
34167
  if not title:
33201
- title = trim(str((chosen or {}).get("title", "") or plan_choice).strip(), 240)
34168
+ title = trim(str((chosen or {}).get("title", "") or plan_choice).strip(), 800)
33202
34169
  if not summary:
33203
- summary = trim(str((chosen or {}).get("summary", "") or "").strip(), 1200)
34170
+ summary = trim(str((chosen or {}).get("summary", "") or "").strip(), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
33204
34171
  todos = bb.get("project_todos", [])
33205
34172
  plan_todos = [t for t in todos if t.get("category") == "plan_step"]
33206
34173
  if not plan_todos and isinstance(plan.get("steps"), list):
@@ -33272,11 +34239,11 @@ body{padding:18px}
33272
34239
  return self._write_plan_file(content)
33273
34240
 
33274
34241
  def _format_plan_bubble_preselection(self, proposal: dict) -> str:
33275
- """Condensed bubble for UI (under PLAN_BUBBLE_MAX_CHARS). No full step listing."""
34242
+ """Condensed bubble for UI. Keeps major details, but is still shorter than plan.md."""
33276
34243
  lines = [self._ui_text("plan_bubble_title")]
33277
34244
  context = str(proposal.get("context", "") or "").strip()
33278
34245
  if context:
33279
- lines.append(self._ui_text("plan_bubble_background", context=trim(context, 300)))
34246
+ lines.append(self._ui_text("plan_bubble_background", context=trim(context, 1200)))
33280
34247
  recommended = str(proposal.get("recommended", "") or "").strip()
33281
34248
  options = proposal.get("options", [])
33282
34249
  if not isinstance(options, list):
@@ -33293,7 +34260,7 @@ body{padding:18px}
33293
34260
  lines.append(header)
33294
34261
  summary = str(opt.get("summary", "") or "").strip()
33295
34262
  if summary:
33296
- lines.append(trim(summary, 200))
34263
+ lines.append(trim(summary, 800))
33297
34264
  steps = opt.get("steps", [])
33298
34265
  step_count = len(steps) if isinstance(steps, list) else 0
33299
34266
  risk = str(opt.get("risk", "") or "").strip()
@@ -33724,8 +34691,8 @@ body{padding:18px}
33724
34691
  profile = bb.get("task_profile", {}) if isinstance(bb.get("task_profile"), dict) else {}
33725
34692
  judgement = bb.get("manager_judgement", {}) if isinstance(bb.get("manager_judgement"), dict) else {}
33726
34693
  grouped_steps = self._group_plan_steps(chosen.get("steps", []))
33727
- chosen_title = trim(str(chosen.get("title", "") or choice_id).strip(), 240)
33728
- chosen_summary = trim(str(chosen.get("summary", "") or "").strip(), 1200)
34694
+ chosen_title = trim(str(chosen.get("title", "") or choice_id).strip(), 800)
34695
+ chosen_summary = trim(str(chosen.get("summary", "") or "").strip(), PLAN_STEP_FULL_CONTENT_MAX_CHARS)
33729
34696
  # Preserve current complexity unless the user explicitly changes it elsewhere.
33730
34697
  _current_complexity = trim(
33731
34698
  str(
@@ -34040,6 +35007,13 @@ body{padding:18px}
34040
35007
  self.agent_round_index = int(self.agent_round_index) + 1
34041
35008
  self.current_phase = "model-call"
34042
35009
  self.current_tool_name = ""
35010
+ # Single-mode skill auto-discovery: same as plan mode. Runs on first 2 rounds only.
35011
+ # Uses goal_sig dedup — if skills already loaded for this goal, no-op.
35012
+ if int(self.agent_round_index) <= 2:
35013
+ try:
35014
+ self._refresh_loaded_skills_for_execution_focus(trigger="single-worker-pre")
35015
+ except Exception:
35016
+ pass
34043
35017
  if level_budget > 0 and int(self.agent_round_index) > int(level_budget):
34044
35018
  force_single_tool_rounds = max(force_single_tool_rounds, 2)
34045
35019
  if not compact_budget_notified:
@@ -35048,6 +36022,22 @@ body{padding:18px}
35048
36022
  "ts": now_ts(),
35049
36023
  }
35050
36024
  )
36025
+ # Auto-load debugging skill on code/compilation/test failures
36026
+ _code_error_keywords = ("bash", "compile", "syntax", "test", "build", "traceback")
36027
+ _is_code_error = any(
36028
+ kw in str(last_fault_reason or "").lower()
36029
+ for kw in _code_error_keywords
36030
+ )
36031
+ if _is_code_error:
36032
+ _bb_skills = self._ensure_blackboard().get("loaded_skills", {})
36033
+ if isinstance(_bb_skills, dict) and "systematic-debugging" not in _bb_skills:
36034
+ try:
36035
+ self._load_skill_with_cache(
36036
+ "systematic-debugging",
36037
+ load_source="auto:code-error-recovery"
36038
+ )
36039
+ except Exception:
36040
+ pass
35051
36041
  self._emit(
35052
36042
  "status",
35053
36043
  {
@@ -36782,10 +37772,15 @@ body[data-ui-style="trad"] button,body[data-ui-style="trad"] a{border-radius:10p
36782
37772
  .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}
36783
37773
  .danger{color:var(--warn);border-color:#f3c0c0}
36784
37774
  .disabled{pointer-events:none;opacity:.5}
36785
- .status-cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:12px}
36786
- .stat{background:linear-gradient(140deg,#fff,#f7fbff);border:1px solid var(--line);border-radius:14px;padding:10px 12px}
37775
+ .status-cards{display:grid;grid-template-columns:minmax(220px,260px) minmax(520px,920px) minmax(300px,360px);justify-content:center;gap:12px;margin-bottom:12px}
37776
+ .top-stats-primary{grid-column:1 / span 2;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;min-width:0}
37777
+ .top-stats-model{grid-column:3;min-width:0}
37778
+ .stat{background:linear-gradient(140deg,#fff,#f7fbff);border:1px solid var(--line);border-radius:14px;padding:10px 12px;min-width:0}
37779
+ .stat.compact{padding:8px 10px}
36787
37780
  .stat .k{font-size:.78rem;color:var(--muted)}
36788
37781
  .stat .v{font-size:1.25rem;font-weight:700}
37782
+ .stat.compact .v{font-size:1.05rem}
37783
+ .stat.model .v{font-size:.94rem;line-height:1.25;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;white-space:normal;overflow-wrap:anywhere;word-break:break-word}
36789
37784
  main{display:grid;grid-template-columns:minmax(220px,260px) minmax(520px,920px) minmax(300px,360px);justify-content:center;gap:12px;height:74vh;min-height:620px;max-height:74vh}
36790
37785
  .panel{background:var(--card);backdrop-filter:blur(8px);border:1px solid #fff;box-shadow:0 10px 28px rgba(14,30,62,.08);border-radius:16px;padding:12px;display:flex;flex-direction:column;min-height:0;min-width:0;height:100%}
36791
37786
  body[data-ui-style="trad"] .panel{border-radius:14px;backdrop-filter:none;box-shadow:0 6px 18px rgba(14,30,62,.05);border-color:#dfe7f2}
@@ -37103,7 +38098,9 @@ h3{font-size:.96rem;margin:10px 0 6px}
37103
38098
  100%{box-shadow:0 0 0 0 rgba(19,184,166,0)}
37104
38099
  }
37105
38100
  @media (max-width:1180px){
37106
- .status-cards{grid-template-columns:repeat(2,minmax(0,1fr))}
38101
+ .status-cards{grid-template-columns:1fr}
38102
+ .top-stats-primary{grid-column:1;grid-template-columns:repeat(3,minmax(0,1fr))}
38103
+ .top-stats-model{grid-column:1}
37107
38104
  main{grid-template-columns:1fr;height:auto;max-height:none;min-height:0}
37108
38105
  .panel{min-height:280px}
37109
38106
  .chat #chat{height:46vh;max-height:46vh;flex:none}
@@ -37113,6 +38110,9 @@ h3{font-size:.96rem;margin:10px 0 6px}
37113
38110
  .ctx-live{margin-left:0;width:100%;min-width:0}
37114
38111
  #runtimeScroll{max-height:42vh}
37115
38112
  }
38113
+ @media (max-width:900px){
38114
+ .top-stats-primary{grid-template-columns:repeat(2,minmax(0,1fr))}
38115
+ }
37116
38116
  .popup-dropdown{position:relative;display:inline-block}
37117
38117
  .popup-menu{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;max-height:360px;overflow-y:auto}
37118
38118
  .popup-menu-wide{min-width:200px}
@@ -37207,7 +38207,7 @@ const I18N={
37207
38207
  upload_pick_file:'Choose files',
37208
38208
  upload_drop_release:'Drop to upload',
37209
38209
  sec_todos:'Todos',sec_tasks:'Tasks',sec_activity:'Activity',sec_commands:'Commands',sec_diffs:'File Diffs',sec_files:'Files',sec_catalog:'Catalog',
37210
- stat_sessions:'Sessions',stat_running:'Running',stat_messages:'Messages',stat_model:'Model',
38210
+ stat_sessions:'Sessions',stat_running:'Running',stat_messages:'Messages',stat_global_tasks:'Global Tasks',stat_daily_sessions:'Daily Sessions',stat_model:'Model',
37211
38211
  no_sessions:'No sessions',no_todos:'No todos',no_tasks:'No tasks',no_activity:'No activity',no_commands:'No commands',no_diffs:'No file diffs',no_files:'No files',no_catalog:'No catalog',no_uploads:'No uploads',
37212
38212
  running:'running',idle:'idle',open:'open',completed:'completed',blocked:'blocked',
37213
38213
  status_pending:'PENDING',status_in_progress:'IN PROGRESS',status_completed:'COMPLETED',status_blocked:'BLOCKED',status_deleted:'DELETED',
@@ -37243,7 +38243,7 @@ const I18N={
37243
38243
  upload_pick_file:'选择文件',
37244
38244
  upload_drop_release:'释放以上传文件',
37245
38245
  sec_todos:'Todos',sec_tasks:'Tasks',sec_activity:'Activity',sec_commands:'Commands',sec_diffs:'File Diffs',sec_files:'文件',sec_catalog:'Catalog',
37246
- stat_sessions:'会话',stat_running:'运行中',stat_messages:'消息',stat_model:'模型',
38246
+ stat_sessions:'会话',stat_running:'运行中',stat_messages:'消息',stat_global_tasks:'全局任务',stat_daily_sessions:'每日会话',stat_model:'模型',
37247
38247
  no_sessions:'暂无会话',no_todos:'暂无 Todos',no_tasks:'暂无 Tasks',no_activity:'暂无活动',no_commands:'暂无命令',no_diffs:'暂无文件差异',no_files:'暂无文件',no_catalog:'暂无目录',no_uploads:'暂无上传',
37248
38248
  running:'运行中',idle:'空闲',open:'未完成',completed:'已完成',blocked:'阻塞',
37249
38249
  status_pending:'待处理',status_in_progress:'进行中',status_completed:'已完成',status_blocked:'阻塞',status_deleted:'已删除',
@@ -37279,7 +38279,7 @@ const I18N={
37279
38279
  upload_pick_file:'選擇檔案',
37280
38280
  upload_drop_release:'釋放以上傳檔案',
37281
38281
  sec_todos:'Todos',sec_tasks:'Tasks',sec_activity:'Activity',sec_commands:'Commands',sec_diffs:'File Diffs',sec_files:'檔案',sec_catalog:'Catalog',
37282
- stat_sessions:'會話',stat_running:'執行中',stat_messages:'訊息',stat_model:'模型',
38282
+ stat_sessions:'會話',stat_running:'執行中',stat_messages:'訊息',stat_global_tasks:'全域任務',stat_daily_sessions:'每日會話',stat_model:'模型',
37283
38283
  no_sessions:'尚無會話',no_todos:'尚無 Todos',no_tasks:'尚無 Tasks',no_activity:'尚無活動',no_commands:'尚無命令',no_diffs:'尚無檔案差異',no_files:'尚無檔案',no_catalog:'尚無目錄',no_uploads:'尚無上傳',
37284
38284
  running:'執行中',idle:'閒置',open:'未完成',completed:'已完成',blocked:'阻塞',
37285
38285
  status_pending:'待處理',status_in_progress:'進行中',status_completed:'已完成',status_blocked:'阻塞',status_deleted:'已刪除',
@@ -37315,7 +38315,7 @@ const I18N={
37315
38315
  upload_pick_file:'ファイルを選択',
37316
38316
  upload_drop_release:'ドロップしてアップロード',
37317
38317
  sec_todos:'Todos',sec_tasks:'Tasks',sec_activity:'Activity',sec_commands:'Commands',sec_diffs:'File Diffs',sec_files:'ファイル',sec_catalog:'Catalog',
37318
- stat_sessions:'セッション',stat_running:'実行中',stat_messages:'メッセージ',stat_model:'モデル',
38318
+ stat_sessions:'セッション',stat_running:'実行中',stat_messages:'メッセージ',stat_global_tasks:'タスク',stat_daily_sessions:'日次セッション',stat_model:'モデル',
37319
38319
  no_sessions:'セッションはありません',no_todos:'Todo はありません',no_tasks:'Task はありません',no_activity:'アクティビティなし',no_commands:'コマンドなし',no_diffs:'差分なし',no_files:'ファイルなし',no_catalog:'カタログなし',no_uploads:'アップロードなし',
37320
38320
  running:'実行中',idle:'待機中',open:'未完了',completed:'完了',blocked:'ブロック',
37321
38321
  status_pending:'未着手',status_in_progress:'進行中',status_completed:'完了',status_blocked:'ブロック',status_deleted:'削除済み',
@@ -37760,7 +38760,9 @@ function tailSig(rows,count,mapper){const arr=Array.isArray(rows)?rows:[];if(!ar
37760
38760
  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}`}
37761
38761
  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('|')}
37762
38762
  function sessionsSignature(list){const rows=Array.isArray(list)?list:[];const sig=tailSig(rows,6,row=>`${String(row?.id||'')}:${row?.running?1:0}:${Number(row?.message_count||0)}:${Number(row?.updated_at||0)}`);const aid=String(S.activeId||'').trim();let activeSig='-';if(aid){const activeRow=rows.find(row=>String(row?.id||'')===aid);if(activeRow){activeSig=`${aid}:${activeRow?.running?1:0}:${Number(activeRow?.message_count||0)}:${Number(activeRow?.updated_at||0)}`}else{activeSig=`missing:${aid}`}}return `${rows.length}|active=${activeSig}|${sig}`}
37763
- function renderStats(){const sessions=S.sessions.length;const running=S.sessions.filter(x=>x.running).length;const msgs=S.sessions.reduce((n,x)=>n+x.message_count,0);const model=S.config?.model||'-';E('topStats').innerHTML=[[t('stat_sessions'),sessions],[t('stat_running'),running],[t('stat_messages'),msgs],[t('stat_model'),model]].map(([k,v])=>`<div class=\"stat\"><div class=\"k\">${esc(k)}</div><div class=\"v\">${esc(v)}</div></div>`).join('')}
38763
+ function _statInfinite(n){const v=Number(n);return(Number.isFinite(v)&&v>0)?String(v):''}
38764
+ function applyRuntimeConfigStats(cfg){if(!cfg||typeof cfg!=='object')return;S.config=S.config||{};if(cfg.scheduler&&typeof cfg.scheduler==='object')S.config.scheduler=cfg.scheduler;if(cfg.session_creation_limit&&typeof cfg.session_creation_limit==='object')S.config.session_creation_limit=cfg.session_creation_limit;if(Object.prototype.hasOwnProperty.call(cfg,'daily_session_limit'))S.config.daily_session_limit=cfg.daily_session_limit;if(Object.prototype.hasOwnProperty.call(cfg,'download_js_lib_enabled'))S.config.download_js_lib_enabled=!!cfg.download_js_lib_enabled;if(Object.prototype.hasOwnProperty.call(cfg,'request_timeout_default'))S.config.request_timeout_default=cfg.request_timeout_default;if(Object.prototype.hasOwnProperty.call(cfg,'run_timeout'))S.config.run_timeout=cfg.run_timeout;if(Object.prototype.hasOwnProperty.call(cfg,'model')&&String(cfg.model||'').trim())S.config.model=cfg.model}
38765
+ function renderStats(){const sessions=S.sessions.length;const running=S.sessions.filter(x=>x.running).length;const msgs=S.sessions.reduce((n,x)=>n+x.message_count,0);const model=S.config?.model||'-';const sched=(S.config&&typeof S.config.scheduler==='object')?S.config.scheduler:{};const quota=(S.config&&typeof S.config.session_creation_limit==='object')?S.config.session_creation_limit:{};const runningTotal=Math.max(0,Number(sched?.running_total||0));const maxTasks=Number(sched?.max_user||0);const globalTasks=`${runningTotal}/${_statInfinite(maxTasks)}`;const dailySessions=(quota&&quota.enabled)?`${Math.max(0,Number(quota.used||0))}/${Math.max(0,Number(quota.limit||0))}`:'∞';const compact=[[t('stat_sessions'),sessions],[t('stat_running'),running],[t('stat_messages'),msgs],[t('stat_global_tasks'),globalTasks],[t('stat_daily_sessions'),dailySessions]].map(([k,v])=>`<div class=\"stat compact\"><div class=\"k\">${esc(k)}</div><div class=\"v\">${esc(v)}</div></div>`).join('');const modelHtml=`<div class=\"stat model\"><div class=\"k\">${esc(t('stat_model'))}</div><div class=\"v\">${esc(model)}</div></div>`;E('topStats').innerHTML=`<div class=\"top-stats-primary\">${compact}</div><div class=\"top-stats-model\">${modelHtml}</div>`}
37764
38766
  function renderSessions(){const html=S.sessions.map(s=>`<div class=\"session-item${s.id===S.activeId?' active':''}\" data-id=\"${esc(s.id)}\"><div><strong>${esc(s.title)}</strong></div><div class=\"mono\">${s.running?t('running'):t('idle')} · ${s.message_count} msgs</div></div>`).join('');setPanelHtml('sessionList',html||`<div class=\"mono\">${esc(t('no_sessions'))}</div>`);for(const el of document.querySelectorAll('#sessionList .session-item')){el.onclick=()=>selectSession(el.getAttribute('data-id'))}}
37765
38767
  function _syncActiveSessionSummaryFromSnapshot(){const sid=String(S.activeId||'').trim();const snap=S.snap;if(!sid||!snap)return false;const rows=Array.isArray(S.sessions)?S.sessions.slice():[];let idx=rows.findIndex(row=>String(row?.id||'')===sid);const running=!!snap?.running;let updatedAt=Number(snap?.updated_at||0);if(!Number.isFinite(updatedAt)||updatedAt<=0){updatedAt=(Date.now()/1000)}let msgCount=Number(snap?.message_count);if(!Number.isFinite(msgCount)||msgCount<0){const arr=Array.isArray(snap?.messages)?snap.messages:[];let cnt=0;for(const row of arr){if(String(row?.role||'').trim()==='tool')continue;cnt+=1}msgCount=cnt}msgCount=Math.max(0,Math.floor(Number(msgCount)||0));const title=String(snap?.title||'').trim();if(idx<0){rows.push({id:sid,title:title||sid,running:running,updated_at:updatedAt,message_count:msgCount});idx=rows.length-1}else{const cur=rows[idx]||{};const next={...cur};let changed=false;if(!!cur.running!==running){next.running=running;changed=true}if(Number(cur.message_count||0)!==msgCount){next.message_count=msgCount;changed=true}if(Number(cur.updated_at||0)!==updatedAt){next.updated_at=updatedAt;changed=true}if(title&&String(cur.title||'')!==title){next.title=title;changed=true}if(!changed)return false;rows[idx]=next}rows.sort((a,b)=>Number(b?.updated_at||0)-Number(a?.updated_at||0));S.sessions=rows;return true}
37766
38768
  function diffLineClass(line){const t=String(line||'').trimStart();if(t.startsWith('+')||/^\\d+\\s+\\+\\s/.test(t))return 'diff-line-add';if(t.startsWith('-')||/^\\d+\\s+-\\s/.test(t))return 'diff-line-del';if(t.startsWith('@@')||t==='⋮'||t.startsWith('⋮ '))return 'diff-line-hunk';return ''}
@@ -39220,7 +40222,8 @@ function _chatVirtBuildMessageNode(m){
39220
40222
  const pillsHtml=pills.map(x=>`<span class=\"manager-delegate-pill\">${esc(String(x))}</span>`).join('');
39221
40223
  const routeHtml=`<div class=\"manager-delegate-route\"><span class=\"agent-bus-pill manager\">${esc(t('role_manager'))}</span><span class=\"agent-bus-arrow\">→</span><span class=\"agent-bus-pill${targetRole?(' '+targetRole):''}\">${esc(targetLabel)}</span></div>`;
39222
40224
  const objectiveHtml=(objective&&instruction&&objective.toLowerCase()===instruction.toLowerCase())?'':(objective?`<div class=\"manager-delegate-line\"><span>${esc(t('event_objective'))}</span><div>${esc(objective)}</div></div>`:'');
39223
- const instructionHtml=instruction?`<div class=\"manager-delegate-line\"><span>${esc(t('event_instruction'))}</span><div>${esc(instruction)}</div></div>`:'';
40225
+ const instructionKey=`${String(m._vk||'')}:manager-instruction`;
40226
+ const instructionHtml=instruction?`<div class=\"manager-delegate-line\"><span>${esc(t('event_instruction'))}</span><div class=\"msg-md\">${renderMarkdownCached(instruction,instructionKey)}</div></div>`:'';
39224
40227
  d.innerHTML=`${roleBadge}<div class=\"manager-delegate-card\"><div class=\"manager-delegate-head\">${esc(t('event_manager_delegate_title'))}</div>${routeHtml}<div class=\"manager-delegate-pills\">${pillsHtml}</div>${objectiveHtml}${instructionHtml}</div>`;
39225
40228
  return d;
39226
40229
  }
@@ -39788,10 +40791,14 @@ function renderChat(reason='snapshot'){
39788
40791
  _chatVirtSyncRunTicker(c);
39789
40792
  }
39790
40793
  function ab2b64(buf){let bin='';const bytes=new Uint8Array(buf);const chunk=0x8000;for(let i=0;i<bytes.length;i+=chunk){bin+=String.fromCharCode(...bytes.subarray(i,i+chunk))}return btoa(bin)}
40794
+ function uiYield(){return new Promise(resolve=>setTimeout(resolve,0))}
40795
+ function blobToBase64(blob){return new Promise((resolve,reject)=>{try{const reader=new FileReader();reader.onload=()=>{const raw=String(reader.result||'');const idx=raw.indexOf(',');resolve(idx>=0?raw.slice(idx+1):raw)};reader.onerror=()=>reject(reader.error||new Error('file read failed'));reader.readAsDataURL(blob)}catch(err){reject(err)}})}
40796
+ async function waitForPendingUploads(){const pending=S.uploadQueuePromise;if(pending&&typeof pending.then==='function')await pending}
39791
40797
  function clipboardFileExtFromType(mime){const low=String(mime||'').toLowerCase();const map={'image/png':'png','image/jpeg':'jpg','image/gif':'gif','image/webp':'webp','application/pdf':'pdf','application/vnd.openxmlformats-officedocument.wordprocessingml.document':'docx','application/msword':'doc','application/vnd.openxmlformats-officedocument.presentationml.presentation':'pptx','application/vnd.ms-powerpoint':'ppt','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':'xlsx','application/vnd.ms-excel':'xls','text/csv':'csv','text/plain':'txt','text/markdown':'md'};if(map[low])return map[low];if(low.includes('/'))return low.split('/').pop().replace(/[^a-z0-9]+/g,'')||'bin';return'bin'}
39792
40798
  function ensureNamedUploadFile(file,index=0,prefix='clipboard'){const src=file instanceof File?file:null;if(!src)return file;const name=String(src.name||'').trim();if(name)return src;const ext=clipboardFileExtFromType(src.type);const stamp=new Date().toISOString().replace(/[-:.TZ]/g,'').slice(0,14);const safe=`${prefix}_${stamp}_${index+1}.${ext}`;try{return new File([src],safe,{type:src.type||'',lastModified:Date.now()})}catch(_){return src}}
39793
40799
  function clipboardFilesFromEvent(ev){const dt=ev&&ev.clipboardData?ev.clipboardData:null;if(!dt)return[];const out=[];const seen=new Set();const pushFile=(raw,idx)=>{const file=ensureNamedUploadFile(raw,idx,'clipboard');if(!(file instanceof File))return;const sig=[String(file.name||''),String(file.type||''),String(file.size||0)].join('::');if(seen.has(sig))return;seen.add(sig);out.push(file)};const files=dt.files?Array.from(dt.files):[];files.forEach((file,idx)=>pushFile(file,idx));const items=dt.items?Array.from(dt.items):[];items.forEach((item,idx)=>{if(!item||item.kind!=='file')return;const file=typeof item.getAsFile==='function'?item.getAsFile():null;if(file)pushFile(file,idx+files.length)});return out}
39794
- async function uploadFiles(fileList){if(!S.activeId){showError(t('select_session_first'));return}if(!fileList||!fileList.length)return;if(S.staticMode&&S.frozen)resumeAutoUpdates();for(const file of Array.from(fileList)){const named=ensureNamedUploadFile(file,0,'upload');if(named.size>20*1024*1024){showError(`${t('file_too_large')}: ${named.name} (>20MB)`);continue}const arr=await named.arrayBuffer();const payload={filename:named.name,mime:named.type||'',content_b64:ab2b64(arr)};await api('/api/sessions/'+S.activeId+'/uploads',{method:'POST',body:JSON.stringify(payload)})}await refreshSnapshot({forceFull:true,allowWhenFrozen:true})}
40800
+ async function _uploadFilesNow(fileList){if(S.staticMode&&S.frozen)resumeAutoUpdates();let uploaded=0;const files=Array.from(fileList||[]).filter(Boolean);for(let i=0;i<files.length;i+=1){const named=ensureNamedUploadFile(files[i],i,'upload');if(named.size>20*1024*1024){showError(`${t('file_too_large')}: ${named.name} (>20MB)`);continue}const payload={filename:named.name,mime:named.type||'',content_b64:await blobToBase64(named)};await api('/api/sessions/'+S.activeId+'/uploads',{method:'POST',body:JSON.stringify(payload)});uploaded+=1;if(i<files.length-1)await uiYield()}if(uploaded>0)await refreshSnapshot({forceFull:true,allowWhenFrozen:true})}
40801
+ async function uploadFiles(fileList){if(!S.activeId){showError(t('select_session_first'));return}const files=Array.from(fileList||[]).filter(Boolean);if(!files.length)return;const run=async()=>{S.uploadInFlight=Math.max(0,Number(S.uploadInFlight||0))+files.length;try{return await _uploadFilesNow(files)}finally{S.uploadInFlight=Math.max(0,Number(S.uploadInFlight||0)-files.length)}};const prev=(S.uploadQueuePromise&&typeof S.uploadQueuePromise.then==='function')?S.uploadQueuePromise:Promise.resolve();const chained=prev.catch(()=>{}).then(run);const queued=chained.finally(()=>{if(S.uploadQueuePromise===queued)S.uploadQueuePromise=null});S.uploadQueuePromise=queued;return queued}
39795
40802
  function normalizeStatus(raw,fallback='pending'){const key=String(raw||'').trim().toLowerCase();const aliases={todo:'pending',doing:'in_progress',inprogress:'in_progress','in-progress':'in_progress',done:'completed',finish:'completed',finished:'completed'};const status=aliases[key]||key||fallback;if(['pending','in_progress','completed','blocked','deleted'].includes(status))return status;return fallback}
39796
40803
  function statusClass(status){return `st-${normalizeStatus(status)}`}
39797
40804
  function statusLabel(status){const s=normalizeStatus(status);if(s==='in_progress')return t('status_in_progress');if(s==='completed')return t('status_completed');if(s==='blocked')return t('status_blocked');if(s==='deleted')return t('status_deleted');return t('status_pending')}
@@ -39897,7 +40904,40 @@ function _normalizeModelCatalog(cat){const src=(cat&&typeof cat==='object')?cat:
39897
40904
  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}
39898
40905
  function applyModelCatalog(cat){const norm=_normalizeModelCatalog(cat);const hasCat=!!(norm.options.length||norm.models.length||norm.selected);if(!hasCat)return false;S.modelOptions=norm.options;S.models=norm.models;S.config=S.config||{};if(norm.selected){S.config.model=norm.selected}else if(!String(S.config.model||'').trim()){const first=(norm.options[0]?.selection||norm.models[0]||'').trim();if(first)S.config.model=first}if(norm.thinking!==null)S.config.thinking=!!norm.thinking;renderModelControls();return true}
39899
40906
  function renderModelControls(){const sel=E('modelSelect');if(!sel)return;sel.innerHTML='';const opts=S.modelOptions||[];if(opts.length){for(const it of opts){const op=document.createElement('option');op.value=it.selection;op.textContent=it.label||it.selection;sel.appendChild(op)}}else{const models=S.models||[];if(!models.length&&S.config?.model){const op=document.createElement('option');op.value=S.config.model;op.textContent=S.config.model;sel.appendChild(op)}for(const m of models){const op=document.createElement('option');op.value=m;op.textContent=m;sel.appendChild(op)}}if(S.config?.model){sel.value=S.config.model;if(sel.value!==S.config.model){const op=document.createElement('option');op.value=S.config.model;op.textContent=S.config.model;sel.appendChild(op);sel.value=S.config.model}}}
39900
- async function refreshSessions(){const rows=await api('/api/sessions');S.sessions=rows;const sig=sessionsSignature(rows);if(sig!==S.lastSessionsSig){S.lastSessionsSig=sig;renderSessions();renderStats()}if(!S.activeId&&rows.length)await selectSession(rows[0].id)}
40907
+ async function refreshSessions(opt={}){
40908
+ const useProvidedCfg=Object.prototype.hasOwnProperty.call(opt,'statsConfig');
40909
+ const useProvidedRows=Object.prototype.hasOwnProperty.call(opt,'sessions');
40910
+ const autoSelect=opt.autoSelect!==false;
40911
+ const cfgPromise=useProvidedCfg?Promise.resolve(opt.statsConfig):api('/api/config?stats=1').catch(()=>null);
40912
+ const rowsPromise=useProvidedRows?Promise.resolve(Array.isArray(opt.sessions)?opt.sessions:[]):api('/api/sessions');
40913
+ const [cfg,rowsRaw]=await Promise.all([cfgPromise,rowsPromise]);
40914
+ const rows=Array.isArray(rowsRaw)?rowsRaw:[];
40915
+ applyRuntimeConfigStats(cfg);
40916
+ S.sessions=rows;
40917
+ const sig=sessionsSignature(rows);
40918
+ if(sig!==S.lastSessionsSig){S.lastSessionsSig=sig;renderSessions()}
40919
+ renderStats();
40920
+ let selectedId='';
40921
+ if(!S.activeId&&rows.length&&autoSelect){
40922
+ selectedId=String(rows[0]?.id||'').trim();
40923
+ if(selectedId)await selectSession(selectedId);
40924
+ }
40925
+ return {rows,selectedId};
40926
+ }
40927
+ async function refreshDeferredCatalogs(){
40928
+ const settled=await Promise.allSettled([
40929
+ api('/api/skills'),
40930
+ api('/api/tools'),
40931
+ api('/api/skills/providers'),
40932
+ api('/api/skills/protocols'),
40933
+ ]);
40934
+ const [skillsRes,toolsRes,providersRes,protocolsRes]=settled;
40935
+ if(skillsRes.status==='fulfilled')S.skills=Array.isArray(skillsRes.value)?skillsRes.value:[];
40936
+ if(toolsRes.status==='fulfilled')S.tools=Array.isArray(toolsRes.value)?toolsRes.value:[];
40937
+ if(providersRes.status==='fulfilled')S.providers=Array.isArray(providersRes.value)?providersRes.value:[];
40938
+ if(protocolsRes.status==='fulfilled')S.protocols=Array.isArray(protocolsRes.value)?protocolsRes.value:[];
40939
+ renderSkillsEntryLink();
40940
+ }
39901
40941
  function _chatVirtIsUserScrolling(chatEl){
39902
40942
  if(!chatEl)return false;
39903
40943
  const now=Date.now();
@@ -40137,20 +41177,71 @@ function bindEvents(id){
40137
41177
  }
40138
41178
  async function loadModelCatalog(forceRefresh=false){const q=forceRefresh?'?refresh=1':'';if(S.activeId){return await api('/api/sessions/'+S.activeId+'/models'+q)}return await api('/api/models'+q)}
40139
41179
  async function selectSession(id){S.activeId=id;S.frozen=false;S.lastEventSeq=0;S.deltaGapCount=0;S.lastDeltaTs=Date.now();S.diffCenterDisabled=Object.create(null);S.previewCenterDisabled=Object.create(null);S.diffCenteredDone=Object.create(null);S.previewCenteredDone=Object.create(null);applyStaticUiClass();renderSessions();ensurePreviewState(id);bindEvents(id);_deltaStartWatchdog();pullRenderState(id,true);await refreshSnapshot({forceFull:true,allowWhenFrozen:true});renderPreviewTabs();renderPreviewVisibility();renderActivePreview(false);showError('')}
40140
- async function createSession(){showError('');const title=prompt(t('session_title_prompt'),t('web_session'));const out=await api('/api/sessions',{method:'POST',body:JSON.stringify({title:title||t('web_session')})});await refreshSessions();await selectSession(out.id)}
41180
+ async function createSession(opt={}){
41181
+ showError('');
41182
+ const usePrompt=opt.prompt!==false;
41183
+ const defaultTitle=t('web_session');
41184
+ let title=String(opt.title||'').trim()||defaultTitle;
41185
+ if(usePrompt){
41186
+ const input=prompt(t('session_title_prompt'),defaultTitle);
41187
+ if(input===null)return;
41188
+ title=String(input||'').trim()||defaultTitle;
41189
+ }
41190
+ try{
41191
+ const out=await api('/api/sessions',{method:'POST',body:JSON.stringify({title})});
41192
+ applyRuntimeConfigStats({session_creation_limit:out?.session_creation_limit});
41193
+ const sid=String(out?.id||'').trim();
41194
+ if(!sid){
41195
+ await refreshSessions();
41196
+ return;
41197
+ }
41198
+ const row={
41199
+ id:sid,
41200
+ title:String(out?.title||title||defaultTitle),
41201
+ running:false,
41202
+ updated_at:(Date.now()/1000),
41203
+ message_count:0,
41204
+ ui_language:String(out?.ui_language||S.config?.language||currentLang()),
41205
+ };
41206
+ S.sessions=[row,...(Array.isArray(S.sessions)?S.sessions:[]).filter(x=>String(x?.id||'')!==sid)];
41207
+ const sig=sessionsSignature(S.sessions);
41208
+ if(sig!==S.lastSessionsSig){S.lastSessionsSig=sig;renderSessions()}
41209
+ renderStats();
41210
+ await selectSession(sid);
41211
+ }catch(err){showError(err.message||String(err))}
41212
+ }
40141
41213
  async function renameSession(){if(!S.activeId){showError(t('select_session_first'));return}const old=S.sessions.find(x=>x.id===S.activeId)?.title||t('session_default');const s=prompt(t('rename_session_prompt'),old);if(!s)return;await api('/api/sessions/'+S.activeId,{method:'PATCH',body:JSON.stringify({title:s})});await refreshSessions();await refreshSnapshot({forceFull:true,allowWhenFrozen:true})}
40142
41214
  async function deleteSession(){if(!S.activeId){showError(t('select_session_first'));return}const deletingId=S.activeId;const ok=confirm(t('delete_confirm'));if(!ok)return;await api('/api/sessions/'+S.activeId,{method:'DELETE'});if(S.previewBySession&&deletingId){delete S.previewBySession[deletingId]}if(S.fileExplorerBySession&&deletingId){delete S.fileExplorerBySession[deletingId]}S.activeId=null;S.snap=null;if(S.es)S.es.close();renderPreviewTabs();renderPreviewVisibility();renderActivePreview(false);await refreshSessions();if(S.sessions.length)await selectSession(S.sessions[0].id)}
40143
41215
  async function applyModel(){const sel=E('modelSelect');const btn=E('applyModelBtn');const model=sel?.value||'';if(!model){showError(t('no_model_selected'));return}if(S.staticMode&&S.frozen)resumeAutoUpdates();S.config=S.config||{};const prevModel=String(S.config.model||'');const prevSnapModel=String(S.snap?.model||'');const prevSnapCatalog=(S.snap&&typeof S.snap==='object')?S.snap.llm_model_catalog:undefined;try{S.config.model=model;if(S.snap&&typeof S.snap==='object'){S.snap.model=_modelNameFromSelection(model)||S.snap.model;if(!S.snap.llm_model_catalog||typeof S.snap.llm_model_catalog!=='object')S.snap.llm_model_catalog={};S.snap.llm_model_catalog.selected=model}renderModelControls();renderStats();if(S.snap)renderBoards();if(sel)sel.disabled=true;if(btn)btn.disabled=true;const path=S.activeId?('/api/sessions/'+S.activeId+'/config/model'):'/api/config/model';const changed=await api(path,{method:'POST',body:JSON.stringify({selection:model,model})});if(changed?.note)showError(changed.note);else showError('');if(!applyModelCatalog(changed)){const cat=await loadModelCatalog();if(!applyModelCatalog(cat)){S.config.model=String(changed?.selected||model||'').trim();renderModelControls()}}if(S.snap&&typeof S.snap==='object'){const selected=String(S.config?.model||model||'').trim();const modelName=_modelNameFromSelection(selected);if(modelName)S.snap.model=modelName;if(changed&&typeof changed==='object')S.snap.llm_model_catalog=changed;renderBoards()}scheduleSnapshot({forceFull:true,delayMs:40,allowWhenFrozen:true})}catch(err){S.config.model=prevModel;if(S.snap&&typeof S.snap==='object'){if(prevSnapModel)S.snap.model=prevSnapModel;if(prevSnapCatalog!==undefined)S.snap.llm_model_catalog=prevSnapCatalog;renderBoards()}renderModelControls();renderStats();showError(err.message||String(err))}finally{if(sel)sel.disabled=false;if(btn)btn.disabled=false}}
40144
41216
 
40145
41217
  async function uploadLlmConfigFile(file){try{if(!S.activeId){showError(t('select_session_first'));return}if(!file){return}const arr=await file.arrayBuffer();const payload={filename:'LLM.config.json',mime:file.type||'application/json',content_b64:ab2b64(arr)};const out=await api('/api/sessions/'+S.activeId+'/uploads',{method:'POST',body:JSON.stringify(payload)});if(!out?.model_catalog){showError(t('config_uploaded_no_profiles'));}else{showError('');const modal=E('llmConfigModal');if(modal)modal.style.display='none'}const cat=out?.model_catalog||await loadModelCatalog();if(!applyModelCatalog(cat)){renderModelControls()}await refreshSnapshot({forceFull:true,allowWhenFrozen:true})}catch(err){showError(err.message||String(err))}}
40146
- async function sendMessage(){showError('');const t=E('prompt').value.trim();if(!t||!S.activeId)return;if(S.staticMode&&S.frozen)resumeAutoUpdates();E('prompt').value='';try{await api('/api/sessions/'+S.activeId+'/message',{method:'POST',body:JSON.stringify({content:t})});S.lastDeltaTs=Date.now();if(!S.es||S.es.readyState===2){scheduleSnapshot({forceFull:false,delayMs:120,allowWhenFrozen:true})}}catch(err){showError(err.message)}}
41218
+ async function sendMessage(){showError('');const t=E('prompt').value.trim();if(!t||!S.activeId)return;if(S.staticMode&&S.frozen)resumeAutoUpdates();E('prompt').value='';try{await waitForPendingUploads();await api('/api/sessions/'+S.activeId+'/message',{method:'POST',body:JSON.stringify({content:t})});S.lastDeltaTs=Date.now();if(!S.es||S.es.readyState===2){scheduleSnapshot({forceFull:false,delayMs:120,allowWhenFrozen:true})}}catch(err){showError(err.message)}}
40147
41219
  async function interruptRun(){if(!S.activeId)return;if(S.staticMode&&S.frozen)resumeAutoUpdates();await api('/api/sessions/'+S.activeId+'/interrupt',{method:'POST'});S.lastDeltaTs=Date.now();if(!S.es||S.es.readyState===2){scheduleSnapshot({forceFull:false,delayMs:140,allowWhenFrozen:true})}}
40148
41220
  async function compactNow(){if(!S.activeId)return;if(S.staticMode&&S.frozen)resumeAutoUpdates();await api('/api/sessions/'+S.activeId+'/compact',{method:'POST'});S.lastDeltaTs=Date.now();scheduleCompactRefreshBurst(COMPACT_AUTO_REFRESH_COUNT);if(!S.es||S.es.readyState===2){scheduleSnapshot({forceFull:false,delayMs:180,allowWhenFrozen:true})}}
40149
41221
  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})}}
40150
41222
  async function togglePlanMode(){if(!S.activeId)return;const states=['auto','on','off'];const current=S.snap?.plan_mode_preference||'auto';const next=states[(states.indexOf(current)+1)%states.length];try{await api('/api/sessions/'+S.activeId+'/config/plan-mode',{method:'POST',body:JSON.stringify({preference:next})});if(S.snap)S.snap.plan_mode_preference=next;const btn=E('planModeBtn');if(btn)btn.textContent='Plan: '+next.charAt(0).toUpperCase()+next.slice(1)}catch(err){showError(err.message||String(err))}}
40151
- async function refreshAll(forceProbe=false){if(S.staticMode&&S.frozen){S.frozen=false;applyStaticUiClass()}S.config=await api('/api/config');applyUiStyle();renderLanguageControls();applyMainI18n();renderUploadList();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})}
41223
+ async function refreshAll(forceProbe=false){
41224
+ if(S.staticMode&&S.frozen){S.frozen=false;applyStaticUiClass()}
41225
+ const [cfg,rowsRaw,mc]=await Promise.all([
41226
+ api('/api/config'),
41227
+ api('/api/sessions'),
41228
+ loadModelCatalog(forceProbe).catch(()=>null),
41229
+ ]);
41230
+ S.config=(cfg&&typeof cfg==='object')?cfg:{};
41231
+ applyRuntimeConfigStats(S.config);
41232
+ applyUiStyle();
41233
+ renderLanguageControls();
41234
+ applyMainI18n();
41235
+ renderUploadList();
41236
+ renderSkillsEntryLink();
41237
+ const rows=Array.isArray(rowsRaw)?rowsRaw:[];
41238
+ const sessState=await refreshSessions({statsConfig:S.config,sessions:rows,autoSelect:true});
41239
+ if(!applyModelCatalog(mc)){renderModelControls()}
41240
+ refreshDeferredCatalogs().catch(()=>{});
41241
+ if(S.activeId&&!String(sessState?.selectedId||'').trim())await refreshSnapshot({forceFull:true,allowWhenFrozen:true});
41242
+ }
40152
41243
  function bindClick(id,fn){const el=E(id);if(el)el.onclick=fn}
40153
- 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('promptComposerShell');const fileInput=E('uploadInput');const promptPick=E('promptFilePick');const promptEl=E('prompt');if(promptPick&&fileInput){promptPick.onclick=(ev)=>{ev.preventDefault();fileInput.click()}}if(drop&&fileInput){let _dragC=0;drop.setAttribute('tabindex','0');drop.addEventListener('click',e=>{if(e.target===drop&&promptEl)promptEl.focus()});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();if(evt==='dragenter')_dragC++;drop.classList.add('dragover')})}for(const evt of ['dragleave','dragend']){drop.addEventListener(evt,e=>{e.preventDefault();if(evt==='dragleave')_dragC--;if(_dragC<=0){_dragC=0;drop.classList.remove('dragover')}})}drop.addEventListener('drop',e=>{e.preventDefault();_dragC=0;drop.classList.remove('dragover');const files=e.dataTransfer?.files;if(files&&files.length)uploadFiles(files).catch(err=>showError(err.message))});drop.addEventListener('paste',e=>{const files=clipboardFilesFromEvent(e);if(!files.length)return;e.preventDefault();drop.classList.add('dragover');setTimeout(()=>drop.classList.remove('dragover'),220);uploadFiles(files).catch(err=>showError(err.message||String(err)))})}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('llmConfigBtn',openLlmConfigModal);bindClick('llmModalClose',()=>{E('llmConfigModal').style.display='none'});bindClick('llmConfigConfirm',submitLlmConfig);const llmProv=E('llmProvider');if(llmProv){llmProv.addEventListener('change',()=>renderLlmFields(llmProv.value))}const llmOverlay=E('llmConfigModal');if(llmOverlay){llmOverlay.addEventListener('click',e=>{if(e.target===llmOverlay)llmOverlay.style.display='none'})}bindClick('sendBtn',sendMessage);bindClick('interruptBtn',interruptRun);bindClick('clearStaleTodosBtn',clearStaleTodos);bindClick('planModeBtn',togglePlanMode);bindClick('refreshFilesBtn',()=>refreshFileExplorer(true));bindClick('previewReloadBtn',()=>renderActivePreview(true));bindClick('previewCopyBtn',()=>copyPreviewCode());const toolsMenuBtn=E('toolsMenuBtn');const toolsMenu=E('toolsMenu');if(toolsMenuBtn&&toolsMenu){toolsMenuBtn.addEventListener('click',e=>{e.stopPropagation();toolsMenu.style.display=toolsMenu.style.display==='none'?'block':'none'})}bindClick('compactAction',(e)=>{if(e)e.preventDefault();compactNow()});bindClick('refreshAction',(e)=>{if(e)e.preventDefault();refreshAll(true)});const levelMenuBtn=E('levelBtn');const levelMenu=E('levelMenu');if(levelMenuBtn&&levelMenu){levelMenuBtn.addEventListener('click',e=>{e.stopPropagation();levelMenu.style.display=levelMenu.style.display==='none'?'block':'none'});levelMenu.addEventListener('click',e=>{e.stopPropagation()});for(const opt of levelMenu.querySelectorAll('.level-option')){opt.addEventListener('click',e=>{e.preventDefault();const lvl=parseInt(opt.getAttribute('data-level')||'0',10);setTaskLevel(lvl);levelMenu.style.display='none'})}}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'});exportMenu.addEventListener('click',e=>{e.stopPropagation()});for(const a of exportMenu.querySelectorAll('.export-item')){a.addEventListener('click',()=>{exportMenu.style.display='none'})}}document.addEventListener('click',()=>{for(const menu of document.querySelectorAll('.popup-menu')){menu.style.display='none'}if(exportMenu)exportMenu.style.display='none'});const langSel=E('langSelect');if(langSel){langSel.onchange=()=>setLanguage(langSel.value).catch(err=>showError(err.message||String(err)))}if(promptEl){promptEl.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();sendMessage()}})}applyUiStyle();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})})})
41244
+ 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('promptComposerShell');const fileInput=E('uploadInput');const promptPick=E('promptFilePick');const promptEl=E('prompt');if(promptPick&&fileInput){promptPick.onclick=(ev)=>{ev.preventDefault();fileInput.click()}}if(drop&&fileInput){let _dragC=0;drop.setAttribute('tabindex','0');drop.addEventListener('click',e=>{if(e.target===drop&&promptEl)promptEl.focus()});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();if(evt==='dragenter')_dragC++;drop.classList.add('dragover')})}for(const evt of ['dragleave','dragend']){drop.addEventListener(evt,e=>{e.preventDefault();if(evt==='dragleave')_dragC--;if(_dragC<=0){_dragC=0;drop.classList.remove('dragover')}})}drop.addEventListener('drop',e=>{e.preventDefault();_dragC=0;drop.classList.remove('dragover');const files=e.dataTransfer?.files;if(files&&files.length)uploadFiles(files).catch(err=>showError(err.message))});drop.addEventListener('paste',e=>{const files=clipboardFilesFromEvent(e);if(!files.length)return;e.preventDefault();drop.classList.add('dragover');setTimeout(()=>drop.classList.remove('dragover'),220);uploadFiles(files).catch(err=>showError(err.message||String(err)))})}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('llmConfigBtn',openLlmConfigModal);bindClick('llmModalClose',()=>{E('llmConfigModal').style.display='none'});bindClick('llmConfigConfirm',submitLlmConfig);const llmProv=E('llmProvider');if(llmProv){llmProv.addEventListener('change',()=>renderLlmFields(llmProv.value))}const llmOverlay=E('llmConfigModal');if(llmOverlay){llmOverlay.addEventListener('click',e=>{if(e.target===llmOverlay)llmOverlay.style.display='none'})}bindClick('sendBtn',sendMessage);bindClick('interruptBtn',interruptRun);bindClick('clearStaleTodosBtn',clearStaleTodos);bindClick('planModeBtn',togglePlanMode);bindClick('refreshFilesBtn',()=>refreshFileExplorer(true));bindClick('previewReloadBtn',()=>renderActivePreview(true));bindClick('previewCopyBtn',()=>copyPreviewCode());const toolsMenuBtn=E('toolsMenuBtn');const toolsMenu=E('toolsMenu');if(toolsMenuBtn&&toolsMenu){toolsMenuBtn.addEventListener('click',e=>{e.stopPropagation();toolsMenu.style.display=toolsMenu.style.display==='none'?'block':'none'})}bindClick('compactAction',(e)=>{if(e)e.preventDefault();compactNow()});bindClick('refreshAction',(e)=>{if(e)e.preventDefault();refreshAll(true)});const levelMenuBtn=E('levelBtn');const levelMenu=E('levelMenu');if(levelMenuBtn&&levelMenu){levelMenuBtn.addEventListener('click',e=>{e.stopPropagation();levelMenu.style.display=levelMenu.style.display==='none'?'block':'none'});levelMenu.addEventListener('click',e=>{e.stopPropagation()});for(const opt of levelMenu.querySelectorAll('.level-option')){opt.addEventListener('click',e=>{e.preventDefault();const lvl=parseInt(opt.getAttribute('data-level')||'0',10);setTaskLevel(lvl);levelMenu.style.display='none'})}}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'});exportMenu.addEventListener('click',e=>{e.stopPropagation()});for(const a of exportMenu.querySelectorAll('.export-item')){a.addEventListener('click',()=>{exportMenu.style.display='none'})}}document.addEventListener('click',()=>{for(const menu of document.querySelectorAll('.popup-menu')){menu.style.display='none'}if(exportMenu)exportMenu.style.display='none'});const langSel=E('langSelect');if(langSel){langSel.onchange=()=>setLanguage(langSel.value).catch(err=>showError(err.message||String(err)))}if(promptEl){promptEl.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();sendMessage()}})}applyUiStyle();applyStaticUiClass();applyMainI18n();_bindPreviewCopyGuard();try{await refreshAll(false);if(!S.sessions.length){const bootCreate=()=>createSession({prompt:false}).catch(err=>showError(err.message||String(err)));if(typeof requestAnimationFrame==='function'){requestAnimationFrame(()=>setTimeout(bootCreate,0))}else{setTimeout(bootCreate,0)}}}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})})})
40154
41245
  """
40155
41246
 
40156
41247
  APP_TS = """type SessionSummary={id:string;title:string;running:boolean;updated_at:number;message_count:number};
@@ -47493,6 +48584,9 @@ class AppContext:
47493
48584
  max_output_tokens: int = AGENT_MAX_OUTPUT_TOKENS,
47494
48585
  max_user: int = 0,
47495
48586
  max_user_sessions: int = 0,
48587
+ daily_session_limit_per_ip: int = 0,
48588
+ daily_session_reset_hour: int = 8,
48589
+ js_lib_download_enabled: bool = True,
47496
48590
  rag_include_filename_entities: bool = RAG_INCLUDE_FILENAME_ENTITIES_DEFAULT,
47497
48591
  ):
47498
48592
  self.workspace = Path(workspace).resolve()
@@ -47556,6 +48650,9 @@ class AppContext:
47556
48650
  self._lock = threading.Lock()
47557
48651
  self.max_user = max(0, int(max_user or 0))
47558
48652
  self.max_user_sessions = max(0, int(max_user_sessions or 0))
48653
+ self.daily_session_limit_per_ip = max(0, int(daily_session_limit_per_ip or 0))
48654
+ self.daily_session_reset_hour = max(0, min(23, int(daily_session_reset_hour or 8)))
48655
+ self.js_lib_download_enabled = bool(js_lib_download_enabled)
47559
48656
  self._task_queue: deque[dict] = deque()
47560
48657
  self._task_queue_seq = 0
47561
48658
  self.tool_specs = TOOLS
@@ -47687,6 +48784,9 @@ class AppContext:
47687
48784
  "dir": str(self.web_ui_dir),
47688
48785
  "show_upload_list": bool(getattr(self, "show_upload_list", False)),
47689
48786
  "ui_style": normalize_ui_style(getattr(self, "ui_style", DEFAULT_UI_STYLE)),
48787
+ "js_lib_download_enabled": bool(getattr(self, "js_lib_download_enabled", True)),
48788
+ "daily_session_limit_per_ip": int(getattr(self, "daily_session_limit_per_ip", 0) or 0),
48789
+ "daily_session_reset_hour": int(getattr(self, "daily_session_reset_hour", 8) or 8),
47690
48790
  "validation": dict(self.web_ui_validation or {}),
47691
48791
  }
47692
48792
 
@@ -48617,6 +49717,91 @@ class AppContext:
48617
49717
  except Exception:
48618
49718
  pass
48619
49719
 
49720
+ def _daily_session_window_info(self, now_dt: datetime | None = None) -> dict:
49721
+ now_local = now_dt.astimezone() if isinstance(now_dt, datetime) else datetime.now().astimezone()
49722
+ reset_hour = int(getattr(self, "daily_session_reset_hour", 8) or 8)
49723
+ boundary = now_local.replace(hour=reset_hour, minute=0, second=0, microsecond=0)
49724
+ start_at = boundary if now_local >= boundary else (boundary - timedelta(days=1))
49725
+ reset_at = start_at + timedelta(days=1)
49726
+ return {
49727
+ "window_key": start_at.isoformat(),
49728
+ "window_start": start_at.isoformat(),
49729
+ "reset_at": reset_at.isoformat(),
49730
+ "reset_at_display": reset_at.strftime("%Y-%m-%d %H:%M:%S %z"),
49731
+ "now": now_local.isoformat(),
49732
+ }
49733
+
49734
+ def _session_daily_limit_state_path(self, user_id: str) -> Path:
49735
+ return self.user_root(user_id) / "session_daily_limit.json"
49736
+
49737
+ def _load_session_daily_limit_state_locked(self, user_id: str) -> dict:
49738
+ path = self._session_daily_limit_state_path(user_id)
49739
+ raw = self.crypto.read_json(path, {})
49740
+ return dict(raw) if isinstance(raw, dict) else {}
49741
+
49742
+ def _save_session_daily_limit_state_locked(self, user_id: str, state: dict) -> None:
49743
+ path = self._session_daily_limit_state_path(user_id)
49744
+ payload = dict(state or {})
49745
+ payload["updated_at"] = datetime.now().astimezone().isoformat()
49746
+ self.crypto.write_json(path, payload)
49747
+
49748
+ def _session_creation_quota_status_locked(self, user_id: str, client_ip: str = "") -> dict:
49749
+ limit = max(0, int(getattr(self, "daily_session_limit_per_ip", 0) or 0))
49750
+ window = self._daily_session_window_info()
49751
+ state = self._load_session_daily_limit_state_locked(user_id)
49752
+ if str(state.get("window_key", "") or "") != str(window.get("window_key", "")):
49753
+ state = {
49754
+ "window_key": str(window.get("window_key", "")),
49755
+ "used": 0,
49756
+ }
49757
+ used = max(0, int(state.get("used", 0) or 0))
49758
+ enabled = bool(limit > 0)
49759
+ remaining = max(0, limit - used) if enabled else None
49760
+ value = f"{used}/{limit}" if enabled else "∞"
49761
+ status = {
49762
+ "enabled": enabled,
49763
+ "limit": int(limit),
49764
+ "used": int(used),
49765
+ "remaining": remaining,
49766
+ "display_value": value,
49767
+ "window_key": str(window.get("window_key", "")),
49768
+ "window_start": str(window.get("window_start", "")),
49769
+ "reset_at": str(window.get("reset_at", "")),
49770
+ "reset_at_display": str(window.get("reset_at_display", "")),
49771
+ "reset_hour": int(getattr(self, "daily_session_reset_hour", 8) or 8),
49772
+ "client_ip": str(client_ip or ""),
49773
+ "user_id": str(user_id or ""),
49774
+ }
49775
+ if enabled and used >= limit:
49776
+ status["message"] = (
49777
+ f"daily session limit reached ({used}/{limit}); "
49778
+ f"resets at {status['reset_at_display']}"
49779
+ )
49780
+ return status
49781
+
49782
+ def session_creation_quota_status(self, user_id: str, client_ip: str = "") -> dict:
49783
+ with self._lock:
49784
+ return self._session_creation_quota_status_locked(user_id, client_ip=client_ip)
49785
+
49786
+ def create_session_for_user(self, user_id: str, title: str | None = None, client_ip: str = "") -> tuple[SessionState, dict]:
49787
+ mgr = self.manager_for_user(user_id)
49788
+ with self._lock:
49789
+ status_before = self._session_creation_quota_status_locked(user_id, client_ip=client_ip)
49790
+ if bool(status_before.get("enabled")) and int(status_before.get("remaining", 0) or 0) <= 0:
49791
+ raise SessionCreationLimitExceeded(status_before)
49792
+ sess = mgr.create(title)
49793
+ state = self._load_session_daily_limit_state_locked(user_id)
49794
+ if str(state.get("window_key", "") or "") != str(status_before.get("window_key", "")):
49795
+ state = {
49796
+ "window_key": str(status_before.get("window_key", "")),
49797
+ "used": 0,
49798
+ }
49799
+ state["used"] = max(0, int(state.get("used", 0) or 0)) + 1
49800
+ state["window_key"] = str(status_before.get("window_key", ""))
49801
+ self._save_session_daily_limit_state_locked(user_id, state)
49802
+ status_after = self._session_creation_quota_status_locked(user_id, client_ip=client_ip)
49803
+ return sess, status_after
49804
+
48620
49805
  def user_root(self, user_id: str) -> Path:
48621
49806
  root = self.codes_root / user_id
48622
49807
  root.mkdir(parents=True, exist_ok=True)
@@ -49879,6 +51064,7 @@ class Handler(BaseHTTPRequestHandler):
49879
51064
  refresh_probe = _to_bool_like((query.get("refresh", ["0"]) or ["0"])[0], default=False) or _to_bool_like(
49880
51065
  (query.get("probe", ["0"]) or ["0"])[0], default=False
49881
51066
  )
51067
+ stats_only = _to_bool_like((query.get("stats", ["0"]) or ["0"])[0], default=False)
49882
51068
  mgr = self._session_mgr()
49883
51069
  if path == "/":
49884
51070
  return self._send_text(self.app.web_ui_agent_index_html(), "text/html; charset=utf-8")
@@ -49894,10 +51080,25 @@ class Handler(BaseHTTPRequestHandler):
49894
51080
  reload_external = _to_bool_like((query.get("reload", ["0"]) or ["0"])[0], default=False)
49895
51081
  return self._send_json(self.app.refresh_web_ui_validation(reload_external=reload_external))
49896
51082
  if path == "/api/config":
49897
- model_cat = mgr.model_catalog()
49898
51083
  skills_port = int(getattr(self.app, "skills_port", 0) or 0)
49899
51084
  skills_enabled = bool(getattr(self.app, "skills_ui_enabled", False))
49900
51085
  scheduler_state = self.app.scheduler_status(self._user_id())
51086
+ session_creation_limit = self.app.session_creation_quota_status(self._user_id(), self._client_ip())
51087
+ if stats_only:
51088
+ return self._send_json(
51089
+ {
51090
+ "scheduler": scheduler_state,
51091
+ "max_user": int(scheduler_state.get("max_user", 0)),
51092
+ "max_user_sessions": int(scheduler_state.get("max_user_sessions", 0)),
51093
+ "daily_session_limit": int(getattr(self.app, "daily_session_limit_per_ip", 0) or 0),
51094
+ "daily_session_reset_hour": int(getattr(self.app, "daily_session_reset_hour", 8) or 8),
51095
+ "session_creation_limit": session_creation_limit,
51096
+ "download_js_lib_enabled": bool(getattr(self.app, "js_lib_download_enabled", True)),
51097
+ "request_timeout_default": int(DEFAULT_REQUEST_TIMEOUT),
51098
+ "run_timeout": int(mgr.max_run_seconds),
51099
+ }
51100
+ )
51101
+ model_cat = mgr.model_catalog()
49901
51102
  skills_url = ""
49902
51103
  if skills_enabled and skills_port > 0:
49903
51104
  host = self.headers.get("Host", "").strip()
@@ -49954,6 +51155,10 @@ class Handler(BaseHTTPRequestHandler):
49954
51155
  "scheduler": scheduler_state,
49955
51156
  "max_user": int(scheduler_state.get("max_user", 0)),
49956
51157
  "max_user_sessions": int(scheduler_state.get("max_user_sessions", 0)),
51158
+ "daily_session_limit": int(getattr(self.app, "daily_session_limit_per_ip", 0) or 0),
51159
+ "daily_session_reset_hour": int(getattr(self.app, "daily_session_reset_hour", 8) or 8),
51160
+ "session_creation_limit": session_creation_limit,
51161
+ "download_js_lib_enabled": bool(getattr(self.app, "js_lib_download_enabled", True)),
49957
51162
  }
49958
51163
  )
49959
51164
  if path == "/api/models":
@@ -50356,8 +51561,29 @@ class Handler(BaseHTTPRequestHandler):
50356
51561
  return self._send_json({"error": str(exc)}, status=400)
50357
51562
  if path == "/api/sessions":
50358
51563
  payload = self._read_json()
50359
- sess = mgr.create(payload.get("title"))
50360
- return self._send_json({"id": sess.id, "title": sess.title, "ui_language": sess.ui_language}, status=201)
51564
+ try:
51565
+ sess, quota_status = self.app.create_session_for_user(
51566
+ self._user_id(),
51567
+ payload.get("title"),
51568
+ client_ip=self._client_ip(),
51569
+ )
51570
+ except SessionCreationLimitExceeded as exc:
51571
+ return self._send_json(
51572
+ {
51573
+ "error": str(exc),
51574
+ "session_creation_limit": dict(getattr(exc, "status", {}) or {}),
51575
+ },
51576
+ status=429,
51577
+ )
51578
+ return self._send_json(
51579
+ {
51580
+ "id": sess.id,
51581
+ "title": sess.title,
51582
+ "ui_language": sess.ui_language,
51583
+ "session_creation_limit": quota_status,
51584
+ },
51585
+ status=201,
51586
+ )
50361
51587
  m = re.match(r"^/api/sessions/([^/]+)/uploads$", path)
50362
51588
  if m:
50363
51589
  sess = mgr.get(m.group(1))
@@ -51253,7 +52479,56 @@ def main():
51253
52479
  parser.add_argument(
51254
52480
  "--config",
51255
52481
  default="",
51256
- help="LLM config source (URL or local file path)",
52482
+ help=(
52483
+ "LLM config source (URL or local file path). "
52484
+ "Also reads startup keys like show_upload_list, download_js_lib and "
52485
+ "daily_session_limit (aliases: daily_sessions_per_ip / "
52486
+ "max_daily_sessions_per_ip / session_daily_limit)."
52487
+ ),
52488
+ )
52489
+ parser.add_argument(
52490
+ "--show_upload_list",
52491
+ "--show-upload-list",
52492
+ dest="show_upload_list",
52493
+ action="store_true",
52494
+ help="Show the upload list panel in WebUI (overrides config).",
52495
+ )
52496
+ parser.add_argument(
52497
+ "--no_show_upload_list",
52498
+ "--no-show-upload-list",
52499
+ dest="show_upload_list",
52500
+ action="store_false",
52501
+ help="Hide the upload list panel in WebUI (overrides config).",
52502
+ )
52503
+ parser.add_argument(
52504
+ "--download_js_lib",
52505
+ "--download-js-lib",
52506
+ dest="download_js_lib",
52507
+ action="store_true",
52508
+ help="Enable JS library download/bootstrap on startup (overrides config).",
52509
+ )
52510
+ parser.add_argument(
52511
+ "--no_download_js_lib",
52512
+ "--no-download-js-lib",
52513
+ dest="download_js_lib",
52514
+ action="store_false",
52515
+ help="Disable JS library download/bootstrap on startup (overrides config).",
52516
+ )
52517
+ parser.add_argument(
52518
+ "--daily_session_limit_per_ip",
52519
+ "--daily-session-limit-per-ip",
52520
+ "--daily_session_limit",
52521
+ "--daily-session-limit",
52522
+ "--daily_sessions_per_ip",
52523
+ "--daily-sessions-per-ip",
52524
+ "--max_daily_sessions_per_ip",
52525
+ "--max-daily-sessions-per-ip",
52526
+ "--session_daily_limit",
52527
+ "--session-daily-limit",
52528
+ dest="daily_session_limit_per_ip",
52529
+ default=None,
52530
+ type=int,
52531
+ help="Per-IP daily session creation limit; 0 means unlimited. Resets at 08:00 server local time.",
51257
52532
  )
51258
52533
  parser.add_argument("--ollama-base-url", default=DEFAULT_OLLAMA_BASE_URL)
51259
52534
  parser.add_argument("--model", default=DEFAULT_OLLAMA_MODEL)
@@ -51337,7 +52612,13 @@ def main():
51337
52612
  default="",
51338
52613
  help="Whether TF-Graph_IDF RAG treats file names as semantic entities (on|off). Default off.",
51339
52614
  )
51340
- parser.set_defaults(auto_model_switch=False, use_external_web_ui=None, arbiter_enabled=True)
52615
+ parser.set_defaults(
52616
+ auto_model_switch=False,
52617
+ use_external_web_ui=None,
52618
+ arbiter_enabled=True,
52619
+ show_upload_list=None,
52620
+ download_js_lib=None,
52621
+ )
51341
52622
  args = parser.parse_args()
51342
52623
  ctx_limit_locked = any(str(arg).split("=", 1)[0] == "--ctx_limit" for arg in sys.argv[1:])
51343
52624
  web_ui_config_path = resolve_optional_file_path(str(getattr(args, "web_ui_config", "") or ""), WORKDIR)
@@ -51365,6 +52646,7 @@ def main():
51365
52646
  )
51366
52647
  )
51367
52648
  resolved_show_upload_list = False
52649
+ resolved_daily_session_limit_per_ip = 0
51368
52650
  external_config: dict = {}
51369
52651
  external_config_source = ""
51370
52652
  bootstrap_base_url = args.ollama_base_url
@@ -51388,6 +52670,9 @@ def main():
51388
52670
  external_show_upload_list = extract_show_upload_list_setting(external_config)
51389
52671
  if external_show_upload_list is not None:
51390
52672
  resolved_show_upload_list = bool(external_show_upload_list)
52673
+ external_daily_session_limit = extract_daily_session_limit_setting(external_config)
52674
+ if external_daily_session_limit is not None:
52675
+ resolved_daily_session_limit_per_ip = int(external_daily_session_limit)
51391
52676
  print(f"[web-agent] external config loaded: {external_config_source}")
51392
52677
  except Exception as exc:
51393
52678
  print(f"[web-agent] invalid --config: {exc}")
@@ -51395,12 +52680,29 @@ def main():
51395
52680
  web_ui_show_upload_list = extract_show_upload_list_setting(web_ui_config)
51396
52681
  if web_ui_show_upload_list is not None:
51397
52682
  resolved_show_upload_list = bool(web_ui_show_upload_list)
52683
+ cli_show_upload_list = getattr(args, "show_upload_list", None)
52684
+ if cli_show_upload_list is not None:
52685
+ resolved_show_upload_list = bool(cli_show_upload_list)
52686
+ web_ui_daily_session_limit = extract_daily_session_limit_setting(web_ui_config)
52687
+ if web_ui_daily_session_limit is not None:
52688
+ resolved_daily_session_limit_per_ip = int(web_ui_daily_session_limit)
52689
+ cli_daily_session_limit = getattr(args, "daily_session_limit_per_ip", None)
52690
+ if cli_daily_session_limit is not None:
52691
+ resolved_daily_session_limit_per_ip = max(0, int(cli_daily_session_limit or 0))
51398
52692
  raw_ui_style = str(getattr(args, "ui_style", "") or "").strip()
51399
52693
  if not raw_ui_style:
51400
52694
  raw_ui_style = str(extract_ui_style_setting(external_config) or "").strip()
51401
52695
  if not raw_ui_style:
51402
52696
  raw_ui_style = str(extract_ui_style_setting(web_ui_config) or "").strip()
51403
52697
  resolved_ui_style = normalize_ui_style(raw_ui_style or DEFAULT_UI_STYLE)
52698
+ _js_dl_enabled = extract_js_lib_download_setting(external_config)
52699
+ if _js_dl_enabled is None:
52700
+ _js_dl_enabled = extract_js_lib_download_setting(web_ui_config)
52701
+ if _js_dl_enabled is None:
52702
+ _js_dl_enabled = True
52703
+ cli_js_dl_enabled = getattr(args, "download_js_lib", None)
52704
+ if cli_js_dl_enabled is not None:
52705
+ _js_dl_enabled = bool(cli_js_dl_enabled)
51404
52706
  startup_tags = list_ollama_models(bootstrap_base_url)
51405
52707
  if startup_tags:
51406
52708
  resolved_model = bootstrap_model if bootstrap_model in startup_tags else startup_tags[0]
@@ -51633,6 +52935,9 @@ def main():
51633
52935
  resolved_max_output_tokens,
51634
52936
  resolved_max_user,
51635
52937
  resolved_max_user_sessions,
52938
+ resolved_daily_session_limit_per_ip,
52939
+ 8,
52940
+ bool(_js_dl_enabled),
51636
52941
  resolved_rag_include_filename_entities,
51637
52942
  )
51638
52943
  config_apply_result: dict = {}
@@ -51644,9 +52949,13 @@ def main():
51644
52949
  print(f"[web-agent] failed to apply --config: {exc}")
51645
52950
  sys.exit(2)
51646
52951
  # JS lib download (default on; set download_js_lib: false in --config to disable)
51647
- _js_dl_enabled = extract_js_lib_download_setting(external_config)
51648
- if _js_dl_enabled is None:
51649
- _js_dl_enabled = True
52952
+ app.js_lib_download_enabled = bool(_js_dl_enabled)
52953
+ print(
52954
+ "[web-agent] session_creation_limit_per_ip="
52955
+ + ("∞" if resolved_daily_session_limit_per_ip <= 0 else str(int(resolved_daily_session_limit_per_ip)))
52956
+ + " reset=08:00 local"
52957
+ )
52958
+ print(f"[web-agent] download_js_lib={'on' if _js_dl_enabled else 'off'}")
51650
52959
  if _js_dl_enabled:
51651
52960
  try:
51652
52961
  app.offline_js_summary = ensure_offline_js_libs(