- simple-mcp-server.py: replace blind 2s sleep with TCP readiness poll so
the smoke test genuinely verifies the server bound and exits non-zero on
failure (addresses Greptile P1: daemon exited claiming false success)
- execution_mixin.py: remove now-unused SseServerTransport import/instance
after switching to FastMCP.sse_app() (dead code flagged by Greptile)
Co-authored-by: Mervin Praison <[email protected]>
Addresses two Greptile P1 findings on the allow_shell opt-in:
1. Routed agents lose shell tools: _inject_routing_handler swapped in a raw
agent from self._agents, stripping execute_command applied to the default
channel clone. Re-apply enable_shell_tools to the routed agent (cloned once
per channel+agent so non-shell channels never gain the shell tool).
2. Manual approval retained auto-approval (security): with auto_approve_shell
false and no Slack approval channel, a pre-existing AutoApproveBackend was
left installed, silently auto-approving shell. Now fail-closed with a
deny-by-default backend that rejects shell commands.
Adds tests for both fail-closed approval paths.
Co-authored-by: Mervin Praison <[email protected]>
- Fix missing approval_webhook_url becoming the literal string "None"
which bypassed WebhookApproval's ValueError guard; now falls through
to platform/gateway approval instead of building a broken backend
- Add fail-closed approval_mode validator (channel|gateway|http|webhook)
so config typos are rejected at load time, matching other enum fields
- Add regression tests for webhook-mode with and without a URL
Co-authored-by: Mervin Praison <[email protected]>
Extend allow_shell to route human-in-the-loop approvals through platform
backends (Slack/Telegram/Discord) plus gateway, HTTP, and webhook modes,
with gateway fallback for channels without native wiring.
Address reviewer feedback on the CLI_BACKEND_EXECUTE hook path:
- Security: redact prompt/system argv values (redact_command) at the
payload serialization boundary and in the plugin bridge, so prompts and
system instructions do not leak into downstream log sinks. The live
in-memory command field is preserved for programmatic consumers.
- Observability: emit the hook on failure too (extracted
_emit_cli_backend_hook helper) so subprocess startup errors and timeouts
remain traceable via error=<message>, instead of only on success.
- Tests: cover redaction (serialization + plugin bridge) and failure-path
hook emission.
Co-authored-by: Mervin Praison <[email protected]>
Log subprocess command and confirm PraisonAI skips LLM HTTP when cli_backend delegates a turn.
Enable with PRAISONAI_CLI_BACKEND_DEBUG=1 or LOGLEVEL=DEBUG.
Gemini backend forwarded --resume on every turn including the first,
which would fail resuming a non-existent session. Gate on
CliSessionBinding.is_resume for parity with codex/grok/claude.
Co-authored-by: Mervin Praison <[email protected]>
Codex backend never forwarded the session id to `codex exec`, so
multi-turn conversations lost context (Greptile P1). Now forwards
`resume <session_id>` on resume turns. Gate both codex and grok
session forwarding on CliSessionBinding.is_resume so the first turn
does not attempt to resume a non-existent session.
Co-authored-by: Mervin Praison <[email protected]>
- Isolate ANTHROPIC_TOKEN/CLAUDE_CODE_OAUTH_TOKEN env in the expiring-token
test so the Keychain read-only path is deterministically exercised (was
leaking a real CI OAuth token and returning the wrong access token).
- Assert both anthropic-beta policy changes: fine-grained-tool-streaming is
present and the unsupported context-1m long-context beta stays absent
(per CodeRabbit review).
Co-authored-by: Mervin Praison <[email protected]>
PraisonAI read Keychain OAuth tokens but refreshed without persisting back,
rotating refresh tokens and breaking Claude CLI and other tools on the same
machine. Use read-only credential resolution, skip LLM auth refresh for
claude-code, fix invalid_request_error misclassification, and drop the
context-1m beta header unsupported on many subscriptions.
The async retry loop emitted RETRY events via the sync emit() path, so
consumers registered through the public add_async_callback() API passed
has_callbacks but never received the event. Add an async-aware
_aemit_retry_stream_event helper (using emit_async) and share event
construction via _build_retry_stream_event. Adds tests for the async-only
callback path and the no-callback no-op case.
Co-authored-by: Mervin Praison <[email protected]>
Add StreamEventType.RETRY emitted alongside the existing ON_RETRY hook (guarded by has_callbacks for zero-overhead) in both sync and async retry loops, carrying attempt/max_attempts/delay/reason in metadata. Map it to a canonical run.retry NDJSON event in the CLI stream-json bridge so consumers get a live 'retrying in Ns (attempt k/N)' signal. Non-JSON/silent modes stay silent.
Co-authored-by: Mervin Praison <[email protected]>
A TypeError/ValueError raised inside a successfully-bound tool (domain
validation) no longer receives a misleading required/optional parameter
hint. The hint is now gated on inspect.signature().bind() confirming the
failure is an argument-binding error, so runtime errors surface their
real message and the model fixes the value, not the parameter names.
Co-authored-by: Mervin Praison <[email protected]>
Adds a zero-overhead resilience layer to the core agent tool-dispatch path.
When a tool call cannot be resolved, the runtime now:
1. Self-repairs case/separator drift (e.g. WebSearch/web-search -> web_search)
against the agent's active tools and executes if a unique match exists.
2. Otherwise returns a model-readable error naming the nearest valid tool
(fuzzy) and listing the available tools so the model can self-correct.
3. On an argument-binding failure, echoes the tool's required/optional params.
No new public API or Agent params; stays on the existing dispatch path.
Co-authored-by: MervinPraison <[email protected]>
- Security: refuse non-localhost bind without --api-key/PRAISONAI_SERVE_API_KEY,
mirroring jobs/server.py; enforce the resolved key unconditionally.
- Performance: cache one AgentsGenerator per app via FastAPI lifespan (built at
startup, closed on shutdown) instead of rebuilding YAML parse + framework
resolution + a 32-worker pool per request; falls back to praisonai.arun if
caching fails.
- Correctness: /agents and /agents/{name} now run the same cached generator, so
identical YAML gets identical lowering (ToolResolver, tool_timeout, approval,
guardrails, retry). Removed the hand-rolled PraisonAgent registration that
silently dropped those safety fields.
Co-authored-by: Mervin Praison <[email protected]>
Remove 13 knowledge.db binaries plus managed_ids.json, blog_post.md,
tutorial.md, token_metrics_export.json, market_research_results.json that
were accidentally committed (PR notes explicitly say not to commit local
artifacts). Add .gitignore rules to prevent recurrence.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #3321:
- Core neutralize_untrusted_text now also collapses U+2028/U+2029/U+0085,
which render as line breaks but bypassed the ASCII control filter.
- Bot _attribute() fallback (core helper unavailable / version skew) now
mirrors the sanitiser: collapses all newline-like separators, strips
control chars, and bounds length.
- Added tests for Unicode separators (core + bot) and the fallback path.
Co-authored-by: Mervin Praison <[email protected]>
Add a core, pure-stdlib helper neutralize_untrusted_text() in
praisonaiagents.session.context and apply it by default to the
third-party-controlled sender display name before it is interpolated into
the [{sender}] attribution prefix. Newlines/CR are collapsed, control chars
stripped and length is bounded, so a hostile name can no longer masquerade
as a fake system directive the model re-reads every turn. Well-behaved names
render byte-identically. On by default, no new config knobs.
Co-authored-by: Mervin Praison <[email protected]>
The durable-reference encoding for long interactive reply/select values only
took effect when a shared store was explicitly passed in tests; every
production renderer/registry used the no-store default and fell back to the
unrecoverable hash. This shares one InMemoryCallbackPayloadStore between the
Telegram render side (render_presentation -> adapt_presentation) and the inbound
registry so long values round-trip losslessly past the 64-byte callback cap.
- create_registry(store=): thread an optional store into the InteractiveRegistry
- TelegramPresentationRenderer.render(callback_store=): pass store to adapt
- TelegramBot: construct one shared store, inject into registry + render
- add regression test asserting production render+dispatch round-trip
Co-authored-by: Mervin Praison <[email protected]>
- POST /agents/{agent_name} now dispatches to the requested registered agent
and returns 404 for an unknown name, instead of silently running the whole
YAML workflow (fixes named-agent regression flagged in review).
- Both routes build a lightweight per-request AgentsGenerator that borrows the
cached generator's shared adapter, tool-timeout pool and resolver while owning
its own cli_config, so concurrent requests overlap without a run-length lock.
Co-authored-by: Mervin Praison <[email protected]>
Addresses two P1 review findings on the tool self-repair path:
- Fold the bind-failure parameter schema into the error string so it survives
conversion to ToolExecutionError (which keeps only the message) and reaches
the model.
- Expand MCP instances into their contained tools via a shared
_iter_active_named_tools() helper so MCP tool names participate in
case/separator repair and appear in the corrective inventory instead of an
empty/opaque container.
Adds 3 tests (public-path param hint, MCP inventory, MCP name repair).
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on the prompt-injection defence:
- Greptile P1 (security): the bot's _attribute() fallback (taken when the
core helper import fails on an older praisonaiagents still allowed by the
>=1.6.152 range) only replaced CR/LF, leaking control chars and oversized
display names into every shared-chat prompt. It now mirrors the core
helper: collapse newlines, strip control chars, collapse whitespace and
length-bound to 240 — byte-identical to neutralize_untrusted_text.
- Greptile P2: neutralize_untrusted_text with max_chars<=3 returned the
3-char "..." string, exceeding the advertised bound. Small bounds now
truncate hard so len(out) <= max_chars always holds.
- Tests: add small-bounds regression (core) and a fallback-path hostile-name
regression that forces the helper import to fail (bot).
Co-authored-by: Mervin Praison <[email protected]>
An unquoted bare prompt (praisonai build a weather agent) arrives as
multiple argv tokens, but the modern `run` command takes a single
positional target. Join the flag-free tokens into one argument so the
whole prompt reaches `run` intact instead of Typer rejecting the extra
positionals. Addresses Greptile P1 (multi-token prompt).
Also adds a regression test pinning that flagless operational commands
(serve/call/realtime/debug/lsp/diag) stay Typer commands and are never
misrouted to `run` as prompts (Greptile P1 #2 was a false positive).
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Interactive reply/select values that exceed a channel's callback byte-cap
(e.g. Telegram's 64-byte limit) were hashed with no way to recover the
original value, so long option values (URLs, file paths, free-text) made
menus silently non-functional. This generalises the durable-reference
pattern already used by approvals to ordinary interactive callbacks.
- Add CallbackPayloadStoreProtocol (peer of ApprovalStoreProtocol) plus a
bounded, zero-dependency InMemoryCallbackPayloadStore default in core.
- Encoders persist an overflowing value under a short reference and emit
reply:@<ref> / select:<action_id>:@<ref>; inline values are unchanged.
- InteractiveRegistry(store=...) resolves @<ref> back to the exact value on
dispatch, preserving the select action_id prefix, and fails closed on an
unknown/expired reference. Without a store the prior hash behaviour is kept.
Additive and backward compatible: adapt_presentation gains an optional
callback_store kwarg; registry gains an optional store arg.
Co-authored-by: MervinPraison <[email protected]>
- Refuse non-localhost bind without --api-key/PRAISONAI_SERVE_API_KEY,
mirroring jobs/server.py policy (POST /agents can run YAML-defined tools).
- Cache one AgentsGenerator per app via FastAPI lifespan instead of rebuilding
it (YAML re-parse, registry re-resolve, fresh 32-worker pool) per request;
serialise the shared cli_config with an asyncio.Lock and close() on shutdown.
- Converge /agents and /agents/{agent_name} on the same generator path so both
apply identical YAML lowering (ToolResolver, tool_timeout, approval,
guardrails, retry) instead of the safety-stripped hand-rolled PraisonAgent.
Co-authored-by: Mervin Praison <[email protected]>
Add StreamEventType.RETRY and emit it alongside the existing ON_RETRY hook in both the sync and async retry loops (guarded by has_callbacks, so it stays zero-overhead when no consumer is attached). The event carries attempt, max_attempts, delay and reason in metadata.
The CLI stream-event bridge maps it to a canonical run.retry NDJSON event so consumers can render a live 'retrying in Ns (attempt k/N)' status instead of a silent, hung-looking terminal. Non-JSON/silent modes stay silent.
Co-authored-by: MervinPraison <[email protected]>
When the LLM emits an unresolvable tool call, the runtime now:
- attempts a deterministic self-repair by normalising case/separator drift
(e.g. 'WebSearch' -> 'web_search') against the agent's active tools;
- otherwise returns a corrective, model-readable error naming the nearest
valid tool and listing the currently available tools;
- on argument-bind failure, echoes the tool's required/optional parameters.
Reuses existing dispatch path; no new public API or Agent params.
Co-authored-by: Mervin Praison <[email protected]>
Add a pure-stdlib core helper neutralize_untrusted_text() in
praisonaiagents.session.context and apply it by default to the sender
display name in the bot's _attribute() prefix. Collapsing newlines/control
chars and bounding length stops a hostile group display name / title from
masquerading as a fake heading or system directive in the per-turn prompt,
while well-behaved names render byte-identically. Safe-by-default; no new
config knobs.
Co-authored-by: Mervin Praison <[email protected]>
A bare free-text prompt (`praisonai "do something"`) now forwards to the
same modern `run` path as `praisonai run`, inheriting session continuity,
--output modes, the credential gate and permissions. `.yaml` workflows and
flag-bearing legacy invocations remain on the legacy dispatcher.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer feedback on the in-loop continuation guard:
- Emit the just-completed (paid) row before the guard halts, so the
request that trips the sponsorship check is no longer silently dropped.
- Coerce sponsor_check_rows to a safe minimum of 1 so a 0 (divide-by-zero)
or negative (never-poll, guard disabled) value cannot break/disable the
safety check; unset preserves the 5000 default.
Adds tests for trigger-row emission and invalid-interval handling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Mervin Praison <[email protected]>
Two additive, backward-compatible features for the synthetic-data generator.
1. In-generator continuation guard. generate_dataset gains an optional
should_continue() -> bool callback, re-checked every `sponsor_check_rows`
completed requests (default 5000). When it returns False the run halts
cleanly via the existing shutdown path (touch stop-file + cancel pending
futures + break). If no callback is passed but `subscription_id` (or env
AZ_SUBSCRIPTION) is set, a built-in Azure sponsorship check is wired in
(quotaId == "Sponsored_2016-01-01"). The guard fails OPEN on transient
errors — an external watchdog is the hard fail-closed guard; this is
belt-and-suspenders. New public helper: azure_sponsorship_guard().
2. Per-row provenance stamping. Emitted rows are now self-describing: _call
stamps `model` (deployment) and passes through `task_type`/`topic` from the
prompt spec when present. The axis recipe stamps task_type/topic onto each
spec, so QC (which already reads r["task_type"]) and downstream category
splits key off metadata instead of brittle text. Keys are added only when a
value is available (backward-compatible).
Tests (network stubbed): the guard halts after N rows when the predicate turns
False and never halts while True; rows carry model/task_type/topic when
provided and omit task_type/topic when absent; plus fail-open and
subscription-id wiring.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The switch to praisonai.arun() dropped the caller's required input on the
native jobs path (job.prompt) and the serve /agents fallback (query), since
arun has no prompt param and the workflow topic is read from YAML input/topic
only. Honour input/topic overrides in AgentsGenerator._merge_cli_config
(reusing the existing merge seam, no new params/exports) and pass
job.prompt / query through cli_config so per-run input drives the workflow
instead of being silently discarded. Static YAML input is preserved when no
prompt is supplied.
Co-authored-by: Mervin Praison <[email protected]>
- serve.py: route the crew fallback through praisonai.arun (owns the
AgentsGenerator lifecycle, runs off the event loop) and replace
asyncio.get_event_loop() with asyncio.to_thread in both agent handlers.
- jobs/executor.py: _run_praisonai_agents now delegates to praisonai.arun
so the caller's agent_file/agent_yaml/framework/config are honoured
instead of silently dropped by a stock Agent.
- scheduler/agent_scheduler.py: reuse an instance-owned ThreadPoolExecutor
with leak accounting and recycling (mirrors AgentsGenerator), unify the
timeout branch into the backoff path, and close() the pool on stop().
Co-authored-by: MervinPraison <[email protected]>
- reuse a supplied empty shared MinHashLSH index (is not None, not `or`)
so cross-batch near-dedup works on the first call
- treat empty MinHash signatures as non-similar (0.0) and reject
minhash_perm/ngram_n < 1, so degenerate config can't collapse a dataset
- global_dedup honours near_dup_method='sliding' and raises a clear
FileNotFoundError for missing paths instead of a cryptic AttributeError
- sliding look-back treats window<=0 as intentional unbounded
- dedup CLI writes to a temp file + atomic replace, so an --out that
aliases an input is never truncated and a malformed line never leaves
a partial output
Co-authored-by: Mervin Praison <[email protected]>
The QC near-dup pass was O(n·3000) sliding-window char-4gram Jaccard, which is
both slow at scale and structurally blind to near-duplicates more than 3000 rows
apart — and score() rebuilds its dedup state per call, so it cannot remove dups
that span multiple batch files (the common case when merging parallel runs).
Add praisonai_train.data.dedup (pure stdlib, no datasketch/numpy):
- MinHasher / MinHashLSH: seeded, deterministic MinHash signatures over the same
normalized char-4gram shingles the sliding engine uses (so near_dup_jaccard
keeps identical semantics), with LSH banding for sub-linear candidate lookup
and no bounded look-back window.
- near_dedup(rows, cfg): one interface, YAML-tunable near_dup_method
("sliding" default = unchanged, or "minhash"). qc.score() now delegates to it
(DRY); default path behaviour is byte-for-byte the same. near_dup_window is now
configurable too.
- global_dedup(sources, cfg): dedup ACROSS many JSONL files / row-lists with ONE
shared exact+LSH index — what per-file score() cannot do.
- CLI: `praisonai-train dedup batch_*.jsonl --out merged.jsonl`.
Backward compatible: no new deps, default engine and outputs unchanged; MinHash
and cross-file dedup are opt-in.
Tests: 13 new (deterministic) — MinHash catches a far near-dup the window misses,
preserves genuinely-distinct rows, cross-file exact+near removal, exact-only, and
default-still-sliding. Full data suite 60 passed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Count completion tokens only for successful (ok) responses so token
throughput and avg_out_tok stay consistent with the success count.
- Thread a json_mode flag through _timed_call/_measure_target/
benchmark_deployments and expose --json-mode/--no-json-mode + yaml key,
so OpenAI-compatible endpoints without JSON mode aren't ranked as failures.
- Validate prompt dicts (require non-empty 'user') and reject n<1 /
concurrency<1 with a clear ValueError instead of KeyError / silent
zero-request runs miscounted as endpoint failures.
- Add 4 tests; existing stubs updated for the new json_mode arg. 102 passed.
Co-authored-by: Mervin Praison <[email protected]>
Add a first-class, protocol-driven speed benchmark to the praisonai-train
data SDK, mirroring the generate/validate idiom. It sends N identical
requests per deployment at a fixed concurrency and reports throughput
(rows/min, completion-tokens/sec), latency (avg/p50/p95), success count
and average output tokens, then ranks deployments fastest-first.
DRY: extract the generator's HTTP call path into shared helpers
(`build_chat_request`, `resolve_cfg`) so benchmarking reuses the exact
same Azure/OpenAI-compatible request construction and credential
resolution as real generation — no duplicated HTTP code. The default
prompt reuses a registered Recipe so benchmarks reflect the real workload.
- data/benchmark.py: `benchmark_deployments(...) -> list[BenchResult]`,
pure `aggregate()`, and network confined to a `_timed_call` seam.
- cli/commands/benchmark.py: `praisonai-train benchmark` (YAML/flags),
ranked table + JSON output.
- setup/benchmark.yaml: documented sample config.
- tests: aggregation math, ranking, success/failure counting, real
threaded path, per-target endpoint override (network stubbed).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The documented gateway.notify_on_undelivered / undelivered_template YAML
opt-in was a dead path: GatewayServerSchema (extra="forbid") rejected the
keys at load time, and neither start_with_config nor from_config_file
propagated them onto self.config, so the delivery router's getattr always
saw the OFF default.
- Accept both keys in GatewayServerSchema (kept optional, lightweight).
- Stamp them onto self.config in start_with_config (primary CLI path) and
from_config_file, honouring the "no new core GatewayConfig field" design.
- Add a regression test covering schema acceptance + propagation.
Co-authored-by: Mervin Praison <[email protected]>
Connect the previously-inert completion path (Greptile P1): BotSessionManager
now reads the agent's last_stop_reason after each turn and, when the new opt-in
surface_completion_reason flag is set, appends a concise user-safe note for
early stops (max_steps / cancelled / error). Off by default so clean
completions and existing deployments are unchanged.
Co-authored-by: Mervin Praison <[email protected]>
Address greptile P1 review feedback on the inbound media-album coalescer:
when the owning Telegram update is cancelled (e.g. during shutdown) after
sibling updates already folded their media in, the buffered temp files could
leak. Add an on_orphan cleanup hook invoked whenever a flushed album has no
live owner awaiting it, wired to a shared _remove_inbound_media helper in the
Telegram bot. Also clarify the max_items cap semantics (bounded, intentional).
Co-authored-by: Mervin Praison <[email protected]>
On a confirmed-permanent outbound failure the reply was parked in the DLQ /
the target marked dead but the user heard nothing. Add an opt-in last-resort
notice: a short plain-text delivery-failure message on the same channel plus a
MESSAGE_UNDELIVERED lifecycle hook so operators can route the failure.
Core (protocol only): add HookEvent.MESSAGE_UNDELIVERED and
MessageUndeliveredInput. Wrapper (praisonai-bot): DeliveryRouter fires the hook
and best-effort sends the notice on permanent failure; default OFF, wired from
gateway.notify_on_undelivered.
Co-authored-by: MervinPraison <[email protected]>
Telegram delivers a media album (multiple photos/files sent together) as N
separate updates sharing one media_group_id, so the agent saw each part as
its own turn. Add an AlbumCoalescer that buffers media parts by group key
and flushes them as one multimodal turn, mirroring the existing
InboundDebouncer text pattern.
- New praisonai_bot/bots/_album.py: bounded per-group buffer (window +
max_items), operator-tunable via config.metadata (media_group_window_ms /
media_group_max), disabled by default so behaviour is unchanged.
- Wire into TelegramBot.handle_message (sibling updates fold into one turn)
and flush pending buffers on stop().
- Tests in tests/unit/test_bot_album.py.
Co-authored-by: MervinPraison <[email protected]>
Add an optional, backward-compatible TurnCompletion contract to the bot
reply path so a chat user learns *why* a turn stopped early instead of
seeing a silently truncated/empty reply.
- TurnCompletion wraps the existing coarse Agent.last_stop_reason
(completed | max_steps | cancelled | error) into a portable value with
a user-safe, localisable .note(); completed turns surface nothing.
- AgentReply gains an optional `completion` field (defaults None).
- extract_completion() reads it off any result (incl. a bare Agent via
last_stop_reason); append_completion_note() lets a gateway optionally
append the note, off by default so existing deployments are unchanged.
Reuses the existing runtime stop reason rather than adding new detection
or Agent params — no new module, no wrapper knobs.
Co-authored-by: MervinPraison <[email protected]>
Move progress_callback below the yield in generate_dataset so 'kept' reflects
rows the consumer has actually received (Greptile P1). Docstring clarified.
Co-authored-by: Mervin Praison <[email protected]>
generate_dataset() gains an optional progress_callback(done, total, kept)
invoked as teacher requests complete, so a CLI/monitor/notebook can show
completion and remaining work. Backward compatible: default None keeps the
original behaviour (no callback). Fires once up-front with (0, total, 0) then
after each completed request; done is monotonic up to total, kept counts unique
rows emitted.
The `generate` CLI command wires a default reporter (_Progress): a tqdm bar when
tqdm is importable, else throttled done/total prints. tqdm stays optional — the
import is guarded and never becomes a hard dependency.
Adds unit tests asserting the callback fires with monotonic done up to total,
that kept tracks dedup, and that the default (no callback) path is unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- intent.translation_intent: an unrecognised non-empty task_type no longer
short-circuits to non-translation; it falls through to the documented text
fallback so an explicit "translate into English" instruction is honoured and
its English output is not dropped as low_script_purity (greptile P1).
- qc.score: the translation `exempted` metric now counts only rows the
English-target exemption ACTUALLY rescued from LowScriptPurity (task-awareness
on AND output below the purity floor), so it reconciles with check behaviour
instead of over-counting failed translations and task_aware_purity=false runs
(greptile P1).
- Add 4 regression tests (29 passed total).
Co-authored-by: Mervin Praison <[email protected]>
Add two dataset-QC features to the praisonai-train data SDK, wired in the
SDK's protocol-driven idiom (registered RowChecks + a corpus metric) and
sharing ONE translation-intent detector so they can never disagree (DRY).
1. Task-aware script purity: the script-purity checks no longer delete an
output that is legitimately in another script because the instruction
asked to translate INTO that language (e.g. a Tamil->English row's
correct output is English). LowScriptPurity/ScriptReview now exempt a
resolved English-target row; a new WrongTargetScript flag catches the
inverse failure (asked for English, output still Tamil). Only a resolved
English target exempts — en->ta / ambiguous / native rows stay strict.
2. Translation-share corpus metric: metrics.translation reports the fraction
of kept rows that are translation tasks, by direction (en_ta/ta_en/unknown)
and an exempted count, computed with the SAME detector so it reconciles
with the per-row exemptions.
New single source of truth: data/intent.translation_intent(); metadata
(task_type) is authoritative, instruction-text detection is the external-row
fallback, ambiguous direction yields no exemption (safe default).
17 new unit tests (mirroring the modeltrain reference); full data suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Addresses Greptile P1: an issue such as '[QUESTION] How do I handle this
API error?' matched 'error' before the question marker was checked,
receiving 'bug' + auto 'claude'. Guard bug detection on \!title.includes('[question]').
Co-authored-by: Mervin Praison <[email protected]>
Bug issues with a "## How to fix" section were labelled question first,
which blocked autoClaude. Check bug/error/crash before how-to heuristics,
ignore "how to fix" for question detection, and let bug label win over
a stale question label for auto-triage.
- qc: include output in exact-dedup key so distinct answers to the same
prompt are not silently dropped
- _util.fields: pair the final assistant turn with the user turn that
precedes it (never a trailing unanswered user turn)
- cli generate: forward --snapshot-every override; defer output truncation
until the first row is produced (a failed run no longer destroys an
existing file / self-referential dedup source); exit non-zero when 0 rows
are generated instead of reporting empty success
- cli validate: skip malformed JSONL lines (with line numbers) instead of
aborting the whole run
- generate: configurable request_timeout; accurate stop-file docstring
- tests: cover distinct-answer dedup and multi-turn chat pairing
Co-authored-by: Mervin Praison <[email protected]>
Remove the legacy `.praison/config.toml` from PROJECT_CONFIG_NAMES so the
walk-up discovery never mislabels a profile-level legacy global config as a
`project:` source. The legacy file is owned exclusively by
_load_global_config() with a `global:` label. Removes the now-dead
search_dir == home skip guard and adds a regression test.
Co-authored-by: Mervin Praison <[email protected]>
Harbor 0.20's BaseAgent.__init__ requires logs_dir as first positional
arg. The TestPraisonAIExternalAgent tests constructed the agent with zero
args, causing a TypeError. Pass logs_dir via pytest tmp_path to match the
existing pattern used by TestPraisonAICodeAgent and TestPraisonAIInstalledAgent.
Co-authored-by: Mervin Praison <[email protected]>
Harbor 0.20 agents expose extra_env (wired into the container exec
context by Trial), not self.env. Passing env=self.env crashed
PraisonAICodeAgent.run() with AttributeError. Omit the per-exec env=
so Harbor's trial-scoped injection applies. Same fix applied to
praisonai_installed_agent.py.
Co-authored-by: Mervin Praison <[email protected]>
Harbor's BaseInstalledAgent._exec always prefixes commands with
'set -o pipefail; ', so the .startswith assertion failed even when the
adapter command was correct. Switch to a substring match.
Co-authored-by: Mervin Praison <[email protected]>
- PRAISON_DEBUG now parsed as a boolean (0/false/no disable it, not just unset)
- auto-resume filters to real numeric checkpoint-* dirs and passes the validated
path to the trainer (instead of leaving resume=True and re-discovering); also
accepts 1/yes/on/off spellings before treating a string as a literal path
- push_model_gguf honors quantization_method default (q4_k_m) matching save path
- final save/eval cadence reconciliation after training_arguments passthrough so
load_best_model_at_end can't reach SFTConfig with mismatched strategies/steps
Co-authored-by: Mervin Praison <[email protected]>
Reverts the removal of tokenize_function — it is imported and re-exported in
praisonai/train.py __all__, so deleting it breaks that public API. Kept it (now
uncalled by the training path but retained for backward compat) and removed only
the internal, unreferenced tokenize_dataset method.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Remove dead tokenize_function/tokenize_dataset (unused since TRL tokenizes
internally; used padding='max_length' which would waste compute if ever rewired).
- save_model_gguf now honors config quantization_method (was hardcoded 'q4_k_m',
inconsistent with push_model_gguf).
- Gate the per-batch DEBUG prints behind PRAISON_DEBUG so normal runs stay readable.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Fix AgentTeam.start() result handling across 22+ workflow/usecase examples
- Fix missing AgentFlow/AgentTeam/AutoAgents imports and registry override demo
- Fix scope_identifiers, state_redis db import, and ToolsMCPServer.run_sse API
- Mark STT and stale tool-import examples as skip until refreshed
- Remove stale db mapping that shadowed praisonaiagents.db submodule
Co-authored-by: Cursor <[email protected]>
HF requires save_steps to be a multiple of eval_steps when load_best_model_at_end
is on. A config with save_steps=100, eval_steps=500 (both valid-looking) crashed.
Now the trainer aligns eval_steps to save_steps (preserving the user's checkpoint
frequency) with a warning, instead of erroring out.
Co-Authored-By: Claude Fable 5 <[email protected]>
resume_from_checkpoint: true now auto-finds the latest checkpoint in output_dir
and resumes from it; if none exists yet (fresh run) it starts fresh instead of
erroring. So after any crash (OOM, GPU reclaim) you just relaunch the SAME
command and training continues from the last saved step. Validated on GPU:
fresh run -> checkpoint-2/4; relaunch -> resumed from checkpoint-4, continued 5->8.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Remove wrapper import from praisonai-code agent_tools.py (C7 gate
regression): use a module-local _run_sync bridge instead of
`from praisonai._async_bridge import run_sync` so the Tier-2 package
no longer depends on the praisonai wrapper at the hot path.
- suite_runner: start child processes with start_new_session=True on
POSIX so timeout killpg reaps only the child tree, never the runner.
- agent_tools/orchestrator: honor acp_execute_command cwd by forwarding
it into the plan step; orchestrator validates it stays in workspace.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Kill Windows subprocess trees on suite-runner timeout (uvicorn hang)
- Restore Agent.launch() shared server globals in execution_mixin
- Add missing praisonai_code.cli.features.agent_tools module
- Fix compression_demo ContextCompressor API usage
- Import AgentFlow in workflow output/routing examples
- Use UTF-8 when parsing doctor ci subprocess output on Windows
- Set BrowserBaseTool __name__ for agent tool schema compatibility
Co-authored-by: Cursor <[email protected]>
Gemma 4 E4B (and other multimodal/MoE/elastic models) have LoRA-adapted params
that don't fire every step (vision/audio towers in text-only training), which
crashes DDP with 'Expected to have finished reduction' when find_unused=False.
Default True works universally; set false for a small speedup on dense-text models.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Set UNSLOTH_USE_NEW_MODEL before unsloth import (DDP non-reentrant path)
- Skip LoRA when full_finetuning: true (train base weights directly)
- Reload model with trained precision (dtype/8bit/full_finetuning), not 4bit-only
- Re-align save/eval strategy when early stopping enables load_best_model_at_end
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Only the main process merges/pushes and writes the final adapter, so multi-GPU
runs don't race to write the same repo/dir.
Co-Authored-By: Claude Fable 5 <[email protected]>
Validated end-to-end on 2x A6000 (unsloth 2026.7.4 / trl 0.24 / torch 2.10):
multi-GPU DDP trains (Data Parallel GPUs = 2, no 'marked ready twice'),
checkpointing + eval + best-model + final_model_dir all work.
Multi-GPU (DDP):
- Detect distributed (LOCAL_RANK/WORLD_SIZE); load full model per-rank
(device_map={'':local_rank}); set UNSLOTH_USE_NEW_MODEL=1 under DDP to force
Unsloth's non-reentrant checkpointing (the legacy path hard-codes reentrant,
which double-fires autograd hooks and crashes DDP). Set ddp_find_unused_parameters
=False (LoRA freezes the base). Launch: torchrun --nproc_per_node=N.
Checkpointing / resume:
- Config-driven save_strategy/save_steps/save_total_limit (was only under an env var);
resume_from_checkpoint (bool=latest, or a path); final_model_dir (fixes the old
lora_model vs output_dir mismatch that broke load_model).
Evaluation / early stopping:
- val_split_ratio carves an eval set; eval_strategy/eval_steps/load_best_model_at_end/
metric_for_best_model; EarlyStoppingCallback via early_stopping_patience.
Flexibility:
- Generic training_arguments passthrough (any raw SFTConfig field), auto-filtered to
what the installed TRL accepts (version-proof; e.g. save_safetensors/group_by_length
differ by version) — warns instead of crashing.
- Knobs: max_grad_norm, warmup_ratio, neftune_noise_alpha, dataloader_num_workers,
data_seed, adam betas, report_to/run_name (config, not just env), push_to_hub.
- Quantization: load_in_8bit, dtype, full_finetuning. LoRA: modules_to_save,
rank_pattern, alpha_pattern, use_dora. Data: local files, shuffle, rename, filter.
Correctness fixes:
- load_in_4bit KeyError when omitted; inference().to(cuda) -> self.device;
drop empty-formatted rows instead of silently training on blanks.
All new keys optional with prior defaults; existing configs unchanged.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Skip publishing (return early) when train:false leaves self.model None,
preventing an AttributeError on the merged/gguf push path.
- Make KNOWN_KEYS a frozenset to prevent accidental class-state mutation.
Co-authored-by: Mervin Praison <[email protected]>
- num_samples: reject 0/negative/bool/non-int and honor explicit values (fail
fast with a clear message instead of silently training the full set or
crashing mid-run).
- Publishing: fail fast in validate_config when huggingface_save /
huggingface_save_gguf / ollama_save is enabled without its target name,
instead of silently skipping the requested upload after a long run.
- Fix pre-existing CI failure: test_interactive_group_is_union was stale after
the `search` tool group was wired into the interactive default set (#3254);
add TOOL_GROUPS["search"] to the expected union.
Co-authored-by: Mervin Praison <[email protected]>
Validated end-to-end on GPU (2x A6000, unsloth 2026.7.4 / trl 0.24.0).
- Response masking: assistant_only_loss now tri-state with default "auto" — a
live probe checks whether the model's chat template actually supports assistant
masking and enables it only when safe, else full-sequence SFT. Prevents TRL's
cryptic RuntimeError on stock Qwen/Gemma/Llama templates (they lack the
{% generation %} markers). Explicit true on an unsupported template now fails
fast with a clear message. 'train_on_responses_only' accepted as an alias.
- Publishing defaults OFF and is gated on a target name being set — a plain
'train locally' config finishes with the LoRA saved to lora_model/ instead of
crashing after training on a missing/foreign hf_model_name.
- Config flags accept real YAML booleans (train: true) not just strings, via a
_flag() coercion helper (no more AttributeError on .lower()).
- Honor dataset num_samples (was advertised but silently ignored).
- validate_config(): clear error on missing required keys, warn on unknown keys
(typo/unsupported) so nothing is silently dropped — friendlier for agents.
- 'model' accepted as an alias for 'model_name' (matches --model flag).
- One concise run-summary block (model / examples / masking mode / steps / output).
- Align license to SPDX "MIT" + setuptools>=77 (matches main).
Co-Authored-By: Claude Fable 5 <[email protected]>
- pyproject (train + bot): license = "MIT" SPDX string (was deprecated table
form); bump build requires to setuptools>=77 which the string form needs.
- _release_lib: 'uv lock' instead of 'uv lock --frozen' so publish regenerates a
missing/stale lockfile (praisonai-train had none; dependency changes stale it)
instead of failing with 'Unable to find lockfile'.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Fail fast when neither the tokenizer nor config provides a chat template,
instead of silently training on empty text (swallowed apply_chat_template).
- Correct Gemma Ollama template to fold the system prompt into the first user
turn per the Gemma spec (no separate system turn).
- Document that assistant_only_loss requires a template exposing assistant
masks ({% generation %}); keep it opt-in and default-off.
- Remove the stale uv.lock (pinned trl<0.9.0 / trl 0.8.6, old torch/transformers/
unsloth floors) so it regenerates against the modern pyproject stack.
Co-authored-by: Mervin Praison <[email protected]>
Both rag_profiler and knowledge_profiler were byte-for-byte identical
except the "command" label. Extract a single command_profiler helper in
cli/commands/_profiling.py and have both delegate to it, so profiling
logic has one owner and cannot drift. Behaviour, flags, and output are
unchanged.
Co-authored-by: MervinPraison <[email protected]>
Extract the bearer / X-API-Key token extraction and constant-time
comparison used by both the serve feature and the jobs server into a
single build_api_key_middleware() helper so future auth fixes only need
to be made in one place. Behaviour and lazy-import posture are unchanged;
per-server public-path allowlists remain distinct via a parameter. The
JobsAuthRequiredMiddleware non-localhost guard is left untouched.
Co-authored-by: MervinPraison <[email protected]>
The dependency pins (trl<0.9.0, unsloth>=2024.8) were incompatible with any recent
Unsloth, which requires trl>=0.18.2 / transformers>=4.51.3. This modernizes the LLM
fine-tuning path so it works with the current stack (tested on 2x A6000 with
unsloth 2026.7.4 / trl 0.24.0 / torch 2.10 / peft 0.19.1).
Changes:
- deps: transformers>=4.51.3, unsloth>=2025.9.1, trl>=0.18.2, peft>=0.13.0,
accelerate>=0.34.0, bitsandbytes>=0.45.0 (pyproject + setup_conda_env.sh; the
conda script no longer pins an ancient Unsloth git commit).
- trainer: migrate to modern TRL API — SFTConfig instead of TrainingArguments,
processing_class instead of tokenizer, dataset_text_field/max_length/packing on
SFTConfig; drop the pre-tokenize + dummy-field hack (TRL tokenizes internally).
- trainer: stop force-applying the llama-3.1 chat template to every model (broke
Gemma/Qwen). Chat template is now config-driven and defaults to the model's own.
- trainer: honor config LoRA params (lora_r/alpha/target_modules/... were ignored).
- trainer: response-only loss via SFTConfig assistant_only_loss (model-agnostic),
replacing the Llama-hardcoded train_on_responses_only markers.
- Ollama Modelfile: add Gemma template + stop tokens (was Llama-only, wrong for Gemma).
Tested: end-to-end LoRA fine-tune smoke runs on GPU for both Qwen2.5-0.5B and
Gemma-2-2B (native templates) — train loop + local save succeed.
Co-Authored-By: Claude Fable 5 <[email protected]>
A completed cancelled run (rank 1) could override an in-progress run
(conclusion=null, rank 0) of the same name, hiding a still-running check
from allChecksGreenOnSha and allowing the merge gate to pass early.
Prioritize any non-completed run so pending checks always block.
Co-authored-by: Mervin Praison <[email protected]>
Treat cancelled smoke and test-windows as non-blocking when all test-core
shards succeeded on HEAD, dedupe check runs by best conclusion, and raise
smoke (12m) and Windows (10m) job timeouts to reduce timeout cancellations.
Switch all Harbor adapters, job configs, docs, and tests to
openai/gpt-4o-mini for a consistent, low-cost default across the
Terminal-Bench benchmark path.
Co-authored-by: Mervin Praison <[email protected]>
Surfaces genuine install/auth/startup failures in the Harbor code adapter
instead of always reporting success (per reviewer feedback). A benchmark
miss still exits 0 (Harbor grades by verification); only a crashed
assistant now fails the attempt.
Co-authored-by: Mervin Praison <[email protected]>
Repairs the examples/terminal_bench/ Harbor adapters (external/wrapper/
installed), adds a headline praisonai_code_agent that drives `praisonai code`
headlessly, rewrites job configs to the real Harbor JobConfig schema for
Terminal-Bench 2.1, adds packaging __init__.py files, rewrites the tests to
skip only when Harbor is truly absent, and adds a manual-dispatch smoke
workflow + RESULTS.md. Examples/CI layer only — no SDK changes.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer feedback that set_store()/migration failing open could
hide reader/writer drift. The local tool store remains authoritative, so
these paths stay best-effort but are now diagnosable via logs.
Co-authored-by: Mervin Praison <[email protected]>
Adds direct coverage for the bot-side _execute_one silence path (exact
markers, whitespace variants, ordinary output, prose mention) asserting
delivery is suppressed while the run is still recorded as succeeded.
Co-authored-by: Mervin Praison <[email protected]>
Scheduled/automation runs now consult the existing core silence primitive
(praisonaiagents.bots.silence.is_intentional_silence_response) before
delivering. When a run's whole output is an exact silence marker
(NO_REPLY / [SILENT] / SILENT) delivery is suppressed while the run still
completes and is recorded, so unattended monitors stop posting the raw
control token. Applied to both the wrapper lightweight path
(AgentScheduler._deliver_result) and the bot full-gateway path
(ScheduledAgentExecutor). Prose merely mentioning the token is unaffected
(exact-match semantics).
Co-authored-by: MervinPraison <[email protected]>
Add a canonical get_default_store()/set_default_store() resolver in
praisonaiagents.scheduler and route the agent tools, gateway tick loop,
and host bridge through it so writer and reader share one backend.
Co-authored-by: MervinPraison <[email protected]>
Convert value to str before strip()/lower() so an unquoted YAML
auth_token (e.g. 12345) cannot raise AttributeError during the
weak-secret check. Preserves empty/None handling and denylist match.
Co-authored-by: Mervin Praison <[email protected]>
- gateway doctor now fails closed on a MISSING auth_token on an external
bind (matching startup), not just a weak one (Greptile P1)
- doctor --json always emits a single parseable JSON document on the
no-channels weak/missing-secret failure path (Greptile P2)
- replace high-entropy fake hex test tokens with an obviously-non-secret
literal to clear GitGuardian false positives without weakening tests
- add TestDoctorGatewaySecretStrength covering the new doctor paths
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile review findings on the doctor auto-fix safety contract:
- In-place repairs no longer discarded: pass a fresh copy to apply_fix so
rules that mutate-and-return their argument still record a diff.
- Collection failures now recorded as blocking refused findings instead of
being silently swallowed (previously produced an apparently clean plan).
- Auto-derived backup paths use microsecond timestamps plus a numeric
uniqueness suffix so a rapid/concurrent run never overwrites an earlier
snapshot.
- No-op apply runs no longer report applied=True; applied now requires an
actual change.
Adds tests for each case.
Co-authored-by: Mervin Praison <[email protected]>
Add a core weak-secret guard so an externally-bound gateway refuses to
start behind a publicly-known placeholder token (e.g. change-me,
your-token-here, or the literal $(openssl rand -hex 16) fix-hint pasted
verbatim). Loopback binds downgrade to a warning, consistent with the
existing permissive-loopback posture.
- core: KNOWN_WEAK_SECRETS, is_weak_secret, assert_gateway_secret_strong,
WeakGatewaySecretError in praisonaiagents.gateway.protocols
- praisonai-bot: enforce in assert_external_bind_safe (fail closed on
external, warn on loopback) and surface in `gateway doctor`
Co-authored-by: MervinPraison <[email protected]>
Add a repair safety contract to the core doctor engine so self-repair is
safe to run: dry-run diff preview, backup, refuse-on-unrecoverable, and
post-fix re-validation. Introduces ConfigDiff/RepairPlan and a
DoctorRulesRegistry.plan_fixes() while keeping apply_all_fixes()/apply_fixes()
backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer-flagged issues in praisonai run --worktree:
- P0: untracked output no longer silently deleted. Teardown now detects
changes via git status --porcelain (sees untracked files, which git diff
does not) and commits them to the isolated branch, retaining the branch;
only the worktree checkout is pruned. Falls back to keeping the worktree
if the commit fails (e.g. no git identity), so output is never lost.
- P1: identical targets get independent worktrees/branches via a per-run
unique token, so concurrent/repeated runs never share state.
- P1: relative/untracked local YAML now resolved to an absolute path before
chdir, so praisonai run ./agents.yaml --worktree still loads its config.
Adds tests for untracked-output preservation and per-run independence.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- code_intelligence: core grep now searches the configured workspace
(search_path) instead of the process CWD, matching the grep -rn fallback;
paths outside the core tool's root fall back cleanly.
- interactive_tools: apply PRAISON_TOOLS_DISABLE via ToolConfig.__post_init__
so direct constructions (to_tool_config, HeadlessInteractiveCore) honor the
advertised opt-out, not just from_env().
Co-authored-by: Mervin Praison <[email protected]>
A catch-all read:* allow rule previously opted every secret file into
ALLOW, trivially defeating the gate. Now only a secret-specific allow
rule (e.g. read:*.env) or a persistent approval opts in; a broad
wildcard falls through to the ASK default. Deny and approvals still win.
Co-authored-by: Mervin Praison <[email protected]>
Wire the existing core GitWorktreeAdapter into the CLI run lifecycle as a live consumer. praisonai run with --worktree provisions a fresh git worktree/branch, runs the agent there, reports the branch + a summary diff, and tears the worktree down (or keeps it with --keep). Degrades to a transparent no-op outside a git repo. Kept lightweight: no new Agent params/modules, only a self-contained context manager in run.py plus two CLI flags.
Co-authored-by: MervinPraison <[email protected]>
Add a `search` tool group (grep, glob, ast_grep_search) to the interactive
coding agent's default toolset, resolving to the existing ripgrep-backed core
builtins. Also route code_intelligence search through the core grep first,
keeping `grep -rn` only as a last-resort fallback.
Co-authored-by: MervinPraison <[email protected]>
Reads of secret files (.env, private keys, *.pem, *.key, id_rsa, etc.) now
default to `ask` in the core permission manager, closing a silent
secret-exfiltration path where a default coding agent could read credentials
and forward them to the model provider. Safe example/sample/template files
remain allowed, and explicit user rules/approvals (e.g. `read:*.env=allow`)
override the default so opting in for trusted workflows stays a single rule.
Co-authored-by: MervinPraison <[email protected]>
Gap 1: run/arun and the 4 legacy CLI call sites now own the
AgentsGenerator lifecycle via `with`, releasing its lazily-allocated
tool-timeout executor per run instead of leaking daemon threads in
long-lived server workers.
Gap 2: _is_protocol_instance / _is_cli_backend_instance now use
isinstance against the @runtime_checkable CliBackendProtocol instead of a
hasattr(execute, stream) duck-type, so a BaseCLIIntegration look-alike is
no longer mistaken for a backend; it fails fast at construction with a
pointed TypeError instead of crashing deep in the agent loop.
Gap 3: both workflow kickoff branches are bracketed in observability_session
so AgentOps init/finalize fires for workflow YAMLs too, and _build_yaml_workflow
now runs cli_backend compatibility validation and warns when a set
tool_timeout cannot be enforced on the native workflow path.
Co-authored-by: MervinPraison <[email protected]>
Make llm.py delegate _format_ollama_tool_result_message to the
protocol-driven OllamaAdapter.format_tool_result_message instead of a
hardcoded, behaviorally-diverged duplicate. The error-handling behavior
(apology-oriented prompt for dict/list results carrying an 'error' key)
is ported into the adapter so the single source of truth now lives in
the adapter layer, matching the pattern used elsewhere. Fully
backward-compatible: call sites are guarded by _is_ollama_provider(), so
Ollama formatting is always produced.
Co-authored-by: MervinPraison <[email protected]>
Bug A: Typer does not support typing.Literal; replaced _SandboxType
Literal with a str/Enum SandboxBackend so grouped --help lazy-loading
of the sandbox command no longer raises RuntimeError. Handler receives
.value to preserve string API.
Bug B: Added missing 'usage' command to COMMAND_CATEGORIES so every
registered lazy command has a help category.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Windows denies a second open of a NamedTemporaryFile while its handle is
still open, causing SQLite "unable to open database file" errors. Replace
the NamedTemporaryFile pattern with pytest tmp_path in the two affected
tests in test_server.py.
Co-authored-by: Dhivya-Bharathy <[email protected]>
- test_syntax_check.py: resolve agent.py via pathlib relative to __file__
so the check runs from any working directory (repo root or package dir)
- suite_runner/executor.py: run the dry-run branch before per-item env
checks so a preview lists runnable items even when credentials are absent
- cli/commands/docs.py: do not fail strict mode on dry-run, since dry-run
intentionally marks runnable items as not_run
Rebased onto current main; the dry-run feature already merged there, so
only the genuinely-missing path fix plus the two reviewer-flagged bug
fixes remain.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Restores pre-gate behaviour where 1000-1500 token context was returned
unchanged for small branch counts, instead of being truncated.
Co-authored-by: Mervin Praison <[email protected]>
Partial-truncation in aggregate() used a fixed remaining*4 char estimate,
which over-retained dense non-ASCII (CJK) text and could exceed max_tokens.
Now selects the longest prefix whose canonical token estimate fits the
remaining budget via _truncate_to_tokens(). Adds regression tests.
Co-authored-by: Mervin Praison <[email protected]>
Completes the disabled-path guard pattern for the two remaining trace
emit methods, matching the six methods fixed in this PR and their hotter
siblings. Byte-identical enabled behavior; only skips wasted allocation
when tracing is disabled.
Co-authored-by: Mervin Praison <[email protected]>
Small parallel groups (e.g. 2 branches) no longer pay for an extra LLM
summarisation call whose token cost roughly matches what it saves. The
cheaper _truncate_context_for_branches fallback is used below the
MIN_BRANCHES_FOR_LLM_SUMMARY threshold; LLM summarisation stays unchanged
for larger fan-outs.
Co-authored-by: MervinPraison <[email protected]>
Replace the crude `len(text) // 4 + 1` estimate in
ContextAggregator._estimate_tokens with a delegation to the package's
canonical estimate_tokens_heuristic. ASCII-identical, but now correctly
weights non-ASCII (CJK) characters, preventing context-budget overruns
for non-English content. Single source of truth for token estimation.
Co-authored-by: MervinPraison <[email protected]>
Add early `if not self._enabled: return` to session_start, session_end,
context_snapshot, agent_handoff, memory_store, and memory_search, matching
the existing pattern in hotter siblings. Avoids allocating a ContextEvent
(and slicing query strings) on every memory/session/handoff op when tracing
is disabled. Output is identical when tracing is enabled.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer (Greptile P1) findings on PR #3164:
- restart --drain-timeout now flows into stop()/graceful-stop so the OLD
process gets the full drain window instead of a fixed 10s cut-off.
- pause/resume/reconnect + URL resolution now honour --host/--port /
GATEWAY_PORT so a gateway on a non-default endpoint is located instead of
always probing 127.0.0.1:8765.
- Windows daemon restart aborts if `schtasks /End` fails, preventing a
duplicate/colliding gateway relaunch.
- Documented that the direct (non-service) restart fallback cannot recover
CLI-only launch flags; service/YAML is the production path.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile P1 findings on the scheduled/proactive delivery path:
the shared scheduled_outbox is drained whole on every delivery, but the
sender captured a single idem and stamped it on every drained row, and
success was read from the aggregate drain count.
- _send now uses each entry's own idempotency key (carried in the
payload) so a backlogged row is no longer sent under the current key,
which previously poisoned the router LRU and suppressed the real row
as a duplicate (message lost).
- Result for this delivery is resolved via status_for(idem) scoped to
our own key, never the aggregate succeeded count, so an unrelated row
succeeding no longer masks this row's failure.
- Drain-exception fallback checks status_for(idem) instead of a blind
re-send, avoiding a duplicate against a still-pending durable row.
- Add regression test for the multi-eligible-row (backlog + fresh) case.
Co-authored-by: Mervin Praison <[email protected]>
The flagship WebSocketGateway is a separate startup path from BotOS and
stamped the identity resolver + admission gate onto each channel bot's
session but never a shared turn-lock map. Channels unifying to one session
therefore still held distinct LockMaps, leaving the concurrent-transcript
bug open on the gateway path. Add _stamp_turn_lock_map (mirroring
_stamp_identity_resolver / _stamp_admission_gate) and wire it into both
start_channels and _start_single_channel, gated on a configured resolver.
Co-authored-by: Mervin Praison <[email protected]>
Route the gateway's scheduled/proactive delivery through the same durable
OutboundQueue the reply path uses, so its UNIQUE idempotency key — which
survives a restart — is the source of truth for "already sent" instead of the
per-process, in-memory LRU that dies with the process. A crash-and-refire after
a restart is now a no-op INSERT resolving to the already-sent row, so the
scheduler no longer double-posts the same proactive message.
The DeliveryRouter remains the sender, preserving its token-bucket throttle,
dead-target suppression, and LRU on top of durability. A genuinely failed
pre-crash send stays non-terminal and is re-delivered at-least-once after
restart. Falls back to the prior LRU-only router path if the durable store
cannot be built.
Adds a minimal OutboundQueue.status_for(key) read so a deduped re-fire is
reported as a suppressed success rather than a spurious failure.
Co-authored-by: Mervin Praison <[email protected]>
If _save() fails (full disk, permissions, failed replace), claim_due
previously still returned the claimed jobs even though the lease and
advanced last_run_at were never persisted. The runner would execute them
while the next poll re-read the unchanged file and re-fired duplicates —
defeating the at-most-once guarantee on the exact failure path it protects.
_save() now returns a bool; claim_due drops the claim (returns []) and
releases in-memory leases when the write did not persist, so the job is
retried cleanly. Applied identically to ConfigYamlScheduleStore and
FileScheduleStore for parity. Adds persist-failure tests for both stores.
Co-authored-by: Mervin Praison <[email protected]>
Cross-platform continuity via the identity resolver unified distinct
platform users onto one persisted session, but each adapter's
BotSessionManager owned its own LockMap keyed on the resolved id. Two
adapters resolving to the same unified id held two distinct locks, so
near-simultaneous turns ran concurrently against one transcript,
interleaving read-modify-write and breaking strict alternation.
Fix reuses the existing LockMap + _get_lock (no new class): BotOS builds
one shared LockMap and injects it into every managed bot's session
manager (mirroring _wire_admission_gate / _wire_outbound_messenger), so
turns serialise on the resolved id regardless of which adapter a message
arrives on. Wired only when an identity resolver is configured, so the
single-adapter path is unchanged and fully backward-compatible.
Co-authored-by: Mervin Praison <[email protected]>
The default schedule store lacked an atomic claim_due(), so ScheduleRunner
degraded to the non-atomic get_due_jobs() path — causing multi-process
gateways to double-fire every scheduled job. Implement claim_due()/complete()
on ConfigYamlScheduleStore, reusing the fcntl/msvcrt advisory-lock +
persisted-lease pattern already proven in FileScheduleStore, so the
at-most-once guarantee now holds out of the box on the default store.
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile P1: PRAISONAI_BROWSER_SKIP_EXTENSION_WAIT now only bypasses
the readiness poll for explicit truthy values (1/true/yes/on), so "false" or
inherited values no longer disable the check. Removed the PYTEST_CURRENT_TEST
auto-skip that would suppress readiness polling for live integration tests and
test-launched subprocesses. Mocked unit tests now opt in via an autouse fixture.
Co-authored-by: Mervin Praison <[email protected]>
- Escape ASCII status marker in _print_success via rich.markup.escape so
"[OK]" renders literally instead of being parsed as (invalid) Rich markup.
- doctor --json now emits a JSON error object on failure instead of
Rich-formatted text, keeping stdout machine-readable for automation.
- Add regression tests for both paths; make cp1252 marker assertion
environment-agnostic.
Addresses greptile P1 review comments on #3228.
Co-authored-by: Mervin Praison <[email protected]>
- Freeze escalate_fields/safe_fields in __post_init__ so directly-constructed
descriptors are truly immutable authorisation records (no post-hoc mutation).
- Model APPROVALS/PAIRING as incomparable siblings: combining two distinct
same-tier scopes escalates to ADMIN (fail closed) instead of silently
picking one, closing a single-scope-check bypass.
- Add tests for incomparable-scope escalation and collection immutability.
Co-authored-by: Mervin Praison <[email protected]>
- Wrap DockerSandbox.start()/execute() in asyncio.wait_for so image pull/startup
can never hang the run (configurable via PRAISONAI_SANDBOX_E2E_DOCKER_TIMEOUT)
- Report unavailable Docker image/registry as SKIP instead of FAIL so the
optional check no longer fails a required run
- Clarify in README that e2e-validation/ and tests/ require a repo checkout
(not shipped in the published wheel)
Co-authored-by: Mervin Praison <[email protected]>
Apply the same STDIO entry-point resolution to cmd_config_generate_recipe
so standalone (pip install praisonai-mcp) users get a runnable command
instead of the umbrella-only "praisonai mcp serve-recipe" (fixes#3210).
Co-authored-by: Mervin Praison <[email protected]>
Strengthens the list-resources/list-prompts regression tests to verify the
full expected set of 7 resources and 7 prompts (by name/URI) rather than only
asserting non-empty output, per reviewer feedback.
Co-authored-by: Mervin Praison <[email protected]>
Wrap tempfile/open in the symlink capability probe so any OSError skips
the test rather than failing module collection (Greptile feedback).
Co-authored-by: Mervin Praison <[email protected]>
- server.py: skip/cap the pre-connect health-check loop under pytest or when
PRAISONAI_BROWSER_SKIP_EXTENSION_WAIT is set, and bound the wait by timeout,
so mocked websockets tests reach websockets.connect instead of timing out.
- sessions.py: create parent directories for custom SQLite db_path.
- test: use a dedicated event loop in test_extension_engine_value to avoid the
deprecated get_event_loop() failing on Python 3.12.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Detect legacy console encodings and fall back to ASCII status markers so
`praisonai-mcp doctor` no longer crashes with UnicodeEncodeError on default
Windows cp1252 consoles. Adds a `--json` mode with no Unicode symbols and a
regression test, mirroring the umbrella `praisonai doctor` fix (#2270).
Co-authored-by: Dhivya-Bharathy <[email protected]>
- praisonai-bot: close the NamedTemporaryFile handle before load/unlink so
os.unlink no longer hits WinError 32 on Windows.
- praisonai-train: add TrainingStorage.close() + context-manager support that
closes the underlying storage backend (e.g. SQLite), releasing the DB file
handle to avoid WinError 32 and ResourceWarning: unclosed database.
- Update SQLite-backed storage tests to deterministically close backends.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Add e2e-validation/ bundle with an orchestrated single-command runner
(run_e2e.py), a manual guide, and a README. The runner covers subprocess,
SandboxManager, backward-compat shim, optional Docker, and CLI smoke; exits
non-zero on any required failure and skips Docker/shim gracefully when
unavailable. Also link the runner from the package README.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Add skipif guards to two tests that fail on default Windows configs:
- test_symbolic_link_resolution: skip when symlink privilege unavailable
- test_timeout_handling_posix: skip on Windows (os.killpg is POSIX-only)
Co-authored-by: Dhivya-Bharathy <[email protected]>
Auto-detect the available entry point via shutil.which so config-generate
emits `praisonai-mcp serve` for standalone installs and remains backward
compatible with `praisonai mcp serve` for umbrella installs. Applies to all
four STDIO client generators (claude-desktop, cursor, vscode, windsurf).
Co-authored-by: Dhivya-Bharathy <[email protected]>
list-resources and list-prompts read the resource/prompt registries without
calling register_all() first, so they always printed empty results on a fresh
process. Add register_all() to both commands, matching cmd_doctor and cmd_serve.
Co-authored-by: Dhivya-Bharathy <[email protected]>
- Probe Ollama's native /api/tags at the server root so a base URL ending
in /v1 is not mangled into /v1/api/tags; add an OpenAI /v1/models fallback
so generic local servers (llama.cpp/LM Studio/vLLM) are detected.
- Prefix detected models by provider (ollama/ vs openai/) instead of always
assuming Ollama.
- Key the probe cache by resolved endpoint so a mid-process OPENAI_BASE_URL /
OLLAMA_HOST change never returns a stale positive/negative.
- Add regression tests for both fixes.
Co-authored-by: Mervin Praison <[email protected]>
Address two P1 review findings on the CLI home unification:
- Writes (config set/reset) always target the canonical ~/.praisonai via a
new get_user_config_write_path(), migrating any existing legacy values
forward while leaving ~/.praison read-only.
- setup's get_praison_home() now expands ~ in PRAISONAI_HOME so setup and the
config resolver resolve the override to the same directory.
Co-authored-by: Mervin Praison <[email protected]>
Probe for a reachable local OpenAI-compatible endpoint (Ollama) and use it as
the zero-config default when no cloud key is configured, downgrading the
interactive API-key wall to a non-blocking suggestion. Non-TTY/CI still fails
fast when nothing is detected.
Co-authored-by: MervinPraison <[email protected]>
Collapse the duplicated "which stores back a CLI session" enumeration onto
one helper so list/continue/resume and show/export/delete address the same
stores in the same order by construction, instead of two hand-kept copies.
- project_sessions.canonical_cli_stores(): single enumerator used by
list_project_sessions, find_last_session (via _resolve_usage_store) and
session_exists_anywhere.
- session_resolver._canonical_stores() now delegates to it, removing the
parallel store list that caused repeated list-vs-resume drift.
- commands/session._create_backend defaults now anchor under the canonical
data home (~/.praisonai) instead of the legacy ~/.praison root, so a
backend-selected store no longer silently splits sessions across two homes.
- Regression test asserting the list-path and resolver-path enumerate
identical store instances.
Kept minimal and backward-compatible: no public API/flag removed, protocol
unchanged, no store rewrite/migration (deferred to the home-unification
effort the issue references).
Co-authored-by: MervinPraison <[email protected]>
Re-root cli/configuration/paths.py on the core praisonaiagents.paths
canonical home (get_data_dir): PRAISONAI_HOME override, ~/.praisonai
default, ~/.praison legacy fallback. This makes the home every command
uses (sessions/traces/logs/cache/state/model-recency) the same directory
`setup` writes config into. Legacy ~/.praison/config.toml is kept as a
read-only fallback so existing installs keep resolving until migrated.
Co-authored-by: MervinPraison <[email protected]>
- integrations/base.py: on the stream_async timeout/early-exit path, cancel and
await the concurrent stderr reader task so it cannot outlive the generator as
a pending task or surface an uncollected exception on the caller's loop.
- framework_adapters/registry.py: key the availability cache by the lowercased
name (matching PluginRegistry.resolve's case-insensitivity) so
invalidate_availability("crewai") clears a prior is_available("CrewAI") result.
Co-authored-by: Mervin Praison <[email protected]>
- integrations/base.py: apply timeout to stream_async via deadline +
asyncio.wait_for on each stdout read and the stderr/wait drain, so a
stalled subprocess can no longer hang forever (timeout was dead code).
- recipe/operations.py: split run_background into async-native
arun_background + a sync entry that routes through the _async_bridge,
which raises a clear error when called from a running loop instead of
deadlocking; export arun_background.
- framework_adapters/registry.py: memoise availability probes per process
(with invalidate_availability test hook) and protocol validation per
adapter class, eliminating repeated adapter construction + inspect on
the run()/arun() hot path.
- _entrypoint.py/agents_generator.py: resolve the adapter once and thread
it into AgentsGenerator so the winning adapter is not constructed twice.
Co-authored-by: MervinPraison <[email protected]>
Bulk agent chat-history restore parsed composite session ids by stripping
the "{session_id}_" prefix, so overlapping caller-supplied ids could load
another conversation under the wrong agent_key (Greptile P1/Security).
Save now tags each child session with parent_session_id + agent_key metadata;
list_sessions surfaces agent_key; bulk restore resolves the exact agent_key
from those tags and only falls back to prefix-stripping for legacy untagged
sessions. Backward compatible; no new params or exports.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: execute_tool_async now runs BEFORE_TOOL/AFTER_TOOL security hooks so
the primary async path (achat -> _execute_unified_achat_completion) no longer
silently skips HookRegistry gating. Zero-overhead when no hooks are registered.
Gap 2: PraisonAIAgents.__init__ raises ValueError for unknown process values
instead of silently running zero tasks; docstring corrected (parallel fan-out
is async_execution=True on tasks, not process="parallel").
Gap 3: Session chat-history restore now reads SessionStore first, mirroring the
save path introduced by the G-2 refactor, so resumed sessions recover history.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Sort DEFAULT_CONTEXT_LENGTHS by descending key length so get_context_length's
partial matching checks specific model names (e.g. gpt-4-32k) before shorter
prefixes (e.g. gpt-4), fixing gpt-4-32k-0314 resolving to 8192 instead of 32768.
Add a module-load assertion so eval-only extras can never silently shadow
canonical MODEL_LIMITS entries.
Co-authored-by: Mervin Praison <[email protected]>
Cache the resolved ToolSearchConfig (including the ImportError fallback)
in a module-level sentinel so _resolve_tool_search_config() returns a
single stable class object. Without this, the fallback path defined a
fresh dataclass per call, breaking isinstance() passthrough across
resolve_tool_search() invocations. This also converges the __getattr__
and resolve_tool_search resolution paths on the same cached class.
Co-authored-by: Mervin Praison <[email protected]>
Build eval/tokens DEFAULT_CONTEXT_LENGTHS from the canonical
context/budgeter.MODEL_LIMITS plus eval-specific extras, so gpt-5 and
gpt-4.1 resolve to their real 1M window in the eval path. All prior eval
keys (dated snapshots, o1/mistral/deepseek/groq) are preserved.
Co-authored-by: MervinPraison <[email protected]>
Replace the eager module-level ToolSearchConfig alias in
config/feature_configs.py with a PEP 562 __getattr__ so that
`from praisonaiagents import Agent` no longer initialises the entire
tools subsystem just to re-export a backward-compat alias.
- ToolSearchConfig is resolved (and tools.tool_search imported) only on
first attribute access, then cached.
- ToolSearchParam uses a string forward reference.
- resolve_tool_search lazy-imports the config when tool search is used.
- ImportError fallback semantics and all import paths preserved.
Co-authored-by: MervinPraison <[email protected]>
- tool_panel.py ApprovalResponse message now carries optional reason
(backward-compatible default None), matching the interactive events
ApprovalResponse contract for the live y/n approval path.
- Clarify ApprovalDialog.on_button_pressed docstring: the live Textual
TUI approves/denies via the tool_panel y/n handler and does not yet
collect a reason, so the param is not misleadingly presented as active.
Addresses Greptile P2 on PR #3180.
Co-authored-by: Mervin Praison <[email protected]>
Add an optional reason field to the interactive ApprovalResponse and prompt
for a one-line correction on Reject in the Rich and Textual frontends,
mirroring the console approval backend. Empty input preserves plain denial.
Co-authored-by: MervinPraison <[email protected]>
- rich_frontend/textual_frontend: escape preview via rich.markup.escape so
diffs/content containing bracket chars no longer raise MarkupError and crash
the approval dialog (Greptile P1)
- events.render_change_preview: synthesise a unified diff from old_string/
new_string for edit calls lacking a precomputed diff (Greptile P2)
Co-authored-by: Mervin Praison <[email protected]>
Render a unified diff (edit/apply_patch) or truncated new content (write)
before prompting for approval in the interactive Rich and Textual frontends,
so users approve the concrete change rather than just a tool label. Adds a
shared render_change_preview helper and ApprovalRequest.change_preview() in
the interactive events module, mirroring approval_backend._render_preview.
Co-authored-by: MervinPraison <[email protected]>
Empty/whitespace-only profile strings were passing through _as_opt_str
as non-None values, making them indistinguishable from a named scope
and violating the documented 'None means unscoped' fail-closed contract.
Mirror the existing trust normalisation in __post_init__ and add tests.
Co-authored-by: Mervin Praison <[email protected]>
Adds an optional `profile` field to the core RouteBinding routing contract
and surfaces the matched profile via RouteMatch/resolve_route. This gives
each route a named isolation scope the wrapper can key its memory namespace
/ secret scope off, so one gateway can multiplex tenants. Resolution is
most-specific-wins and fails closed: an unmatched route carries profile=None
and never inherits another tenant's profile.
Core-only, backward-compatible (all fields optional), no new deps/exports.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer findings on PR #3187:
- Graceful at-least-once fallback when metadata.message:write scope is absent
(was a permanent message-loss regression on apps without the scope).
- Reconcile threaded sends via conversations.replies (history excludes replies).
- Forward idempotency_key on the initial send path, not just crash-recovered drain.
- Gate key forwarding on an explicit idempotency_key param (drop **kwargs fallback).
Co-authored-by: Mervin Praison <[email protected]>
Slack, Telegram, Discord, and Webhook approval backends persisted the
pending approval before polling but skipped _resolve_pending on their
early-return guards (missing chat/channel id, failed send) and outer
except handlers. This left stale status='pending' rows that rehydrate()
surfaced as false-positive outstanding approvals after a restart.
Each missed exit path now records the fallback denial via _resolve_pending
before returning, matching the HTTP reference backend. Adds regression
tests for the early-exit and exception paths.
Co-authored-by: Mervin Praison <[email protected]>
The Discord native upload branch caught all exceptions and returned
False, so a transient blip bypassed deliver_with_retry entirely and the
file was silently dropped — the exact gap this PR set out to close, but
only Telegram/Slack got it. Let transport errors propagate (matching the
other transports) so the retry wrapper backs off and retries; only the
missing optional discord dependency is caught locally, and unresolved
channels still fall through to the no-primitive False return.
Co-authored-by: Mervin Praison <[email protected]>
The outbox's crash-reconciliation seam shipped unused: no channel adapter
implemented was_delivered() or declared reconciles_unknown_send, so every
built-in channel fell back to at-least-once (a duplicate on restart).
Slack can read back recent channel history and match a client-side idempotency
key stamped into the message metadata, so it now opts into effectively-once:
- SlackBot declares reconciles_unknown_send=True and implements was_delivered()
via conversations.history, matching the key in message metadata.
- send_message stamps the idempotency key into Slack metadata when provided.
- DurableDelivery carries the idempotency key in the outbox payload and
forwards it to adapters whose send_message accepts it (backward-compatible).
- _build_reconciler now passes the target channel to was_delivered when its
signature accepts it (legacy single-arg form still supported).
Channels whose platform cannot confirm delivery stay at-least-once by design.
Co-authored-by: MervinPraison <[email protected]>
Chat approval backends (Slack, Telegram, Discord, Webhook, HTTP) held the
pending approval only in an in-memory polling coroutine, so a gateway/bot
restart while an approval was outstanding stranded the blocked agent run.
Add an opt-in DurableApprovalMixin to the shared _approval_base module that
wraps the existing ApprovalStore: it persists the request before polling,
records the final decision, and exposes rehydrate() to recover outstanding
approvals on startup. Each chat backend now accepts an optional store= and a
shared DEFAULT_APPROVAL_TIMEOUT. When no store is given behaviour is unchanged
(fully backward-compatible); no new dependency is introduced.
Co-authored-by: MervinPraison <[email protected]>
Media uploads via DeliveryRouter.send_media were attempted exactly once —
a transient transport blip (HTTP 5xx, rate limit, connection reset) silently
dropped the file while the accompanying text was delivered durably. Text gets
resilience because adapters wrap send_message in deliver_with_retry (bounded
exponential backoff honouring server Retry-After); the raw media upload path
reached none of it.
Wrap the media upload in the same deliver_with_retry helper, reusing the
adapter's already-configured backoff policy when present. Non-retryable
outcomes are preserved: an adapter with no upload primitive still returns
False without retrying, and a permanent error still surfaces as a failed send.
No new params, modules, exports, or schema — the minimal wiring that closes
the reliability gap.
Co-authored-by: MervinPraison <[email protected]>
- _resolve_effective_tool_timeout and _validate_cli_backend_compatibility now
fall back to {} when roles/agents are explicit YAML nulls, preventing a
TypeError on dict unpacking (CodeRabbit).
- Remove redundant inner `import os` in _strict_validation_enabled; os is
already imported at module top (Greptile).
Co-authored-by: Mervin Praison <[email protected]>
- _normalize_yaml_config: detect duplicate agent/task names and unknown
task agent refs; raise under PRAISONAI_VALIDATE_STRICT or warn loudly and
preserve both entries by suffixing, instead of silently dropping.
- _resolve_effective_tool_timeout: apply the tightest (min) declared
tool_timeout to the shared tool dict and warn on any overridden agent,
so a fast agent is never silently forced to wait a slower agent's budget.
- BasicSkillMutator._is_safe_path: allow nested skill files (references/,
scripts/, assets/) while still rejecting absolute paths and parent
traversal; real containment stays enforced by the caller's resolve().
Co-authored-by: MervinPraison <[email protected]>
- execution_mixin: nest sync_display_callbacks restore fully under the
status_tool_callback identity check so a finishing agent can't clobber a
concurrent agent's live callback (3+ verbose-TTY agents race).
- agents: shallow-copy self.variables into task.variables so tasks can't
mutate shared AgentTeam state.
- agents: add async-aware _arun_task_start_hook/_arun_task_complete_hook that
await coroutine lifecycle callbacks and offload sync ones to the executor,
so arun_task never blocks the loop or silently drops an async hook.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: arun_task() now fires on_task_start/on_task_complete and propagates
self.variables, matching run_task(). Extracted _run_task_start_hook and
_run_task_complete_hook shared helpers so both sync and async paths stay in
sync (mirrors the _apply_task_guardrail helper pattern).
Gap 3: verbose+TTY status animation now swaps and restores the shared
sync_display_callbacks['tool_call'] entry under main.py's existing
_callbacks_lock, with an identity check on restore so concurrent verbose
agents don't clobber each other's entry.
Gap 2 (Memory adapter-protocol bypass) intentionally deferred: routing 34
vendor-specific call sites through the protocol is a large architectural
refactor with high regression risk, out of scope for a minimal fix.
Co-authored-by: Mervin Praison <[email protected]>
Deep-copy of formatted tool schemas now only runs when a
BEFORE_TOOL_DEFINITIONS hook is actually registered, matching the
has_hooks fast-path idiom used by every other hook event.
Co-authored-by: MervinPraison <[email protected]>
Addresses Greptile review: on_agent_start/aon_agent_start constructed a
ConversationSession eagerly on every call, reading agent.name/agent.agent_id
and building an object discarded on resume. Move construction into the
build_session factory (create-branch only) while capturing the created
instance for identical cache identity/timestamps.
Co-authored-by: Mervin Praison <[email protected]>
Deduplicate the create-or-resume session logic that PraisonAIDB and
PersistenceOrchestrator each reimplemented across their sync and async
surfaces. A new internal helper persistence/conversation/_ops.py owns the
store-level flow; both classes call it while keeping their distinct
return-type contracts (DbMessage vs ConversationMessage), caching, and
lock/cooldown machinery. Behaviour-preserving.
Co-authored-by: MervinPraison <[email protected]>
The pre-existing test_windows_scheduled_task_command_is_well_formed asserted
`--config` appeared inline in the /TR value. This PR intentionally routes the
scheduled task at the generated .cmd wrapper (single, well-formed quoting) to
avoid the nested-quote hazard for paths with spaces, so /TR now points at the
wrapper path and the wrapper owns the --config invocation. Assert the new,
intended contract instead of the outdated implementation detail.
Co-authored-by: Mervin Praison <[email protected]>
The installed daemons run python -m praisonai_bot gateway start, whose Typer command discarded GatewayHandler.start()'s int return. Typer ignores a returned int, so the process always exited 0 - the generated units' Restart=on-failure / RestartPreventExitStatus=78 / KeepAlive.SuccessfulExit directives never saw the real fatal-config (78) / transient (75) code, making the whole feature inert. Surface it via typer.Exit.
Also replace the Windows scheduled-task's fragile inline cmd /c string (nested quotes that misparse paths under C:\Program Files) with a single quoted path to the generated .cmd wrapper, which already owns the exit-78 translation.
Co-authored-by: Mervin Praison <[email protected]>
Installed supervisor units restarted the gateway unconditionally, so the
runtime's fatal-config exit (78, EX_CONFIG) was discarded and a broken
config crash-looped forever. Map the existing core exit-code contract to
per-OS restart directives:
- systemd: Restart=on-failure + RestartPreventExitStatus=78 +
StartLimitBurst=5 (bounded restarts).
- launchd: KeepAlive dict (SuccessfulExit=false, Crashed=true) +
ThrottleInterval so a clean/fatal exit is not relaunched every few
seconds.
- windows: wrap the scheduled task / startup script so exit 78 maps to a
clean stop instead of a relaunch.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer P1 findings on `praisonai usage`:
- Read current-project and global stores separately so `--by project`
no longer collapses both sources into a single `current` bucket.
- Raise per-store scan limit (1000 -> 100_000) so large stores are not
silently truncated (also affects `--days 0`).
- Surface store-open/read failures via warnings (table) and an `errors`
field (JSON) instead of reporting them as empty usage.
Co-authored-by: Mervin Praison <[email protected]>
- Honor PRAISONAI_HOME in legacy-session migration lookup via
get_sessions_dir() instead of a hardcoded ~/.praisonai/sessions path.
- Gate legacy name-only fallback to global scope only, so a workspace-scoped
run never silently adopts another project's history (isolation, #3154).
- Resolve workspace identity per-directory (cache keyed by cwd) so a
long-lived process that changes directory gets the correct identity while
still avoiding repeated git calls per directory.
- Add tests for directory-change identity and global-scoped legacy fallback.
Co-authored-by: Mervin Praison <[email protected]>
Strict-mode toolset resolution flattened ToolResolutionError (a ValueError
subclass) into a plain ValueError, dropping .unknown/.suggestions. Re-raise
the typed error before the generic handler and export the resolver API from
praisonaiagents.tools so strict callers can catch it.
Co-authored-by: Mervin Praison <[email protected]>
Aggregates already-persisted per-session total_tokens/cost from the local
session store(s) and reports by day/model/project with --days/--project/--json
filters. Wrapper-only reporting surface; no core change, no new deps.
Co-authored-by: MervinPraison <[email protected]>
Same-named agents in different projects no longer share conversation
history by default. The auto-derived session id now folds in a stable
workspace/project component (git root-commit, else resolved cwd, else
global) instead of being a function of the agent name alone.
- Add praisonaiagents/session/workspace.py with workspace_id() resolver
- Derive auto id as history_<sha256("{workspace}:{name}")[:8]>
- Keep name-only sha256 + md5 fallbacks so prior sessions still resolve
- Add PRAISONAI_GLOBAL_SESSIONS=true opt-out for name-only continuity
Co-authored-by: MervinPraison <[email protected]>
Address two greptile P1 findings on PR #3150:
- command_parser: broaden unsafe-expansion detector to match bare $VAR
(e.g. `rm -rf $HOME`) in addition to $IFS/${...}; $(...) and backticks
remain excluded so command substitution stays on the deny-checked
decomposition path.
- memory.reset_long_term: re-raise on Chroma delete_collection failure so
a failed reset is not silently reported as success while long-term
memories remain available.
Co-authored-by: Mervin Praison <[email protected]>
- permissions: escalate shell commands containing unresolvable parameter
expansion ($IFS / ${...}) to ASK instead of letting a broad allow rule
short-circuit a specific deny. Command substitution ($()/backticks) is
left to the existing per-op decomposition.
- llm: _setup_event_tracking now no-ops on empty events and merges into the
process-global litellm.callbacks (tracking per-instance ownership) instead
of overwriting it, so unrelated agents' callbacks are preserved.
- memory: reset_long_term() deletes only this instance's own Chroma
collection rather than resetting the entire shared client; collection name
is now an opt-in config override (default unchanged).
Co-authored-by: Mervin Praison <[email protected]>
2026-07-18 07:24:47 +00:00
536 changed files with 19394 additions and 6946 deletions
Loaded 100 of 536 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.