Main (#6072, #5848) already runs timed async-path sync tools on the bounded
per-agent pool with overflow spill, and returns structured MCP transport
timeouts with cancellation. Drop the now-redundant transport timeout hunks
and update tests to main's fail-fast/structured contracts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HookRunner guarded its stuck-hook slot with one asyncio.Lock, but
execute_sync() drives each call on its own event loop per thread, so
parallel tool calls with a sync BEFORE_TOOL hook could hang until the
bridge timeout. Any still-running invocation also filled the slot, so
concurrent healthy calls were denied as 'still running past its timeout'.
Use a threading.Lock, run the hook via a concurrent Future, and mark
the slot only once a worker actually times out. The #5795 tests now use
their own HookRegistry (with an allow-all sync hook) so hooks leaked
into the global registry by other tests can't block them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- execution_mixin: sync tools on the async path bound queue wait and
execution separately (parity with tool_execution.py), so calls queued
for the 5-worker pool are no longer timed out while healthy.
- Middleware worker bound covers both phases.
- Tests: async parallel queue-wait regression and a fake-session test of
the stdio runner's concurrent dispatch and shutdown cancellation.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Sync tool path: charge tool_timeout from when the body starts, not from
submit; size the per-agent pool to the parallel executor width (5).
- Async tool path: run sync tools on a dedicated bounded per-agent pool
instead of the loop's shared default executor, so hung tools cannot
starve other to_thread users or wedge later calls.
- MCP stdio runner: run each request as its own task with a blocking
queue read instead of a serial 10ms poll loop.
- MCP HTTP-stream/SSE/WebSocket: cancel the in-flight call on timeout
and return a descriptive error instead of a blank 'Error: '.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Spill threads started when every pool worker is busy were uncounted, so
hung calls leaked threads in proportion to concurrency. Count them per
agent, cap at _MAX_SPILL_TOOL_THREADS (8) and refuse fast with
tool_pool_exhausted once reached; decrement when each thread exits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Explicit URI options (e.g. ?retryWrites=false, maxPoolSize) were overridden
by the adapter's hard-coded MongoClient kwargs, which now also affected
MongoDBStateStore. Drop any default the URI already sets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Obtain the pymongo client from MongoDBStorageAdapter (same template as
RedisStateStore) instead of a second independent MongoClient init.
Document layout and TTL index are unchanged.
- Fix get()/exists() raising TypeError when a key has a TTL (datetime
compared with float) and ttl() mis-reading naive UTC datetimes as local.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Await the backend generator's aclose() and shutdown_asyncgens() on the
private loop so backend cleanup runs when the consumer closes the stream.
- When every pool worker is busy, run a timed sync tool on its own daemon
thread instead of queueing it (wait time counted against the timeout).
Overflow is still refused once the orphan cap is hit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Streaming: one bounded/cancellable daemon-thread bridge for both the
running-loop and no-loop cases; closing the generator stops the producer
and a backend RuntimeError can no longer re-run the stream.
- Async tools: with a tool timeout, sync tools run on the bounded per-agent
tool pool (shared _submit_to_tool_pool/_retire_tool_executor helpers) so
timed-out tools no longer strand threads in the default executor.
- Circuit breaker: input errors (LookupError, FileNotFoundError,
PermissionError, ...) are neutral and non-retryable; sync and async paths
share one breaker config; remediation text reads the live
recovery_timeout; drop the unused timeout=30.0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: make completion promise the only autonomy success signal (fixes#6027)
When a completion_promise is configured, the tool_completion, keyword,
direct-stage and no_tool_calls heuristics no longer end the run with
success=True in run_autonomous/run_autonomous_async. Promise-less runs are
unchanged. The async tool path now increments _autonomy_turn_tool_count for
sync/async parity, and `praisonai loop` sets mode="iterative" so
--completion-promise/--max-iterations take effect without --level full_auto.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: tell the model the completion promise and only count executed async tools
Addresses review on #6064: SDK autonomy loops now append the expected
<promise> tag to the prompt (unless already present in the prompt or
instructions), and the async per-turn tool count is incremented only after
the tool runs so BEFORE_TOOL-blocked calls cannot trigger tool_completion.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix: require approval for GitHub write tools and scope git to repo_path (fixes#6023)
- Register github_create_branch/commit_and_push/create_pull_request as
high-risk and add them to the default deny preset, so unattended agents
need an explicit allow (same as execute_command).
- Add optional repo_path (cwd for every git/gh call) and paths args.
- Stage with 'git add -u' (+ explicit paths) after the branch-safety
checks, so untracked files are not swept in and a refusal leaves the
index untouched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: scope github_commit_and_push staging to the working directory
git add -u with no pathspec stages tracked edits across the whole
repository, so a call from a subdirectory could publish unrelated sibling
changes. Use 'git add -u -- .' and scope the no-changes status check the
same way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix: run kanban dispatcher git work off the event loop (fixes#6039)
- dispatch_once / _spawn_worker / _shutdown hand _cleanup_completed_tasks
and _prepare_worktree to a worker thread, serialised by a class-level
lock so integrations never run git concurrently in the base checkout.
- Workers start in their own session; shutdown SIGTERMs every worker
group first, waits for all of them off the loop, then SIGKILLs groups
that are still alive.
- Sync helpers are unchanged, so the CLI one-shot dispatch path keeps
working.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: clean up cancelled worktree prep and always SIGKILL worker groups on shutdown
- Cancelling dispatch during worktree preparation now waits for the git
thread, discards the unused worktree/branch, closes the run and releases
the claim before re-raising.
- Shutdown SIGKILLs every worker's process group even when the worker
itself already exited, so SIGTERM-ignoring descendants cannot survive.
- Tests: new cancellation test; descendant test uses a marker file and a
SIGTERM-ignoring child instead of os.kill(pid, 0), which zombies pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix: make loop-guard turn time limit configurable via ExecutionConfig.max_execution_time (fixes#6026)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: clarify that only ExecutionConfig.max_execution_time=0 disables the turn time limit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix: walk fallback_models on the custom-LLM chat/achat path (fixes#6030)
provider/model, dict, base_url and model-object agents call
LLM.get_response* directly and turned any primary-model outage into a
None turn without trying a single fallback model. Wrap that call so a
fallback-class error (per classify_llm_error) moves to the next entry in
fallback_models via a throwaway per-call LLM, emitting MODEL_FALLBACK.
self.llm_instance is never mutated, so concurrent turns keep the primary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: keep base_url for bare-name fallbacks and skip fallback after a tool ran
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix: kanban dispatcher heartbeats its live workers instead of re-running them (fixes#6025)
The dispatcher stamped a worker's heartbeat only at spawn, so any task
running longer than claim TTL + stale timeout (~30 min by default) was
reclaimed while healthy, re-dispatched, and the first worker orphaned.
- Renew lease/heartbeat for every live child before the reclaim sweep;
terminate a child whose claim was lost.
- Never overwrite a running worker's handle in running_tasks.
- Close a reclaimed task's open run as 'crashed' and count the failure.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: close reclaimed kanban run atomically and keep lost-claim worktree tracking
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: log run_closed when reclaim closes a run; scope headless output_file test to its project root
The reclaim sweep now records the usual run_closed event in the same
transaction that closes the abandoned run. test_output_file_and_metrics_json
writes under tmp_path, which the #5770 project-root guard rejects; point
PRAISONAI_PROJECT_ROOT at tmp_path so the test checks the written files
without loosening the guard. This test already fails the same way on main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix: keep gateway scheduler tick alive on errors (fixes#6035)
A tick that raises (store lock I/O such as EMFILE, a malformed scheduler:
block) is now logged and retried with capped backoff instead of ending
_scheduler_task for good. _build_run_policy tolerates a non-mapping
scheduler: block by warning and using the fail-closed defaults.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: cap scheduler backoff exponent and scope headless output_file test to its project root
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: whitespace in headless output_file test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
#5770 contains task output_file writes to the project root (cwd or
PRAISONAI_PROJECT_ROOT), matching Agent.output_file. These tests write under
tmp_path, which is outside the cwd, so main's test-core (root) failed with
'output_file not written'. Point the project root at tmp_path.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
aiui 0.3.148 ships create_replay_app (PraisonAIUI#317), which
praisonai.replay.watch() and replay_app() (#6017) need.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* fix(ts): send maxOutputTokens to the AI SDK so the output-token cap applies
AI SDK 5 renamed generateText/streamText's `maxTokens` to `maxOutputTokens`
and ignores the old key. The package pins ai@^6, so AISDKBackend (and
src/ai/generate-text.ts) passed a key the SDK drops: neither
SAFE_DEFAULTS.maxOutputTokens (4096) nor a caller's maxTokens reached the
model on any AI SDK-backed provider (Anthropic, Google, ...).
Send maxOutputTokens, and keep maxTokens for the ai@4 peer range.
Found with SoreqAI (soreqai.com).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* test(ts): cover maxOutputTokens on all five changed AI SDK calls
Per review: generateText alone left four calls untested. Adds
AISDKBackend.streamText (stream drained so the SDK call runs) and
generateObject, plus the generateText/streamText helpers in
src/ai/generate-text.ts, each checking the default cap and a caller override.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(ts): send maxOutputTokens from the generateObject/streamObject helpers too
Per CodeRabbit: src/ai/generate-object.ts passed only the old maxTokens key
to sdk.generateObject and sdk.streamObject, the same dropped-cap bug. Adds
maxOutputTokens there and a test for each helper.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Shaked Yacoby <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix(context-agent): write saved artifacts as UTF-8
ContextAgent saved its PRP, analysis summary, and results/interaction
files with the platform default encoding, while other writers in the same
file already pass encoding="utf-8". Writing a PRP or summary that contains
non-ASCII (any non-English content) raised UnicodeEncodeError on Windows,
and the JSON results escaped non-ASCII to \uXXXX. Open every ContextAgent
output file as UTF-8 and dump JSON with ensure_ascii=False so the saved
artifacts are readable and portable, matching the rest of the module.
* test(context-agent): keep the regression test's files inside tmp_path
* test(context-agent): cover all result JSON writers for UTF-8
Populate non-ASCII in every result field and assert each saved file
(comprehensive_analysis, pattern_library, validation_framework,
all_agent_interactions) round-trips as readable UTF-8, not just
comprehensive_analysis.json.
* fix(memory): use timezone-aware UTC timestamps in the learn store
The learn store built timestamps with the deprecated datetime.utcnow(),
which returns a naive datetime. Retention pruning subtracted a naive now
from timestamps parsed back from storage, and datetime.utcnow().timestamp()
computed the wrong epoch by treating the naive UTC value as local time.
Use datetime.now(timezone.utc) everywhere, and when parsing a stored
timestamp that predates this change (naive, no offset), treat it as UTC so
pruning keeps working on existing stores instead of raising "can't subtract
offset-naive and offset-aware datetimes".
* test(memory): add docstrings to learn-store timestamp tests
* fix(memory): normalize timestamps before sorting in the learn store
list_all() and the excess-entry pruning sorted raw ISO strings, so a
stored timestamp with a non-UTC offset could order against a UTC value in
the wrong direction. Add a shared sort key that parses each timestamp,
treats naive values as UTC, and buckets unparseable ones last. Covers the
updated_at ordering and the last_used/created_at pruning tie-breakers.
Also add an end-to-end prune() test and a list ordering test for mixed
offsets.
* fix(memory): sort missing/malformed timestamps as oldest in the learn store
Bucketing unparseable values last flipped prune()'s eviction order: an
entry never used (last_used=None, which parses as empty) sorted after used
ones, so it was evicted last instead of first. Bucket missing/malformed
values as the oldest instant instead. prune() (ascending) then evicts them
first, matching the previous raw-string behaviour, and list_all()
(descending) still lists them last. Add a regression test with two
use_count=0 entries where only one was ever used.
* fix: window doom-loop per-tool count and route download_file through shared SSRF guard (fixes#5957)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: keep download_file SSRF guard independent of ALLOW_LOCAL_CRAWL (#5957)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: download_file writes to temp file so a rejected download keeps the existing destination (#5957)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: MervinPraison <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix(security): contain Task and Workflow step output_file writes in project root
AgentTeam.save_output_to_file and both workflow step writers opened
output_file without the resolve_within_root check Agent.output_file
already applies, so a relative or {{input}}-templated path could write
anywhere on disk. Route all three through a shared write_output_file
helper that rejects paths outside the project root (PRAISONAI_PROJECT_ROOT
or cwd), honours create_directory and writes UTF-8.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Stanley Shen <[email protected]>
* fix: preserve output_file permissions on overwrite
atomic_write_text replaces the destination with a mkstemp file (0600),
which silently made shared reports owner-only. Use plain open() like
Agent.output_file so existing modes and umask are kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix: replace existing output_file instead of truncating it
Truncating in place writes through a hardlink to a file outside the
project root. Create new files exclusively so umask applies, and replace
existing ones atomically, restoring their previous mode.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Stanley Shen <[email protected]>
* fix: overwrite output_file in place unless it is hardlinked
Replacing the file through a temp file needed write access to the parent
directory and changed the mode through the destination path afterwards.
Truncate the existing file in place when it has a single link, and only
unlink and recreate it (with the old mode) when it is hardlinked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Stanley Shen <[email protected]>
* fix: swap in hardlinked output_file replacement after writing it
Unlinking first lost the old report if creating or writing the new one
failed, and the replacement's mode was masked by umask. Write a temp file
in the same directory, set the old mode on its descriptor, then
os.replace it over the hardlinked name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Stanley Shen <[email protected]>
* fix: keep group on hardlinked output_file replacement or drop group bits
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Signed-off-by: Stanley Shen <[email protected]>
Co-authored-by: Claude Sonnet 5.5 <[email protected]>
Co-authored-by: MervinPraison <[email protected]>
Before, one variable that could not be deep-copied (a lock or a client, for
example) switched the whole snapshot to identity comparison, so a list another
variable's step changed in place was no longer recorded on step_end. Use the
existing _isolate_scope, which shares only the uncopyable values.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- AgentTeam: a repeated start()/astart() resets per-task run state and the
content the previous run injected, instead of skipping completed tasks and
returning stale output. restore_session_state() still resumes.
- RAG.retrieve: resolve top_k/rerank per call instead of writing them to the
shared RAGConfig (aretrieve runs concurrently in an executor).
- SmartRetriever: over-fetch before post-filtering, raise ValueError on an
invalid path_filter regex, return store errors from _search instead of
storing them on self.
- SubQuestionEngine: unwrap RetrievalResult.chunks from SmartRetriever.
Co-Authored-By: Claude Opus 5.5 <[email protected]>