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.
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/Clouds_Coder.py +1406 -97
- {clouds_coder-2026.3.31/clouds_coder.egg-info → clouds_coder-2026.4.2}/PKG-INFO +1 -1
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2/clouds_coder.egg-info}/PKG-INFO +1 -1
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/pyproject.toml +1 -1
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/LICENSE +0 -0
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/README.md +0 -0
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/setup.cfg +0 -0
- {clouds_coder-2026.3.31 → clouds_coder-2026.4.2}/tests/test_smoke.py +0 -0
|
@@ -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 =
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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"- {
|
|
19010
|
-
f"({
|
|
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
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
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
|
-
|
|
19029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19130
|
-
|
|
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
|
-
|
|
19265
|
-
|
|
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 =
|
|
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 ""),
|
|
23518
|
-
raw_full = trim(str(pt.get("full_content", "") or ""),
|
|
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,
|
|
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=
|
|
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
|
|
25640
|
+
if phase in ("research", "design") and validation_ok:
|
|
24729
25641
|
phase_evidence = True
|
|
24730
|
-
elif phase == "implement" and wrote_files and
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
|
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(),
|
|
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(),
|
|
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
|
|
26300
|
+
if subtasks_done and validation_ok:
|
|
25383
26301
|
should_advance = True
|
|
25384
|
-
# Priority 2: Phase-based heuristics (
|
|
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
|
|
26304
|
+
if phase in ("research", "design") and validation_ok:
|
|
25387
26305
|
should_advance = True
|
|
25388
|
-
elif phase == "implement" and wrote_files and
|
|
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
|
|
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 =
|
|
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)
|
|
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
|
|
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)
|
|
29674
|
-
"E)
|
|
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 >=
|
|
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(
|
|
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(),
|
|
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(),
|
|
33190
|
-
summary = trim(str(plan.get("summary", "") or "").strip(),
|
|
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(),
|
|
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(),
|
|
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
|
|
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,
|
|
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,
|
|
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(),
|
|
33728
|
-
chosen_summary = trim(str(chosen.get("summary", "") or "").strip(),
|
|
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:
|
|
36786
|
-
.
|
|
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:
|
|
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
|
|
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&"a.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
|
|
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
|
|
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(
|
|
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(
|
|
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){
|
|
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)
|
|
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
|
-
|
|
50360
|
-
|
|
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=
|
|
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(
|
|
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
|
-
|
|
51648
|
-
|
|
51649
|
-
|
|
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(
|