mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-02 19:58:46 +00:00
Prepare v7.0.0 with verified synthesis follow-ups
Preserve execution-limit causes and separate Telegram host notices from the assistant silent chain. Remove the obsolete unconditional continuation instruction, align reviewed schema pins, and retire duplicate extraction size policy. Correct browser test flow control and synchronize final release carriers. Publication awaits final exact-candidate tests, review convergence and CI. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
parent
d5418e05b8
commit
59ce693b36
19 changed files with 225 additions and 56 deletions
19
README.md
19
README.md
|
|
@ -12,7 +12,7 @@
|
|||
[](https://ouroboros-agent.ai/install/#linux)
|
||||
[][download-windows-x64]
|
||||
[](https://github.com/razzant/OuroborosHub)
|
||||
[](VERSION)
|
||||
[](VERSION)
|
||||
|
||||
Ouroboros is an open-source, general-purpose AI agent whose identity, durable memory, and history continue across tasks and restarts. It works on external projects, coordinates a live swarm of specialist agents, and can rewrite the implementation it runs on, including its code, architecture, prompts, tools, and dependencies. Reflection can also change how it understands itself without severing that continuity.
|
||||
|
||||
|
|
@ -64,13 +64,13 @@ The desktop packages already contain an optional CLI installer. On macOS, after
|
|||
|
||||
</details>
|
||||
|
||||
[download-macos-arm64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17.dmg
|
||||
[download-windows-x64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17-windows-x64.zip
|
||||
[download-linux-deb-amd64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/ouroboros_7.0.0-rc.17_amd64.deb
|
||||
[download-linux-rpm-x86_64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/ouroboros-7.0.0-rc.17-1.x86_64.rpm
|
||||
[download-linux-rpm-red80-x86_64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/ouroboros-7.0.0-rc.17-1.red80.x86_64.rpm
|
||||
[download-linux-appimage-x86_64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17-linux-x86_64.AppImage
|
||||
[download-linux-x86_64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17-linux-x86_64.tar.gz
|
||||
[download-macos-arm64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0.dmg
|
||||
[download-windows-x64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0-windows-x64.zip
|
||||
[download-linux-deb-amd64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0/ouroboros_7.0.0_amd64.deb
|
||||
[download-linux-rpm-x86_64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0/ouroboros-7.0.0-1.x86_64.rpm
|
||||
[download-linux-rpm-red80-x86_64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0/ouroboros-7.0.0-1.red80.x86_64.rpm
|
||||
[download-linux-appimage-x86_64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0-linux-x86_64.AppImage
|
||||
[download-linux-x86_64]: https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0-linux-x86_64.tar.gz
|
||||
|
||||
Ouroboros bundles [Claudexor](https://github.com/razzant/claudexor) as its local execution layer for delegated coding and hosted-agent review. Ouroboros owns the task, memory, review, and final integration, while Claudexor runs the selected connected coding harness and returns durable execution evidence. [Explore Claudexor](https://claudexor.ai/).
|
||||
|
||||
|
|
@ -449,8 +449,7 @@ and the reason.
|
|||
|
||||
| Version | Date | Description |
|
||||
|---------|------|-------------|
|
||||
| 7.0.0-rc.17 | 2026-09-08 | **Fix-forward candidate: make the cancellation E2E stub recognize multipart finalization markers consistently across provider wire shapes.** |
|
||||
| 7.0.0-rc.16 | 2026-09-08 | **Release candidate of 7.0.0 — reviewed model-wait and subscription routing, account-role controls, MCP decision-lane capability, safer owned-daemon restart custody, scratchpad/evidence disclosure, and cross-platform lock fixes. Claudexor is pinned to 3.10.0.** |
|
||||
| 7.0.0 | 2026-09-08 | **v7: a modular runtime with full ordinary-conversation tools, complete delegated inputs and visible owner dialogue.** Main and Project conversations retain their chosen tools and working folders; required owner waits preserve the live browser without occupying pooled execution capacity. Publish admission is visible in its intended chat, and GitHub/image/plan failures retain their actual causes. Planning advice stays optional; source-backed evidence, separate host notices and exact-workload test reuse preserve honest review without new approval machinery. Integrates the subscription model and account-role controls, owned startup/restart custody and cross-platform repairs, with Claudexor pinned to 3.10.1. |
|
||||
| 6.114.0 | 2026-09-01 | **feat: capability preservation as a first-class invariant, shape-first OpenRouter failover, and a green 3-OS matrix.** Capability preservation joins the immune system as a first-class invariant (#447, stages 1-3: golden capability suite, seam pins that actually differentiate, executable item-21 triggers, binaries judged by magic bytes). Same-model OpenRouter provider failover is now derived from the shape of the replayed reasoning artifact instead of a hard-coded model-family roster (#468): readable text/summary artifacts stay failover-eligible for every family, a bare `response_id` no longer pins, sealed artifacts (encrypted/signed/redacted, not vouched by the anthropic/gemini roster — `openai/` excluded on 2026-07 field evidence) keep the continuity pin on both the dispatch and reroute paths, and the pin reason plus the refusing upstream provider reach durable telemetry. Delegation and chat truth converge: delegated-run questions ride the escalation hierarchy (#204), the routing picker dispatches real routes (#198), typed owner quiz cards and the escalation channel (Q-2a/Q-2b), live-first owner deliveries, honest chat-activity conclusions with budget-pause resume, review findings in the task-card Reviews checkpoint, persistent stable-target registrations, a hold on the live delegated leaf when a nanny round dies unknown, and healed cross-generation custody disclosures. Rich chat stack (markdown, media, galleries, links — Andrei Kaznacheev), Chat viewport stability, Dashboard/Updates one-verdict action surface, Settings effect honesty, skill Repair affordance after preflight failure, node/npm launches through an execution-probed runtime ladder, the `ultra` reasoning-effort tier (@ndrew1337), and an O(delta) in-lock ledger read with a guarded torn-tail append boundary (@deebosh). Claudexor runtime pinned to 3.9.5 with delayed-startup reconciliation, a foreground quota-refresh bridge, and preserved delegated text fragments; Updates restart state survives same-SHA reconnects. CI: the serial service-log suites no longer race the child, web source pins are CRLF-safe on Windows, and the size-ratchet manifest is exact. This tag also carries the untagged 6.113.5 (generation-safe browser cleanup after timed-out tools, #440 audit fix-forward). |
|
||||
| 6.113.5 | 2026-08-31 | **fix: browser cleanup after a timed-out tool is generation-safe (integrates community PR #429 by @mikemikimike, closes #409).** A stateful-tool timeout now retires the whole browser generation: the shared state slot is replaced with a fresh object, the abandoned worker keeps writing only into its retired one, and the close is queued on the retiring executor so it always runs on the owning worker thread — including the already-settled race — with the cognitive lease closing after that cleanup. A late infrastructure-error retry that observes a replaced generation closes only its own retired session, so it can no longer cross-thread-kill the next command's browser. Closes the follow-up findings of the #440 post-merge audit; the never-settling-worker session leak stays a disclosed residual of in-process Playwright. |
|
||||
| 6.113.4 | 2026-08-30 | **fix: release verification follows the family-mounted sign-in card.** The browser acceptance tests now re-resolve the active per-family login host after every account-list rebuild, matching the placement introduced by the roster UI while preserving the full cancellation, reconciliation, stale-poll, and terminal-race assertions. No lifecycle check is weakened and the managed Claudexor runtime remains pinned to 3.9.2. |
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
7.0.0-rc.17
|
||||
7.0.0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Ouroboros v7.0.0-rc.17 — Architecture & Reference
|
||||
# Ouroboros v7.0.0 — Architecture & Reference
|
||||
|
||||
This file is NOT a changelog. Version history lives in README.md, git tags, and commit log.
|
||||
|
||||
|
|
@ -1437,7 +1437,7 @@ Read-only children can read/list the existing project-scoped knowledge store wit
|
|||
|
||||
**Scheduling.** `schedule_subagent` requires `subagent_id`, a focused `objective`, and `expected_output`; the remaining public fields describe child-local context, constraints, memory, capability needs, write surface, narrower deadline, delegation budget, and acceptance claims. There is no model-visible lane/executor axis and no public `effort` override: the selected row is the complete execution choice, lineage/bounds/route/budget are host-derived, and omission never inherits the parent's acceptance claims. `subagent_runtime.select_subagent_snapshot` copies an immutable snapshot of the exact enabled row into the child task; an `api_model` row becomes an ordinary recursive API child on that exact model/effort, an `agent_session` row an ordinary recursive Ouroboros nanny on that exact external route. The cash side of a burst — each sibling launched before the first sibling's first response pays its own prefix write on cache-write-priced routes — is disclosed to the mind in the tool description as an affordance and deliberately not scheduled by the host. The old lane/executor resolver serves only old durable records; for those historical lane envelopes, `schedule_subagent` reports the requested lane only — effective facts remain on the dispatched child record — and a task carrying `configured_subagent` goes straight to `subagent_runtime`, so legacy policy cannot reinterpret an active selection.
|
||||
|
||||
**Waiting on children.** `wait_task`/`get_task_result` return the full single-child handoff (verification receipts red/masked-first, exact omitted count); `wait_tasks` stays batch-compact: `task_id, status, cost_usd, child_result_sha256, outcome_axes, result, trace_summary, capability_delta when the child has something to disclose, duplicate_of`, plus the nullable cost-finality pair `accounted_upper_bound_usd`/`cost_final` and, when the child's envelope carries one, `execution_evidence` (§11.1). Both use `task_status.SETTLED_STATUSES`; a pending cancellation is the typed `cancel_state: "pending"` projection, never completion.
|
||||
**Waiting on children.** `wait_task`/`get_task_result` return the full single-child handoff (verification receipts red/masked-first, exact omitted count); `wait_tasks` stays batch-compact: `task_id, status, cost_usd, child_result_sha256, outcome_axes, result, terminal_host_notice when present, trace_summary, capability_delta when the child has something to disclose, duplicate_of`, plus the nullable cost-finality pair `accounted_upper_bound_usd`/`cost_final` and, when the child's envelope carries one, `execution_evidence` (§11.1). Both use `task_status.SETTLED_STATUSES`; a pending cancellation is the typed `cancel_state: "pending"` projection, never completion.
|
||||
|
||||
**What a delegated run costs.** Claudexor reports the amount in `summary.spendUsd` and its exactness in `summary.spendEstimated`; `delegate_custody.disclosed_spend` is the single reader of the pair, so the ledger row and the payload the nanny relays cannot tell different stories. Runs ask `authPreference: subscription` explicitly, because the engine default falls back to a paid key invisibly. Four cases:
|
||||
|
||||
|
|
@ -2071,7 +2071,7 @@ and failed source writes disclose `applied_source_status="unavailable"`.
|
|||
| `StateResponse.active_direct_turns`/`active_chat_activities` (phases queued/working/finalizing/budget_paused — one predicate `budget_pause_fact` decides budget pause); `TypingOutbound` activity fields; `ChatOutbound.task_phase`/`task_terminal_status` | `ouroboros/gateway/contracts.py`, `supervisor/active_activity.py`, `web/modules/chat_activity.js` | `tests/test_gateway_parity.py` + the activity test files |
|
||||
| `project_thread` stamp on all seven outbound frame types, stamped at the message-bus broadcast choke; a stamped frame is never adopted by Main (`chat_activity.mainThreadAccepts`) | `supervisor/message_bus.py`, `ouroboros/projects_registry.py` | `tests/test_message_bus.py`, `web/tests/chat_thread_routing.test.js` |
|
||||
| Media/link envelopes — media `task_id`/`size_bytes`/`download_url`; `LinkAction {label,url}` with at most twelve absolute HTTP(S) actions; `links` in `WS_MESSAGE_TYPES`; `chat.links` host topic | `ouroboros/gateway/contracts.py`, `ouroboros/tools/core.py`, `ouroboros/event_bus.py` | `tests/test_contracts.py` |
|
||||
| Owner quiz ABI — `QuizOption {label, detail?}`, `QuizOutbound` (quiz_id, question, options, stake, `assumption` (required for optional clarification), additive `wait_for_answer` for a managed root that must wait, lifecycle state open/answered/expired_terminal/superseded), separate `QuizStateOutbound` discriminator, `chat.quiz` host topic; the producer is the one escalation verb `escalate(question, options, stake, assumption, wait_for_answer=False)` — a ROOT asks the owner, a SUBAGENT delivers a typed frame to its nearest LIVE ancestor, which answers via `forward_to_worker` or escalates verbatim, so the owner sees only what no ancestor answered; answers arrive through the ONE ingress `POST /api/decisions` (family ids `quiz:{task_id}:{quiz_id}`, `routing:{client_message_id}:{routing_token}`; `interaction:` reserved), request-id idempotent, first answer wins, validated against the STORED options; `option_index` is optional for the quiz family alone — a comment-only answer writes NO `answered_index`, because a stored 0 would replay as "chose the first option"; injected as the typed `KIND_QUIZ_ANSWER` mailbox control and broadcast as `quiz_state` (carrying the recorded `comment` when the owner answered in their own words, so the live card shows `Owner's answer:` exactly as replay does); expiry is structural only (the task-done seam flips open quizzes to `expired_terminal`) and history replay merges the projection state | `ouroboros/gateway/contracts.py`, `ouroboros/gateway/task_decision.py`, `ouroboros/owner_quiz.py`, `ouroboros/tools/core.py` | `tests/test_gateway_parity.py`, `tests/test_quiz_display.py`, `tests/test_quiz_answer.py`, `web/tests/chat_decision.test.js` |
|
||||
| Owner quiz ABI — `QuizOption {label, detail?}`, `QuizOutbound` (quiz_id, question, options, stake, `assumption` (required for optional clarification), additive `wait_for_answer` for a live pooled or ordinary-conversation root that must wait, lifecycle state open/answered/expired_terminal/superseded), separate `QuizStateOutbound` discriminator, `chat.quiz` host topic; the producer is the one escalation verb `escalate(question, options, stake, assumption, wait_for_answer=False)` — a ROOT asks the owner, a SUBAGENT delivers a typed frame to its nearest LIVE ancestor, which answers via `forward_to_worker` or escalates verbatim, so the owner sees only what no ancestor answered; answers arrive through the ONE ingress `POST /api/decisions` (family ids `quiz:{task_id}:{quiz_id}`, `routing:{client_message_id}:{routing_token}`; `interaction:` reserved), request-id idempotent, first answer wins, validated against the STORED options; `option_index` is optional for the quiz family alone — a comment-only answer writes NO `answered_index`, because a stored 0 would replay as "chose the first option"; injected as the typed `KIND_QUIZ_ANSWER` mailbox control and broadcast as `quiz_state` (carrying the recorded `comment` when the owner answered in their own words, so the live card shows `Owner's answer:` exactly as replay does); expiry is structural only (the task-done seam flips open quizzes to `expired_terminal`) and history replay merges the projection state | `ouroboros/gateway/contracts.py`, `ouroboros/gateway/task_decision.py`, `ouroboros/owner_quiz.py`, `ouroboros/tools/core.py` | `tests/test_gateway_parity.py`, `tests/test_quiz_display.py`, `tests/test_quiz_answer.py`, `web/tests/chat_decision.test.js` |
|
||||
| Managed update ABI — preflight, `UpdateMergePlan`, pinned apply, `update_status_ready` WS notice | `ouroboros/gateway/contracts.py` | `tests/test_update_apply_routing.py` |
|
||||
| `ChatOutbound.review_projection` — bounded actor findings via `utils.truncate_review_artifact`, at most `MAX_PROJECTED_ACTOR_FINDINGS` rows (`review_execution_projection.py`) | `ouroboros/gateway/contracts.py` | `tests/test_review_substrate_v2.py`, `web/tests/review_truth.test.js` |
|
||||
| Skill preflight statuses — `preflight_failed` is fresh-only; a stale failure surfaces as `preflight_failed_stale`; absence means the caller could not know | `ouroboros/skill_review_status.py` | `tests/test_skill_preflight_repair.py`, `web/tests/skill_preflight_repair.test.js` |
|
||||
|
|
|
|||
|
|
@ -46,24 +46,24 @@
|
|||
<article>
|
||||
<p class="download-platform">macOS 12+ · Apple silicon only</p>
|
||||
<h2>macOS</h2>
|
||||
<a data-release-download="macos-arm64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17.dmg">Download for macOS (.dmg)</a>
|
||||
<a data-release-download="macos-arm64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0.dmg">Download for macOS (.dmg)</a>
|
||||
<p>Open the DMG, drag <code>Ouroboros.app</code> to Applications, then launch it from Applications.</p>
|
||||
</article>
|
||||
<article>
|
||||
<p class="download-platform">Windows x64</p>
|
||||
<h2>Windows</h2>
|
||||
<a data-release-download="windows-x64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17-windows-x64.zip">Download for Windows (.zip)</a>
|
||||
<a data-release-download="windows-x64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0-windows-x64.zip">Download for Windows (.zip)</a>
|
||||
<p>Extract the ZIP, open the <code>Ouroboros</code> folder, and run <code>Ouroboros.exe</code>.</p>
|
||||
</article>
|
||||
<article id="linux">
|
||||
<p class="download-platform">Linux x86_64</p>
|
||||
<h2>Linux</h2>
|
||||
<div class="download-actions">
|
||||
<a data-release-download="linux-deb-amd64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/ouroboros_7.0.0-rc.17_amd64.deb">Debian / Ubuntu / Astra (.deb)</a>
|
||||
<a data-release-download="linux-rpm-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/ouroboros-7.0.0-rc.17-1.x86_64.rpm">Fedora / RHEL (.rpm)</a>
|
||||
<a data-release-download="linux-rpm-red80-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/ouroboros-7.0.0-rc.17-1.red80.x86_64.rpm">RED OS 8 (.rpm)</a>
|
||||
<a data-release-download="linux-appimage-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17-linux-x86_64.AppImage">Portable AppImage</a>
|
||||
<a data-release-download="linux-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17-linux-x86_64.tar.gz">tar.gz archive</a>
|
||||
<a data-release-download="linux-deb-amd64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/ouroboros_7.0.0_amd64.deb">Debian / Ubuntu / Astra (.deb)</a>
|
||||
<a data-release-download="linux-rpm-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/ouroboros-7.0.0-1.x86_64.rpm">Fedora / RHEL (.rpm)</a>
|
||||
<a data-release-download="linux-rpm-red80-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/ouroboros-7.0.0-1.red80.x86_64.rpm">RED OS 8 (.rpm)</a>
|
||||
<a data-release-download="linux-appimage-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0-linux-x86_64.AppImage">Portable AppImage</a>
|
||||
<a data-release-download="linux-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0-linux-x86_64.tar.gz">tar.gz archive</a>
|
||||
</div>
|
||||
<p>Prefer the package for your distribution. Use the AppImage or archive on other distributions; Git must already be installed.</p>
|
||||
</article>
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
<section class="install-primary" aria-labelledby="macos-quick-start">
|
||||
<h2 id="macos-quick-start">macOS quick start</h2>
|
||||
<ol class="install-steps">
|
||||
<li>Click <a data-release-download="macos-arm64" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17.dmg">Download for macOS (.dmg)</a>.</li>
|
||||
<li>Click <a data-release-download="macos-arm64" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0.dmg">Download for macOS (.dmg)</a>.</li>
|
||||
<li>Open the DMG and drag <code>Ouroboros.app</code> onto the <strong>Applications</strong> shortcut.</li>
|
||||
<li>Open Ouroboros from Applications. If Gatekeeper asks, right-click the app and choose <strong>Open</strong>.</li>
|
||||
</ol>
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ def _quiz_answer_frame(
|
|||
lines.append(f"The owner chose option {option_index + 1}: {label}")
|
||||
if comment:
|
||||
lines.append(f"Owner comment (verbatim): {comment}")
|
||||
if str(block.get("assumption") or ""):
|
||||
if str(block.get("assumption") or "") and not block.get("wait_for_answer"):
|
||||
lines.append(
|
||||
f"You continued under the assumption: {block.get('assumption')} — "
|
||||
"judge yourself whether work has moved past the answered fork."
|
||||
|
|
|
|||
|
|
@ -540,6 +540,15 @@ def _maybe_early_finalize(
|
|||
limit_ctx, controls["finalize_deadline_ts"],
|
||||
)
|
||||
return _loop()._handle_forced_finalization(limit_ctx, str(controls["finalize_now"]))
|
||||
# A completed-tool wait may expire without a mailbox control. Preserve
|
||||
# that existing execution bound before a soft-budget finalizer labels it.
|
||||
from ouroboros.model_wait import ModelWaitInterrupted
|
||||
|
||||
waiter = getattr(tools._ctx, "model_wait_context", None)
|
||||
reason = waiter.control_reason() if waiter is not None else None
|
||||
if reason in {"absolute_ceiling", "execution_deadline"}:
|
||||
return _handle_model_wait_control(limit_ctx, ModelWaitInterrupted(reason),
|
||||
transport_episode=transport_episode)
|
||||
# An active episode owns the deadline sliver: its last free redial +
|
||||
# no-resend terminal replace the paid deadline_local finalize call.
|
||||
if transport_episode is None:
|
||||
|
|
|
|||
|
|
@ -176,8 +176,7 @@ fenced `mermaid` and `chart` blocks, Markdown tables, and LaTeX natively, so
|
|||
diagrams and plots need no generated image files; produced files go through
|
||||
`send_file`/`send_photo`/`send_video`, and I never construct or guess a
|
||||
download URL — only a host-returned URL, repeated unchanged. `escalate` is for
|
||||
a genuine authority or product fork, not routine uncertainty: I state the
|
||||
assumption I keep working under and continue. `plan_task` is for load-bearing
|
||||
a genuine authority or product fork, not routine uncertainty. `plan_task` is for load-bearing
|
||||
decisions that would be expensive to reverse; cheap, reversible work does not
|
||||
need it.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "ouroboros"
|
||||
version = "7.0.0rc17"
|
||||
version = "7.0.0"
|
||||
description = "Self-creating AI agent with constitution, background consciousness, and persistent identity"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
|
|
|||
|
|
@ -46,24 +46,24 @@
|
|||
<article>
|
||||
<p class="download-platform">macOS 12+ · Apple silicon only</p>
|
||||
<h2>macOS</h2>
|
||||
<a data-release-download="macos-arm64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17.dmg">Download for macOS (.dmg)</a>
|
||||
<a data-release-download="macos-arm64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0.dmg">Download for macOS (.dmg)</a>
|
||||
<p>Open the DMG, drag <code>Ouroboros.app</code> to Applications, then launch it from Applications.</p>
|
||||
</article>
|
||||
<article>
|
||||
<p class="download-platform">Windows x64</p>
|
||||
<h2>Windows</h2>
|
||||
<a data-release-download="windows-x64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17-windows-x64.zip">Download for Windows (.zip)</a>
|
||||
<a data-release-download="windows-x64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0-windows-x64.zip">Download for Windows (.zip)</a>
|
||||
<p>Extract the ZIP, open the <code>Ouroboros</code> folder, and run <code>Ouroboros.exe</code>.</p>
|
||||
</article>
|
||||
<article id="linux">
|
||||
<p class="download-platform">Linux x86_64</p>
|
||||
<h2>Linux</h2>
|
||||
<div class="download-actions">
|
||||
<a data-release-download="linux-deb-amd64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/ouroboros_7.0.0-rc.17_amd64.deb">Debian / Ubuntu / Astra (.deb)</a>
|
||||
<a data-release-download="linux-rpm-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/ouroboros-7.0.0-rc.17-1.x86_64.rpm">Fedora / RHEL (.rpm)</a>
|
||||
<a data-release-download="linux-rpm-red80-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/ouroboros-7.0.0-rc.17-1.red80.x86_64.rpm">RED OS 8 (.rpm)</a>
|
||||
<a data-release-download="linux-appimage-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17-linux-x86_64.AppImage">Portable AppImage</a>
|
||||
<a data-release-download="linux-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17-linux-x86_64.tar.gz">tar.gz archive</a>
|
||||
<a data-release-download="linux-deb-amd64" class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/ouroboros_7.0.0_amd64.deb">Debian / Ubuntu / Astra (.deb)</a>
|
||||
<a data-release-download="linux-rpm-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/ouroboros-7.0.0-1.x86_64.rpm">Fedora / RHEL (.rpm)</a>
|
||||
<a data-release-download="linux-rpm-red80-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/ouroboros-7.0.0-1.red80.x86_64.rpm">RED OS 8 (.rpm)</a>
|
||||
<a data-release-download="linux-appimage-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0-linux-x86_64.AppImage">Portable AppImage</a>
|
||||
<a data-release-download="linux-x86_64" class="btn btn-quiet" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0-linux-x86_64.tar.gz">tar.gz archive</a>
|
||||
</div>
|
||||
<p>Prefer the package for your distribution. Use the AppImage or archive on other distributions; Git must already be installed.</p>
|
||||
</article>
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
<section class="install-primary" aria-labelledby="macos-quick-start">
|
||||
<h2 id="macos-quick-start">macOS quick start</h2>
|
||||
<ol class="install-steps">
|
||||
<li>Click <a data-release-download="macos-arm64" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0-rc.17/Ouroboros-7.0.0-rc.17.dmg">Download for macOS (.dmg)</a>.</li>
|
||||
<li>Click <a data-release-download="macos-arm64" href="https://github.com/razzant/ouroboros/releases/download/v7.0.0/Ouroboros-7.0.0.dmg">Download for macOS (.dmg)</a>.</li>
|
||||
<li>Open the DMG and drag <code>Ouroboros.app</code> onto the <strong>Applications</strong> shortcut.</li>
|
||||
<li>Open Ouroboros from Applications. If Gatekeeper asks, right-click the app and choose <strong>Open</strong>.</li>
|
||||
</ol>
|
||||
|
|
|
|||
|
|
@ -1056,7 +1056,8 @@ def _make_outbound(api):
|
|||
# formatting; an absent/True hint renders markdown→HTML as before.
|
||||
parse_mode = "" if event.get("markdown") is False else "HTML"
|
||||
|
||||
silent_on = _is_silent_mode_enabled(local_settings)
|
||||
# A host receipt must neither replace nor become the tracked agent reply.
|
||||
silent_on = _is_silent_mode_enabled(local_settings) and event.get("role") != "system"
|
||||
tracked_msg_id = _get_silent_msg(api, chat_id) if silent_on else 0
|
||||
|
||||
# Silent mode: try to edit the previously tracked message in-place.
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ def test_core_catalog_schema_bytes_and_handler_owners_are_stable():
|
|||
separators=(",", ":"),
|
||||
).encode()
|
||||
assert hashlib.sha256(schema_bytes).hexdigest() == (
|
||||
"5488b1842c13a8b0869277017d043df10fcc940145c778048e28cdc4703051bf"
|
||||
"7d7e86522b2413f1a13e41dfcf16bc167c60a7bc04df4a31fcd59c2ac2285208"
|
||||
)
|
||||
assert {
|
||||
entry.name: (entry.handler.__module__, entry.handler.__name__)
|
||||
|
|
|
|||
|
|
@ -84,3 +84,64 @@ def test_direct_root_can_request_wait_through_the_existing_quiz(tmp_path):
|
|||
assert event["type"] == "send_quiz" and event["wait_for_answer"] is True
|
||||
assert ctx._owner_wait_requested == event["quiz_id"]
|
||||
assert load_task_result(tmp_path, "native")["owner_quiz"][event["quiz_id"]]["wait_for_answer"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("required", [False, True])
|
||||
def test_answer_frame_describes_only_work_that_continued(required):
|
||||
from ouroboros.gateway.task_decision import _quiz_answer_frame
|
||||
|
||||
block = {"quiz_id": "q", "question": "Proceed?", "options": ["Yes", "No"],
|
||||
"assumption": "Keep the current layout", "wait_for_answer": required}
|
||||
frame = _quiz_answer_frame(block, 0, "Proceed with the saved form")
|
||||
assert "Proceed with the saved form" in frame
|
||||
assert ("You continued under the assumption" in frame) is not required
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cold", [False, True])
|
||||
def test_expired_wait_clock_precedes_post_tool_budget(tmp_path, monkeypatch, cold):
|
||||
"""A saved tail may have spent its budget, but an expired clock keeps its cause."""
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
from ouroboros import loop, model_wait, owner_wait, task_pacing
|
||||
from tests.test_owner_wait_cold_loop import cold_registry
|
||||
from tests.test_loop_transport_wait import _loop_kwargs
|
||||
|
||||
def no_network(*_args, **_kwargs):
|
||||
pytest.fail("synthetic wait must never contact a provider")
|
||||
monkeypatch.setattr(socket.socket, "connect", no_network)
|
||||
monkeypatch.setattr(socket.socket, "connect_ex", no_network)
|
||||
monkeypatch.setenv("OUROBOROS_TASK_ABS_CEILING_SEC", "21600")
|
||||
registry = cold_registry(tmp_path, monkeypatch, task_pacing.CostCeiling(state="active", ceiling_usd=9.0))
|
||||
ctx = registry._ctx
|
||||
ctx.is_direct_chat, ctx.current_chat_id, ctx.event_queue = True, 1, queue.Queue()
|
||||
calls = []
|
||||
|
||||
with model_wait.task_model_wait_scope(task={"id": ctx.task_id, "_is_direct_chat": True},
|
||||
drive_root=tmp_path, event_queue=ctx.event_queue, worker_slot_held=False) as controller:
|
||||
ctx.model_wait_context, controller.tool_context = controller, ctx
|
||||
|
||||
def expire_and_wait(context, checkpoint):
|
||||
controller.started_monotonic = time.monotonic() - 21601
|
||||
assert controller.control_reason() == "absolute_ceiling"
|
||||
owner_wait.direct_owner_wait(context, checkpoint)
|
||||
ctx.owner_wait_callback = expire_and_wait
|
||||
if not cold:
|
||||
ctx.owner_wait_resume, ctx._owner_wait_requested = None, ""
|
||||
|
||||
def dispatch(call, _disposition, **_kwargs):
|
||||
calls.append("ordinary_send")
|
||||
call.accumulated_usage["cost"] = 10.0
|
||||
return {"role": "assistant", "content": "", "tool_calls": [{
|
||||
"id": "ask", "type": "function", "function": {
|
||||
"name": "escalate", "arguments": json.dumps({"question": "Continue?",
|
||||
"options": [{"label": "Yes"}, {"label": "No"}], "wait_for_answer": True})}}]}, 0.0
|
||||
monkeypatch.setattr(loop, "_dispatch_round_model", dispatch)
|
||||
monkeypatch.setattr(loop, "_call_forced_model_once", lambda *_a, **_k: pytest.fail("expired clock entered a paid finalizer"))
|
||||
monkeypatch.setattr(loop, "_finish_tool_round_budget", lambda *_a, **_k: pytest.fail("expired clock entered budget tail"))
|
||||
_text, usage, trace = loop.run_llm_loop(**{
|
||||
**_loop_kwargs(tmp_path, registry, []), "event_queue": ctx.event_queue})
|
||||
|
||||
assert calls == ([] if cold else ["ordinary_send"])
|
||||
assert usage["reason_code"] != "budget_exhausted"
|
||||
assert trace["forced_finalization"]["control_reason"] == "absolute_ceiling"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import json
|
|||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
|
@ -22,6 +24,36 @@ local_form = form_fixture
|
|||
wait_clone = clone_fixture
|
||||
|
||||
|
||||
@contextmanager
|
||||
def chat_connection(server):
|
||||
"""Consume live frames like the UI; an unread client stalls WS flow control."""
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
from websockets.sync.client import connect
|
||||
|
||||
stopped, errors = threading.Event(), []
|
||||
with connect(ws_url(server), open_timeout=30, proxy=None) as ws:
|
||||
def receive():
|
||||
while not stopped.is_set():
|
||||
try:
|
||||
ws.recv(timeout=1)
|
||||
except TimeoutError:
|
||||
continue
|
||||
except ConnectionClosed as exc:
|
||||
if not stopped.is_set():
|
||||
errors.append(exc)
|
||||
return
|
||||
reader = threading.Thread(target=receive)
|
||||
reader.start()
|
||||
try:
|
||||
yield ws
|
||||
finally:
|
||||
stopped.set()
|
||||
ws.close()
|
||||
reader.join(5)
|
||||
assert not reader.is_alive()
|
||||
assert not errors, errors
|
||||
|
||||
|
||||
@pytest.mark.parametrize("surface", ["main", "project"])
|
||||
@pytest.mark.parametrize("answer_path", ["text", "quiz", "stop"])
|
||||
def test_native_owner_wait_retains_form_and_remains_addressable(
|
||||
|
|
@ -69,9 +101,7 @@ def test_native_owner_wait_retains_form_and_remains_addressable(
|
|||
project = _api(server.base_url, "POST", "/api/projects", {
|
||||
"name": "Native waiting", "path": str(folder),
|
||||
})["project"]
|
||||
from websockets.sync.client import connect
|
||||
|
||||
with connect(ws_url(server), open_timeout=30, proxy=None) as ws:
|
||||
with chat_connection(server) as ws:
|
||||
def submit(text, *, in_project=False):
|
||||
message_id = uuid.uuid4().hex
|
||||
ws.send(json.dumps({"type": "chat", "content": text, "client_message_id": message_id,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ Re-cut on the v7next tip: the reference's ``_ACCEPT_DELTA_CHILD_CAP`` and
|
|||
``_accept_capability_deltas`` rows are superseded — upstream re-homed the
|
||||
capability-delta aggregate into ``delegate_evidence`` (the facade reads it
|
||||
back at call time), so those two names are neither moved nor pinned here.
|
||||
Module size follows the shared size-ratchet tests and manifest; this historical
|
||||
extraction must not impose a second cap or a minimum amount of retained code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -114,15 +116,3 @@ def test_review_evidence_section_owner_is_forced_into_every_review_pack():
|
|||
"ouroboros/review_evidence_sections.py",
|
||||
):
|
||||
assert rel in review_context_atlas._REVIEW_STACK_PATHS, rel
|
||||
|
||||
|
||||
def test_review_evidence_extraction_size_bounds_have_meaningful_headroom():
|
||||
counts = {
|
||||
module.__name__: len(
|
||||
pathlib.Path(module.__file__).read_text(encoding="utf-8").splitlines()
|
||||
)
|
||||
for module in (review_evidence, *_LEAVES)
|
||||
}
|
||||
assert all(count <= 1000 for count in counts.values())
|
||||
assert 600 <= counts["ouroboros.review_evidence_sections"] <= 1000
|
||||
assert 600 <= counts["ouroboros.review_evidence"] <= 1000
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import asyncio, importlib.util, json, sys, types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _load_plugin():
|
||||
root = Path(__file__).resolve().parents[1] / "skills" / "telegram"
|
||||
|
|
@ -73,6 +75,84 @@ def test_real_reply_is_sent(tmp_path, monkeypatch):
|
|||
assert len(_Rec.sent) == 1 and "answer" in _Rec.sent[0][1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("silent", [None, "off", "on"], ids=["default", "off", "on"])
|
||||
def test_terminal_host_notice_preserves_answer_and_silent_reply_chain(tmp_path, monkeypatch, silent):
|
||||
"""The real split delivery keeps both authors' text through Telegram's edit mode."""
|
||||
from collections import deque
|
||||
import queue
|
||||
|
||||
from ouroboros.task_finalization import prepare_terminal_send_event, register_final_answer_owed
|
||||
from ouroboros.tools.control_runtime import _send_user_message
|
||||
from ouroboros.utils import append_jsonl
|
||||
from supervisor import events_chat_delivery as delivery, message_bus
|
||||
from supervisor.terminal_delivery import pending_deliveries
|
||||
|
||||
settings = {} if silent is None else {"TELEGRAM_SILENT_MODE": silent}
|
||||
plugin, api = _setup(tmp_path, monkeypatch, **settings)
|
||||
handle = plugin._make_outbound(api)
|
||||
published, frames = [], []
|
||||
|
||||
def publish(_topic, event):
|
||||
published.append(dict(event))
|
||||
asyncio.run(handle(event))
|
||||
|
||||
bridge = message_bus.LocalChatBridge({})
|
||||
bridge._broadcast_fn = frames.append
|
||||
monkeypatch.setattr(message_bus, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(message_bus, "get_bridge", lambda: bridge)
|
||||
monkeypatch.setattr(message_bus, "load_state", lambda: {"owner_id": 7})
|
||||
monkeypatch.setattr(message_bus, "_advance_project_visible_revision", lambda _chat: None)
|
||||
monkeypatch.setattr(message_bus, "publish_event", publish)
|
||||
monkeypatch.setattr(delivery, "_DELIVERED_MESSAGE_IDS", deque(maxlen=256))
|
||||
host = types.SimpleNamespace(DRIVE_ROOT=tmp_path, RUNNING={}, append_jsonl=append_jsonl,
|
||||
send_with_budget=message_bus.send_with_budget)
|
||||
task = {"id": "native-reply", "type": "task", "chat_id": 1, "_is_direct_chat": True}
|
||||
events = queue.Queue()
|
||||
tool_ctx = types.SimpleNamespace(current_chat_id=1, task_id=task["id"], task_metadata={},
|
||||
event_queue=events, pending_events=[], drive_root=tmp_path,
|
||||
drive_logs=lambda: tmp_path / "logs")
|
||||
ongoing = ["I found the source.", "I am checking the result."]
|
||||
for text in ongoing:
|
||||
assert "sent to owner chat" in _send_user_message(tool_ctx, text)
|
||||
delivery._handle_send_message(events.get_nowait(), host)
|
||||
assert not tool_ctx.pending_events
|
||||
assert all(row["system_type"] == "proactive_message" for row in published)
|
||||
assert all("task_terminal_status" not in row for row in published)
|
||||
assert len(_Rec.edited) == (1 if silent == "on" else 0)
|
||||
|
||||
answer, notice = "Complete model answer: λ", "Plan review stayed open; inspect the task details."
|
||||
event = prepare_terminal_send_event(tmp_path, task, answer, {
|
||||
"terminal_origin": "model_final", "terminal_host_notice": notice,
|
||||
}, {"type": "send_message", "task_id": task["id"], "chat_id": 1,
|
||||
"text": answer, "log_text": answer, "format": "markdown"}, ephemeral=False, presence=False)
|
||||
register_final_answer_owed(task, event, env_drive_root=tmp_path)
|
||||
delivery_id = event["delivery_id"]
|
||||
delivery._handle_send_message(event, host)
|
||||
sent_counts = len(_Rec.sent), len(_Rec.edited), len(published)
|
||||
delivery._handle_send_message(event, host) # Buffered final after its live copy.
|
||||
assert sent_counts == (len(_Rec.sent), len(_Rec.edited), len(published))
|
||||
assert event["text"] == event["log_text"] == answer and event["delivery_id"] == delivery_id
|
||||
assert [(row["role"], row["content"]) for row in frames] == [
|
||||
*(('assistant', text) for text in ongoing), ('assistant', answer), ('system', notice),
|
||||
]
|
||||
assert pending_deliveries(tmp_path) == []
|
||||
visible = {row[3]: row[1] for row in _Rec.sent}
|
||||
for _chat, message_id, text, _format in _Rec.edited:
|
||||
visible[message_id] = text
|
||||
assert list(visible.values()) == ([answer, notice] if silent == "on" else [*ongoing, answer, notice])
|
||||
if silent == "on":
|
||||
# The system notice neither overwrites nor takes ownership of the normal edit chain.
|
||||
assert plugin._get_silent_msg(api, 42) == _Rec.sent[0][3]
|
||||
delivery._handle_send_message({"type": "send_message", "chat_id": 1,
|
||||
"text": "Separate host receipt", "role": "system",
|
||||
"system_type": "terminal_incident"}, host)
|
||||
receipt_id = _Rec.sent[-1][3]
|
||||
assert "sent to owner chat" in _send_user_message(tool_ctx, "A later ongoing reply.")
|
||||
delivery._handle_send_message(events.get_nowait(), host)
|
||||
assert _Rec.edited[-1][1:3] == (_Rec.sent[0][3], "A later ongoing reply.")
|
||||
assert all(row[1] != receipt_id for row in _Rec.edited)
|
||||
|
||||
|
||||
def test_subagent_cards_off_hides_activity(tmp_path, monkeypatch):
|
||||
plugin, api = _setup(tmp_path, monkeypatch, TELEGRAM_SUBAGENT_CARDS="off")
|
||||
handle = plugin._make_outbound(api)
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -1535,7 +1535,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "7.0.0rc17"
|
||||
version = "7.0.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "croniter" },
|
||||
|
|
|
|||
|
|
@ -1346,4 +1346,4 @@ export const MAX_QUIZ_OPTIONS = 6;
|
|||
// REFUSES a longer comment (it is delivered verbatim, never truncated), so
|
||||
// the card must not offer to send one.
|
||||
export const MAX_DECISION_COMMENT = 2000;
|
||||
export const GATEWAY_CONTRACT_VERSION = '7.0.0-rc.17';
|
||||
export const GATEWAY_CONTRACT_VERSION = '7.0.0';
|
||||
|
|
|
|||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "ouroboros-web",
|
||||
"version": "7.0.0-rc.17",
|
||||
"version": "7.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ouroboros-web",
|
||||
"version": "7.0.0-rc.17",
|
||||
"version": "7.0.0",
|
||||
"devDependencies": {
|
||||
"eslint": "10.10.0",
|
||||
"globals": "17.12.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ouroboros-web",
|
||||
"version": "7.0.0-rc.17",
|
||||
"version": "7.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Ouroboros browser UI package boundary",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue