Skip to content

feat(providers): chat tool-calling via openclaude + explicit provider mode#106

Closed
sistemabritto wants to merge 20 commits into
evolution-foundation:mainfrom
sistemabritto:feat/chat-openclaude-provider-routing
Closed

feat(providers): chat tool-calling via openclaude + explicit provider mode#106
sistemabritto wants to merge 20 commits into
evolution-foundation:mainfrom
sistemabritto:feat/chat-openclaude-provider-routing

Conversation

@sistemabritto

@sistemabritto sistemabritto commented Jun 12, 2026

Copy link
Copy Markdown

Summary

This PR makes non-Anthropic providers actually work across EvoNexus — agent chat, terminal, and image generation — and adds NVIDIA NIM as a first-class provider for text/code (LLM) and image (FLUX).

Why NVIDIA: build.nvidia.com offers a free tier with generous credits across strong open models (DeepSeek, Qwen, Llama, StepFun for text; BFL FLUX.1/FLUX.2 for images) behind an OpenAI-compatible endpoint. For day-to-day operation this gives every EvoNexus user a zero-cost third leg to triangulate between Anthropic and OpenRouter: when one provider rate-limits or runs out of credits mid-workflow, switching providers in the dashboard now actually works — which is the bug this PR fixes.

Problems fixed

1. Agent chat silently bypassed openclaude (root cause of "switching providers does nothing")

chat-bridge.js routed every non-Anthropic provider to a raw fetch on /chat/completions:

  • No tool calling — agents became text-only chatbots (no Read/Bash/skills)
  • No conversation history — each turn sent only [system, user]
  • Codex OAuth broke deterministically — the path required an API key, and provider-config.js deletes OPENAI_API_KEY for codex_auth

Fix: external providers in code mode now go through @anthropic-ai/claude-agent-sdk with pathToClaudeCodeExecutable pointing at the openclaude binary and a clean whitelisted env (mirrors ClaudeBridge). Tool approval UI, streaming, session resume, and subagents all work unchanged. Chat-only models keep the REST path as fallback.

2. Terminal blocked agentic models by name heuristic

isCodeModel() gated the terminal on the model name matching a code-ish regex — openrouter/owl-alpha, stepfun-ai/step-3.7-flash etc. were refused. Fix: explicit per-provider mode: "code" | "chat" field in providers.json that overrides the heuristic.

3. openclaude >=0.18 requires NVIDIA_API_KEY for NIM endpoints

With only OPENAI_API_KEY set the CLI exits 1 ("Claude Code process exited with code 1" in the chat UI). Fix: loadProviderConfig() derives NVIDIA_API_KEY from OPENAI_API_KEY when the base URL is *.nvidia.com — the UI keeps a single key field.

4. Saving provider config wiped stored secrets

The Configure modal submits blank password inputs when the user edits other fields (e.g. model) — update_provider_config accepted the empty string and erased the saved key, breaking every subsequent session with auth errors. Fix: empty values no longer overwrite stored *KEY/*SECRET/*TOKEN vars.

5. Terminal agent personas resolved from the wrong directory

claude-bridge.js read .claude/agents/{agent}.md from the session cwd; sessions outside the workspace root silently fell back to a generic persona. Fix: resolve from WORKSPACE_ROOT with cwd fallback — same fix chat-bridge got in cab8966.

6. ai-image-creator: NVIDIA NIM image provider (FLUX)

  • --provider nvidia with three models: flux.2-klein-4b (default — best composition, ~2.5s), flux.1-dev (30 steps), flux.1-schnell (fastest). Keywords auto-select the provider.
  • Aspect ratios mapped to the exact dimension set + 1.06MP pixel budget the API validates (422 otherwise).
  • Per-model cfg_scale/steps defaults — the API schemas differ per model (schnell requires cfg=0, klein requires cfg>=1).
  • JPEG output auto-converted to PNG (ImageMagick -> ffmpeg fallback; temp files beside the output because snap-confined ffmpeg cannot read /tmp).
  • Honest docs: FLUX text rendering (PT-BR accents especially) is hit-or-miss — for text-critical art use multiple candidates or gemini/gpt5.

7. make telegram failed silently

The target printed a success message even when screen/bun were missing. Now it fails loudly with install instructions.

Verification (all live, not simulated)

  • npm test in terminal-server: 7/7 pass (5 new tests for the mode override)
  • Chat E2E over a real WebSocket against a running server: agent persona + NVIDIA (stepfun-ai/step-3.7-flash) -> full event stream (tool_use_start, tool_input_delta, thinking_delta, result: success)
  • Tool-calling E2E via OpenRouter: model invoked Read, result returned, all UI events emitted
  • Terminal PTY: openclaude 0.18 boots with the NVIDIA banner, no auth error
  • Image generation: PT-BR Instagram post at 832x1024 (4:5) via flux.2-klein-4b in 3.4s, PNG verified with file; flux.1-dev and flux.1-schnell keywords verified

Notes for reviewers

  • providers.example.json gains the mode field and an NVIDIA NIM entry; user providers.json is untouched/gitignored.
  • The chat REST fallback path is preserved for genuinely chat-completion-only models.
  • All NVIDIA constraints (dimension enum, pixel budget, per-model cfg ranges) were discovered by probing the live API — they are documented in code comments.

🤖 Generated with Claude Code

DavidsonGomes and others added 7 commits June 12, 2026 06:14
…volution-foundation#52)

Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate
limiting. Add flask-limiter (in-memory, single-process MVP) with:
- 60 req/min/IP on view_share (Vault §2.S1)
- Global default 600 req/min on all other routes (non-blocking baseline)
- Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options
  headers on every public share response (Vault §2.S2)

The Limiter singleton lives in rate_limit.py to break the circular-import
chain between app.py (which imports route blueprints) and the blueprints
that need @limiter.limit() decorators.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ortals (evolution-foundation#53)

* feat(security): rate-limit public share endpoint + security headers

Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate
limiting. Add flask-limiter (in-memory, single-process MVP) with:
- 60 req/min/IP on view_share (Vault §2.S1)
- Global default 600 req/min on all other routes (non-blocking baseline)
- Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options
  headers on every public share response (Vault §2.S2)

The Limiter singleton lives in rate_limit.py to break the circular-import
chain between app.py (which imports route blueprints) and the blueprints
that need @limiter.limit() decorators.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(plugins): B2.0 public_pages capability — read-only token-bound portals

Add the public_pages plugin capability (B2.0 scope, read-only):

plugin_schema.py:
- Add Capability.public_pages and Capability.safe_uninstall enum values
- Add PluginPublicPageTokenSource + PluginPublicPage Pydantic models
  (bundle must be under ui/public/, revoked_when disallowed in v1 to prevent SQL injection)
- Extend ReadonlyQuery with public_via + bind_token_param fields
- Add 4 PluginManifest model validators: capability required, table slug-prefix,
  unique ids/route_prefixes, readonly_data references valid page

routes/plugin_public_pages.py (new):
- GET /p/<slug>/<route_prefix>/<token>       — serve portal bundle (60 req/min/IP)
- GET /p/<slug>/<route_prefix>/<token>/data  — serve token-bound readonly query (120/min)
- GET /p/<slug>/public-assets/<path>         — serve ui/public/ static assets
- Token validation via parametric SQL (identifiers validated at install by schema)
- Module-level _PLUGIN_PUBLIC_PREFIXES cache for install/uninstall lifecycle
- Vault §B2.S2: Referrer-Policy, Cache-Control no-store, HSTS, X-Content-Type-Options on all responses
- CSP: default-src 'self' on portal bundles
- Rate limiting via rate_limit.limiter (imported from PR evolution-foundation#52)

app.py:
- Import and register plugin_public_pages_bp
- /p/... paths already bypass auth_middleware (non-/api/ paths are passthrough)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…vation, sandboxed hook (evolution-foundation#54)

- plugin_schema.py: PluginSafeUninstall, PluginPreUninstallHook, PluginUserConfirmation
  models + validators (block_uninstall enforcement, preserved_tables slug-prefix check,
  safe_uninstall_enabled_requires_confirmation, no _orphan_* refs in readonly SQL)
- routes/plugins.py: uninstall gate (admin-only, confirmation_phrase, exported_at check,
  zip_password); sandboxed pre-hook subprocess (no secrets, read-only DB copy, 600s timeout);
  cascade-DELETE filtering for preserved_host_entities; orphan table rename
  (_orphan_{slug}_{table}); EVONEXUS_ALLOW_FORCE_UNINSTALL=1 escape hatch with audit;
  reinstall SHA256 check against plugin_orphans before orphan recovery
- app.py: plugin_orphans table migration (id, slug, tablename, orphaned_at,
  orphaned_by_user_id, original_plugin_version, original_sha256, original_publisher_url,
  recovered_at, UNIQUE(slug, tablename))
- PluginUninstall.tsx: 3-step wizard (regulatory reason+checkbox → ZIP password → typed
  phrase); force-uninstall orange banner; integrated in PluginDetail.tsx
- docs/plugin-contract.md: full plugin.yaml contract for public_pages + safe_uninstall

Vault §B3 mitigations: S1 block_uninstall gate, S2 admin enforcement, S3 sandboxed hook,
S4 no _orphan_* SQL refs, S5 AES-256 ZIP password, S6 force-uninstall audit trail.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er auto-injection (evolution-foundation#55)

Two related Wave 2.1.x extensions to the plugin contract that close the last
two gaps blocking endpoint-level RBAC for plugin authors (gap inventory in
evonexus-plugin-nutri Step 3 RBAC decision).

Changes
- PluginWritableResource.requires_role: Optional[List[str]] — when set, the
  POST/PUT/DELETE handler returns 403 if current_user.role is not in the list.
  'admin' role always passes (super-user override). Backwards compatible:
  resources without the field accept any authenticated user (legacy default).
  Validator enforces kebab-case role names (^[a-z][a-z0-9-]*$).

- routes.plugins.writable_data: enforces requires_role at the endpoint, with
  a 403 message naming the required roles and the actor's current role.

- routes.plugins.readonly_data: auto-injects :current_user_id and
  :current_user_role as bind params on every readonly query. Plugins can
  reference them directly in SQL for server-enforced scoping without an
  app-layer wrapper. The two parameter names are reserved — client requests
  carrying them in the query string get 400 (no identity spoofing).

Tests
- tests/backend/test_plugins_rbac_and_scoping.py — 13 cases covering Pydantic
  acceptance/rejection, 403/200 paths for writable, scoping/spoofing for readonly,
  backwards compat for resources without requires_role and queries without
  :current_user_id refs.

Compat
- Existing plugins (PM Essentials) continue to work unchanged — the new field
  defaults to None and the auto-injected bind params are silently ignored if
  the SQL doesn't reference them.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ser, raw bundle for clients (evolution-foundation#56)

The portal_page handler at /p/{slug}/{route_prefix}/{token} previously served
the plugin's JS bundle with mimetype application/javascript regardless of the
caller. Browsers navigating to a portal URL saw the JS source instead of a
rendered page. Discovered during evonexus-plugin-nutri Step 5.

Changes
- portal_page: when the Accept header includes text/html and NOT
  application/javascript, render a minimal HTML shell that loads the bundle
  as <script type="module" src="/p/{slug}/public-assets/{file}"> and
  instantiates the plugin's declared custom_element_name. Token reaches the
  element via data-token attribute (no need for the bundle to re-parse
  window.location).
- _serve_html_shell: new helper. Defensive: validates bundle path is inside
  ui/public/ and custom_element_name matches alphanum-dash before emitting.
  Sets X-Content-Type-Options: nosniff + a tight CSP (default-src 'self',
  frame-ancestors 'none', no external scripts).
- Programmatic clients (curl, fetch with Accept: application/javascript)
  keep getting the raw bundle — backwards compatible.
- Bundle is fetched from the existing /p/{slug}/public-assets/{path} route
  (no token), which is safe because the bundle contains no patient data —
  data lives behind the token-gated /data endpoint.

Tests
- tests/backend/test_plugin_public_pages_html_shell.py — 9 cases:
  HTML accept renders shell, JS accept returns bundle, default Accept (*/*)
  returns bundle, invalid token returns 404 even with HTML accept, CSP +
  X-Content-Type-Options present, custom element name appears exactly once,
  XSS-safe (single <script> tag, token only inside data-token), legacy
  programmatic fetches unchanged.

Compat
- Existing public-page consumers using fetch() with Accept: application/json
  or default see no behaviour change. Plugins that already shipped a bundle
  start rendering correctly in browsers without any plugin update.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…volution-foundation#57)

Bumps version to 0.33.0 so the plugin nutri (which requires this version)
can install. Bundles the five plugin-contract PRs (evolution-foundation#52evolution-foundation#56) merged today
into a single release. Plus a UX fix on the install wizard so 409s say why
they conflicted.

The fix
- lib/api.ts buildError now falls back to data.conflicts[0] when the
  standard error/message fields are absent. The plugin preview endpoint
  returns {conflicts: string[], manifest, ...} on 409 — without this fix
  the wizard showed only "409 CONFLICT" with the actual reason hidden.
- PluginInstallModal: conflicts type was Record<string, unknown>, backend
  always returned string[]; the JSON.keys() coercion produced index strings.
  Now typed as string[] and rendered as a list.

Tested
- Frontend tsc --noEmit clean
- Plugin nutri 200 pytest still pass after the 11 `# nosec B603` markers
  added to subprocess.run calls (false positives from regex security scan —
  all calls use list args, no shell=True)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… mode

Non-Anthropic providers in the agent chat previously bypassed openclaude
entirely — a raw /chat/completions fetch with no tool calling, no session
history, and a hard API-key requirement that broke Codex OAuth. The
terminal was separately blocked by the isCodeModel() name heuristic for
agentic models like openrouter/owl-alpha.

- chat-bridge: external providers in code mode now go through the Agent
  SDK with pathToClaudeCodeExecutable pointing at the openclaude binary
  and a clean whitelisted env (mirrors ClaudeBridge). Restores tool
  calling, structured streaming, UI tool approval, and session resume.
  Chat-only models keep the REST path; missing binary falls back with a
  clear log.
- chat-bridge: external providers get full system-prompt replacement for
  agent personas (append is too weak for GPT models), matching the
  terminal behavior.
- provider-config: new per-provider `mode: code|chat` field overrides the
  model-name heuristic; the terminal no longer refuses agentic models
  whose names don't match the code regex.
- providers.example.json: mode field on openrouter/omnirouter + new
  NVIDIA NIM provider entry (OpenAI-compatible endpoint).
- tests: provider-config mode override coverage (node --test).

Verified end-to-end against OpenRouter: text streaming and Read tool
call complete with all UI events (tool_use_start, tool_input_delta,
block_stop, result).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @sistemabritto, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

OpenClaude >=0.18 detects integrate.api.nvidia.com and demands the key
in NVIDIA_API_KEY, exiting 1 when only OPENAI_API_KEY is set ("Claude
Code process exited with code 1" in the chat). Derive NVIDIA_API_KEY
from OPENAI_API_KEY in loadProviderConfig so the UI keeps a single key
field, and allowlist the var in both config layers.

Verified: chat session via Agent SDK + openclaude against NVIDIA NIM
(stepfun-ai/step-3.7-flash) completes with result: success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New security issues found

# EvoNexus uses the shared evonexus.db for all plugin tables (no per-plugin DB).
conn = _get_db()
try:
row = conn.execute(sql, (token,)).fetchone()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

Comment on lines +843 to +845
_recovery_conn.execute(
f"ALTER TABLE {_orphan_table_name} RENAME TO {_orig_table}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

}
if _orphan_table_name in _existing:
_recovery_conn.execute(
f"ALTER TABLE {_orphan_table_name} RENAME TO {_orig_table}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.django.security.injection.tainted-sql-string): Detected user input used to manually construct a SQL string. This is usually bad practice because manual construction could accidentally result in a SQL injection. An attacker could use a SQL injection to steal or modify contents of the database. Instead, use a parameterized query which is available by default in most database engines. Alternatively, consider using the Django object-relational mappers (ORM) instead of raw SQL queries.

Source: opengrep

}
if _orphan_table_name in _existing:
_recovery_conn.execute(
f"ALTER TABLE {_orphan_table_name} RENAME TO {_orig_table}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.flask.security.injection.tainted-sql-string): Detected user input used to manually construct a SQL string. This is usually bad practice because manual construction could accidentally result in a SQL injection. An attacker could use a SQL injection to steal or modify contents of the database. Instead, use a parameterized query which is available by default in most database engines. Alternatively, consider using an object-relational mapper (ORM) such as SQLAlchemy which will protect your queries.

Source: opengrep

Comment on lines +1355 to +1362
_proc = subprocess.run(
["python3", str(_hook_script_path)],
cwd=str(plugin_dir),
env=_hook_env,
capture_output=True,
text=True,
timeout=_hook_timeout,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.flask.security.injection.subprocess-injection): Detected user input entering a subprocess call unsafely. This could result in a command injection vulnerability. An attacker could use this vulnerability to execute arbitrary commands on the host, which allows them to download malware, scan sensitive data, or run any command they wish on the server. Do not let users choose the command to run. In general, prefer to use Python API versions of system commands. If you must use subprocess, use a dictionary to allowlist a set of commands.

Source: opengrep


try:
_proc = subprocess.run(
["python3", str(_hook_script_path)],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.dangerous-subprocess-use): Detected subprocess function 'uninstall_plugin' with user controlled data. A malicious actor could leverage this to perform command injection. You may consider using 'shlex.escape()'.

Source: opengrep

# the preservation clause comes from the manifest (validated at install).
_preserve_clause = _preserved_host_entities[_tbl]
_where = f"(source_plugin = ?) AND NOT ({_preserve_clause})"
conn.execute(f"DELETE FROM {_tbl} WHERE {_where}", (slug,))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

# the preservation clause comes from the manifest (validated at install).
_preserve_clause = _preserved_host_entities[_tbl]
_where = f"(source_plugin = ?) AND NOT ({_preserve_clause})"
conn.execute(f"DELETE FROM {_tbl} WHERE {_where}", (slug,))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.flask.security.injection.tainted-sql-string): Detected user input used to manually construct a SQL string. This is usually bad practice because manual construction could accidentally result in a SQL injection. An attacker could use a SQL injection to steal or modify contents of the database. Instead, use a parameterized query which is available by default in most database engines. Alternatively, consider using an object-relational mapper (ORM) such as SQLAlchemy which will protect your queries.

Source: opengrep

_orphan_name = f"_orphan_{slug}_{_orig_table}"
try:
# Rename to orphan name
_orphan_conn.execute(f"ALTER TABLE {_orig_table} RENAME TO {_orphan_name}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

_orphan_name = f"_orphan_{slug}_{_orig_table}"
try:
# Rename to orphan name
_orphan_conn.execute(f"ALTER TABLE {_orig_table} RENAME TO {_orphan_name}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.flask.security.injection.tainted-sql-string): Detected user input used to manually construct a SQL string. This is usually bad practice because manual construction could accidentally result in a SQL injection. An attacker could use a SQL injection to steal or modify contents of the database. Instead, use a parameterized query which is available by default in most database engines. Alternatively, consider using an object-relational mapper (ORM) such as SQLAlchemy which will protect your queries.

Source: opengrep

sistemabritto and others added 12 commits June 12, 2026 10:37
Extends the NVIDIA provider beyond chat/code into image generation, and
fixes three bugs found while dogfooding the multi-provider flow:

- ai-image-creator: NVIDIA NIM provider with FLUX models — flux.2-klein-4b
  (default, best composition, ~2.5s), flux.1-dev (30 steps), flux.1-schnell.
  Aspect-ratio mapping constrained to the dimension set and 1.06MP pixel
  budget the API validates; per-model cfg_scale/steps defaults (schnell
  requires cfg=0, klein requires cfg>=1); JPEG output auto-converted to
  PNG via ImageMagick with ffmpeg fallback (temp files beside the output —
  snap-confined ffmpeg cannot read /tmp). Uses NVIDIA_API_KEY from .env.

- backend/providers: saving provider config with a blank key field no
  longer wipes the stored secret — the UI submits empty password inputs
  when editing other fields (e.g. model), silently erasing the API key
  and breaking every session afterwards with auth errors.

- claude-bridge: terminal agent personas now resolve from WORKSPACE_ROOT
  with cwd fallback, matching chat-bridge (cab8966) — sessions started
  outside the workspace root were silently falling back to a generic
  "You are the X agent" persona.

- Makefile: `make telegram` fails loudly when screen/bun are missing
  instead of printing a false success message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le providers

Enforce a minimum interval between chat completion requests and retry
429/503 responses with exponential backoff, honoring Retry-After.
Configurable via CHAT_MIN_INTERVAL_MS, CHAT_MAX_RETRIES, CHAT_BASE_DELAY_MS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
onclose only cleared the keepalive and never reconnected — a dropped
socket left the terminal/chat dead until the component remounted
(switching tabs). Now both components reconnect with capped exponential
backoff and rejoin immediately on visibilitychange, replaying the
session buffer on a cleared terminal to avoid duplicate output. If the
process died while disconnected, surface it instead of silently
restarting the agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r fallback

- Pin the CLI conversation to the terminal-server session UUID:
  first start uses --session-id, restarts use --resume when a persisted
  conversation file exists — provider crashes and server restarts no
  longer lose the conversation.
- Per-agent model tiers: agents declare model: opus|sonnet|haiku in
  frontmatter; providers.json maps each tier to a provider model via
  the new model_tiers field.
- Pass --fallback-model from the new fallback_models chain (first entry
  distinct from the primary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… totals

Older skill versions logged image cost entries without
token_usage.total_tokens — the unguarded access in the Image Generation
table threw a TypeError and unmounted the whole page. Normalize the
image-costs payload defensively, same pattern as normalizeCostData.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eator

New 'openai' provider hitting api.openai.com/v1/images/generations
directly, with the 'image2' model keyword (auto-detects provider).
Requires a platform API key (AI_IMG_CREATOR_OPENAI_KEY or
OPENAI_API_KEY) — ChatGPT Plus/Codex OAuth tokens lack the
api.model.images.request scope (verified: 401), so they cannot be used
for image generation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ude 0.18

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e cost estimate

Images API bills text input and image output at different per-token
rates — use the prompt/completion split when available instead of
total_tokens at the input rate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reds-bearing test script

Top-level business folders (finance/, meetings/, social/, etc.) sit
outside the workspace/** rule and were trackable — never commit private
operational data, especially in a public PR. Also ignore local backups
and scripts/ghost_integration_test.py (has hardcoded Ghost API keys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dation

- Heartbeats can now run an in-process Python handler instead of
  spawning a Claude agent: new 'handler' field (max_turns=0), DB
  migration, schema validation, dispatcher sync, runner execution path.
- provider_fallback.py: 429/quota detection + model/provider rotation
  with cooldown tracking (foundation; not yet wired into the runner).

Tests: tests/heartbeats 25/26 pass (the 1 failure is a data-driven seed
assertion against the gitignored local heartbeats.yaml, not a regression).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sistemabritto

Copy link
Copy Markdown
Author

Substituído por #108 — rebaseado limpo no main e com escopo focado apenas nas melhorias genéricas (provider routing, resume, fallback, reconexão WebSocket, fixes). As skills específicas do workspace foram removidas da contribuição.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants