diff --git a/crates/colibri-daemon/tests/intake_scheduler_loop.rs b/crates/colibri-daemon/tests/intake_scheduler_loop.rs index 983d3fa..4058a2b 100644 --- a/crates/colibri-daemon/tests/intake_scheduler_loop.rs +++ b/crates/colibri-daemon/tests/intake_scheduler_loop.rs @@ -1,8 +1,7 @@ -//! Regression test for the bug the 2026-05-27 osa FreeBSD smoke caught: +//! Regression test for the bug a 2026-05-27 osa FreeBSD smoke caught: //! `main.rs` started the socket server but never spawned `daemon::run_loop`, so //! an `intake-task` reported `queued` over the socket yet was never drained into -//! the SQLite coordination store. See -//! `docs/internal/sessions/2026-05-27-osa-freebsd-daemon-scheduler-smoke.md`. +//! the SQLite coordination store. //! //! This proves the full path: socket `intake-task` → `run_loop` scheduler tick → //! persisted SQLite task. If `run_loop` is ever dropped from the wiring again, diff --git a/crates/colibri-glasspane-tui/src/main.rs b/crates/colibri-glasspane-tui/src/main.rs index 27089ae..7c05a3c 100644 --- a/crates/colibri-glasspane-tui/src/main.rs +++ b/crates/colibri-glasspane-tui/src/main.rs @@ -1,4 +1,4 @@ -// colibri-harness — herdr-like supervision TUI built on Colibri primitives. +// colibri-harness — Colibri-native supervision TUI built on Colibri primitives. // // Connects to a colibri-daemon Unix socket, polls GlasspaneSnapshot every 2s, // lets the operator spawn/kill agents, drill into pane details, and cycle diff --git a/docs/CLAWDIE-AGENT-WIKI.md b/docs/CLAWDIE-AGENT-WIKI.md index 022bded..dd52751 100644 --- a/docs/CLAWDIE-AGENT-WIKI.md +++ b/docs/CLAWDIE-AGENT-WIKI.md @@ -1,10 +1,11 @@ # Clawdie Agent — wiki / mindmap -The **Clawdie agent** is the simplified, operator-friendly face of Colibri: one -small Rust binary (`clawdie`) that ships with exactly two things wired up — a -Telegram bot and a DeepSeek lane — sitting on top of the proven control-plane -core. Everything heavier (cost modes, quotas, multi-provider fallback) is -deliberately **lifted**. +The **Clawdie agent** mini-binary is an experimental, operator-friendly face of +Colibri: one small Rust binary (`clawdie`) that wires a Telegram bot and a +DeepSeek lane on top of the control-plane core. It is not the current live-ISO +service contract; the FreeBSD live USB runs `colibri_daemon` directly, while +`service clawdie` is reserved for a future installed disk/server service once +that implementation is chosen. This is a mindmap-style wiki page (the format from the Herdr/Colibri capability graph work — see [`HERDR-VS-COLIBRI-GRAPH.md`](./HERDR-VS-COLIBRI-GRAPH.md) and @@ -59,9 +60,9 @@ Two halves of the same daemon, both kept: | **Glasspane** | `colibri-glasspane` + daemon socket | The "radar": agent state, panes, supervision snapshot | | **Coordination** | `colibri-store` + daemon loop | Task board, agent registry, session lifecycle | -The Clawdie agent reuses these as-is over the Herdr Unix-socket API. It adds the -Telegram + DeepSeek front door and **removes** the cost/quota machinery from its -own runtime path. +The Clawdie mini-binary reuses these as-is over the Colibri control-plane socket. +It adds the Telegram + DeepSeek front door and **removes** the cost/quota +machinery from its own runtime path. ## Surface area (the whole product) @@ -72,7 +73,7 @@ graph LR ds -->|reply| clawdie clawdie --> gp["glasspane radar"] clawdie --> co["coordination board"] - clawdie -. Herdr socket .- ui["operator tools (colibri CLI / TUI)"] + clawdie -. Colibri socket .- ui["operator tools (colibri CLI / TUI)"] ``` ## Files @@ -95,4 +96,4 @@ graph LR One DeepSeek key serves both the Telegram chat lane and the daemon's own provider routing. -See [`CLAWDIE-BUILD.md`](./CLAWDIE-BUILD.md) for the build + ISO instructions. +See [`CLAWDIE-BUILD.md`](./CLAWDIE-BUILD.md) for experimental build notes. diff --git a/docs/CLAWDIE-BUILD.md b/docs/CLAWDIE-BUILD.md index 6da4a18..45e2d8a 100644 --- a/docs/CLAWDIE-BUILD.md +++ b/docs/CLAWDIE-BUILD.md @@ -1,8 +1,10 @@ -# Clawdie agent — build & ISO instructions +# Experimental `clawdie` mini-binary — build notes -The `clawdie` binary is the simplified Colibri agent (see -[`CLAWDIE-AGENT-WIKI.md`](./CLAWDIE-AGENT-WIKI.md)). It is configured almost -entirely by **build flags**, so a baked ISO ships ready to run. +The `clawdie` binary is an experimental Colibri-side candidate for a future +deployed-system service (see [`CLAWDIE-AGENT-WIKI.md`](./CLAWDIE-AGENT-WIKI.md)). +It is **not** the current FreeBSD live-ISO service contract. The live USB uses +`colibri_daemon`; `service clawdie` remains reserved for an installed disk/server +service once that implementation is chosen. ## 1. Build with baked credentials @@ -29,8 +31,8 @@ Build with **no** flags for a "bring your own key" binary — it reads ## 2. What the binary does out of the box -- Starts the control-plane core: glasspane supervision + Herdr Unix socket + - coordination loop. +- Starts the control-plane core: glasspane supervision + Colibri control-plane + socket + coordination loop. - If a Telegram token **and** a DeepSeek key are present, runs the Telegram bridge (long-poll → DeepSeek → reply). - No token → headless (control plane only). Token but no key → bridge disabled @@ -39,9 +41,10 @@ Build with **no** flags for a "bring your own key" binary — it reads No cost modes, quotas, context budgets, or provider fallback — those are lifted from this agent on purpose. -## 3. Run as a service (like Clawdie-AI) +## 3. Run as an experimental service -On FreeBSD, install the rc.d script and enable it: +On FreeBSD, install the rc.d script only on a throwaway test host or an explicit +deployed-service experiment: ```sh pw groupadd clawdie @@ -65,18 +68,13 @@ chmod 0600 /usr/local/etc/clawdie/clawdie.env service clawdie restart ``` -## 4. ISO integration +## 4. ISO status -The ISO build stages the prebuilt FreeBSD `clawdie` release binary + rc.d (it -never compiles Rust while the image is mounted), the same model as the Colibri -daemon staging. In `clawdie-iso`: - -- `build.cfg`: `FEATURE_CLAWDIE`, `CLAWDIE_ENABLE`, and the build-flag creds. -- `scripts/stage-clawdie-iso.sh`: installs binary + rc.d + rc.conf sample. - -Build the binary on the FreeBSD/OSA host (not Debian/Linux), then run the ISO -preflight. **Do not `cargo clean` the colibri checkout** until the ISO build has -consumed `target/release/clawdie`. +The current `clawdie-iso` baseline does **not** stage this mini-binary or its +rc.d script. ISO builds stage `colibri-daemon`, `colibri`, `colibri-smoke-agent`, +and preferably `colibri-tui` from this checkout. If a deployed-system +`service clawdie` lane is reintroduced later, it should get fresh packaging and +acceptance criteria instead of silently treating this experiment as final. ## 5. Next ISO build — XFCE operator-USB fixes to carry forward @@ -84,8 +82,8 @@ The next operator-USB image must retain the XFCE/live-GUI fixes already proven on real hardware (see `clawdie-iso/PLAN-OPERATOR-USB-NEXT.md` and `doc/AMD-ASUS-XFCE-LIVE-USB-FINDINGS.md`). Load-bearing items: -- **SDDM over LightDM** — LightDM was the silent blocker for live GUI boot on - Intel and AMD. SDDM is part of the operator-USB contract; do **not** revert. +- **SDDM remains the supported live display manager** — keep it unless an + alternative has equivalent real-hardware proof for Intel and AMD GUI boot. - **`clawdie-live-gpu`** — pre-SDDM rc.d service that does a conservative KMS pick (Intel iGPU works out of the box; AMD/NVIDIA picked conservatively) before the display manager starts. diff --git a/docs/COLIBRI-DAEMON-GLASSPANE-INTEGRATION.md b/docs/COLIBRI-DAEMON-GLASSPANE-INTEGRATION.md index 0177fad..21b658b 100644 --- a/docs/COLIBRI-DAEMON-GLASSPANE-INTEGRATION.md +++ b/docs/COLIBRI-DAEMON-GLASSPANE-INTEGRATION.md @@ -18,8 +18,8 @@ crates to be updated in lockstep. │ │ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────────┐ │ │ │ Spawner │ │ Sessions │ │ Heartbeat │ │ Socket Server │ │ -│ │ (agents) │ │ (JSONL) │ │ (30s) │ │ (Unix domain) │──┼──► Herdr / web -│ └─────┬─────┘ └───────────┘ └─────┬─────┘ └───────┬───────┘ │ board / Zed +│ │ (agents) │ │ (JSONL) │ │ (30s) │ │ (Unix domain) │──┼──► Colibri TUI / web +│ └─────┬─────┘ └───────────┘ └─────┬─────┘ └───────┬───────┘ │ Zed / optional Herdr Linux │ │ │ │ │ │ │ stdout JSONL │ poll_exit() │ query │ │ ▼ ▼ ▼ │ @@ -47,7 +47,7 @@ matching the existing watchdog convention). ### Wire types (defined in `colibri-daemon/src/lib.rs`) -**Inbound: `HerdrCommand`** (tagged by `cmd` field): +**Inbound: `ColibriCommand`** (tagged by `cmd` field): | `cmd` value | Parameters | Purpose | | -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | @@ -59,7 +59,7 @@ matching the existing watchdog convention). | `get-session` | `session_id` | Full session dump (turns + prompt) | | `compact-session` | `session_id` | Compact oldest turns in a session | -**Outbound: `HerdrResponse`**: +**Outbound: `ColibriResponse`**: ```json { @@ -104,8 +104,8 @@ Server: {"ok":true,"data":{"agent_id":"a1b2-c3d4","status":"running"}}\n ### Operator CLI smoke helpers -`colibri-client` also ships two small binaries for manual Herdr/SSH smoke tests -without hand-writing socket JSON: +`colibri-client` also ships small binaries for manual display-client and SSH +smoke tests without hand-writing socket JSON: ```sh # Inspect daemon state @@ -219,19 +219,19 @@ AgentHandle SupervisedPane Session ### Daemon lifecycle events → Glasspane AgentState -| Daemon lifecycle event | Ingested Pi event type | Resulting `AgentState` | Notes | -| ------------------------------ | ------------------------------------------------------- | ---------------------- | ------------------------------------------ | -| Agent subprocess spawned | _(pane attached, state = `Idle`)_ | `Idle` | `SupervisedPane::new` defaults to `Idle` | -| Agent emits `session` header | `session` / `session_started` | `Idle` | Also captures `pi_session_id` and `cwd` | -| Agent emits turn/message/tool | `turn_start`, `message_start`, `tool_execution_*`, etc. | `Working` | Any of 14 event types | -| Agent emits compaction events | `auto_compaction_*`, `compaction_*` | `Working` | Compaction is active work | -| Agent emits retry events | `auto_retry_*` | `Working` | Retry is active work | -| Agent awaits steering/approval | `queue_update` | `Blocked` | Operator attention needed (Herdr headline) | -| Turn/task complete | `turn_end` / `agent_end` | `Done` | Agent reached a completion point | -| Agent emits explicit error | `error` | `Error` | Terminal failure state | -| Agent subprocess exits (0) | _daemon injects `agent_end`_ | `Done` | Heartbeat detected normal exit | -| Agent subprocess exits (!=0) | _daemon injects `error`_ | `Error` | Heartbeat detected crash/error exit | -| Agent killed externally | _daemon injects `error`_ | `Error` | `kill-agent` command | +| Daemon lifecycle event | Ingested Pi event type | Resulting `AgentState` | Notes | +| ------------------------------ | ------------------------------------------------------- | ---------------------- | ---------------------------------------------- | +| Agent subprocess spawned | _(pane attached, state = `Idle`)_ | `Idle` | `SupervisedPane::new` defaults to `Idle` | +| Agent emits `session` header | `session` / `session_started` | `Idle` | Also captures `pi_session_id` and `cwd` | +| Agent emits turn/message/tool | `turn_start`, `message_start`, `tool_execution_*`, etc. | `Working` | Any of 14 event types | +| Agent emits compaction events | `auto_compaction_*`, `compaction_*` | `Working` | Compaction is active work | +| Agent emits retry events | `auto_retry_*` | `Working` | Retry is active work | +| Agent awaits steering/approval | `queue_update` | `Blocked` | Operator attention needed (dashboard headline) | +| Turn/task complete | `turn_end` / `agent_end` | `Done` | Agent reached a completion point | +| Agent emits explicit error | `error` | `Error` | Terminal failure state | +| Agent subprocess exits (0) | _daemon injects `agent_end`_ | `Done` | Heartbeat detected normal exit | +| Agent subprocess exits (!=0) | _daemon injects `error`_ | `Error` | Heartbeat detected crash/error exit | +| Agent killed externally | _daemon injects `error`_ | `Error` | `kill-agent` command | ### State transition diagram @@ -446,11 +446,12 @@ let snapshot = state.glasspane.read().await.snapshot_at( ### Where it is consumed -| Consumer | Transport | Phase | Purpose | -| -------------------- | ------------------ | ----- | -------------------------------- | -| Herdr (Linux) | Unix socket | 4 | Operator dashboard display | -| Zed / web board | HTTP / SSE | 4 | Web-based supervision view | -| colibri-orchestrator | In-memory / socket | 5 | Route/dispatch work across panes | +| Consumer | Transport | Phase | Purpose | +| ----------------------------- | ------------------ | ----- | -------------------------------------------- | +| `colibri` CLI / `colibri-tui` | Unix socket | 4 | Native operator dashboard and smoke surface | +| Herdr (Linux/macOS optional) | Unix socket/bridge | 4 | Optional external display client, not source | +| Zed / web board | HTTP / SSE | 4 | Web-based supervision view | +| colibri-orchestrator | In-memory / socket | 5 | Route/dispatch work across panes | ### Wire shape (JSON) @@ -513,9 +514,10 @@ library crate, not a process — it is compiled into the daemon binary. 4. socket::serve(state, shutdown_rx) ← BLOCKING ├── Binds Unix socket at config.socket_path ├── Accepts connections - └── Dispatches HerdrCommand variants + └── Dispatches ColibriCommand variants │ -5. External clients (Herdr, web) connect and send commands +5. External clients (colibri CLI/TUI, optional Herdr Linux/macOS, web) connect + and send commands ``` ### Clean boot checklist (in sequence) @@ -553,6 +555,6 @@ Unix socket path (`DaemonConfig.socket_path`). | `docs/HERDR-VS-COLIBRI-GRAPH.md` | Hybrid boundary: Herdr as Linux display client | | `crates/colibri-daemon/src/socket.rs` | Socket server implementation | | `crates/colibri-daemon/src/daemon.rs` | Daemon background loop + heartbeat | -| `crates/colibri-daemon/src/lib.rs` | Wire types: `HerdrCommand`, `HerdrResponse` | +| `crates/colibri-daemon/src/lib.rs` | Wire types: `ColibriCommand`, `ColibriResponse` | | `crates/colibri-daemon/src/spawner.rs` | Agent subprocess spawner | | `crates/colibri-glasspane/src/lib.rs` | State machine, supervisor, snapshot contract | diff --git a/docs/HERDR-VS-COLIBRI-GRAPH.md b/docs/HERDR-VS-COLIBRI-GRAPH.md index 29cd055..b241dc2 100644 --- a/docs/HERDR-VS-COLIBRI-GRAPH.md +++ b/docs/HERDR-VS-COLIBRI-GRAPH.md @@ -105,14 +105,16 @@ graph LR | `run.manifest` | `build_run_manifest()` → `clawdie.interagent.run-manifest.v1` | run-manifest emitter | **Proposed — roadmap, NOT locked** (graph for comparison; names firm up when -code lands): supervision `supervise.attach/list/state` (Herdr socket); -coordination `board.task(queued→claimed→started→done|failed)`, `skills.*`; +code lands): supervision `supervise.attach/list/state` (Colibri source of +truth, with optional Herdr-compatible Linux display); coordination +`board.task(queued→claimed→started→done|failed)`, `skills.*`; execution `schedule.cron/interval/once`, `remote.*`. ## 5. Drop candidates (gated) -Covered by Herdr (supervision, Linux client) or `colibri-deepseek` -(provider/cache); remove only after proof gates + per-file caller inventory. +Covered by Herdr where a Linux display client is desired, or by +`colibri-deepseek` (provider/cache); remove only after proof gates + per-file +caller inventory. Snapshot: clawdie-ai `archive/multitenant-claude-pre-divergence`. - `tmux-screenshot-command.ts` + glass-pane glue → **Herdr** supervision. diff --git a/docs/ISO-INTEGRATION-PLAN.md b/docs/ISO-INTEGRATION-PLAN.md index 1ac1fe0..ba31a2b 100644 --- a/docs/ISO-INTEGRATION-PLAN.md +++ b/docs/ISO-INTEGRATION-PLAN.md @@ -27,6 +27,16 @@ operator/display Herdr is not being ported to FreeBSD. On the ISO, the native answer is Colibri daemon + glasspane/TUI/harness. +## Platform boundary + +| Target | Source of truth | Display/control surface | Claim boundary | +| ------------------------- | -------------------------------------- | --------------------------------------- | -------------------------------------------------------------- | +| FreeBSD 15 live ISO | `colibri_daemon` + `colibri-glasspane` | `colibri` CLI / `colibri-tui` | Requires FreeBSD build/boot evidence; GUI needs hardware proof | +| Linux/macOS operator host | Colibri snapshot/API from the daemon | Optional Herdr, terminal panes, web/Zed | Display-client proof only; does not prove FreeBSD runtime | + +Herdr remains an optional Linux/macOS display client. It must not become a +FreeBSD ISO dependency or the source of truth for supervision state. + ## Current baseline Already landed and validated: @@ -316,7 +326,7 @@ Required: - Phase 3: task capability matching assigns work to jail workers. - Phase 4: per-task ephemeral jail option if needed. -### Lane D — Native dashboard / Herdr replacement +### Lane D — Native dashboard / optional Herdr display 1. **FreeBSD-native dashboard** - Continue with `colibri-glasspane-tui` / harness rather than porting Herdr. diff --git a/docs/MULTIAGENT-WORKFLOW-IMPROVEMENTS.md b/docs/MULTIAGENT-WORKFLOW-IMPROVEMENTS.md index 6028114..b961f4e 100644 --- a/docs/MULTIAGENT-WORKFLOW-IMPROVEMENTS.md +++ b/docs/MULTIAGENT-WORKFLOW-IMPROVEMENTS.md @@ -191,7 +191,8 @@ Failed: 0 ❌ ### Benefits -- ✅ Guarantees Linux and FreeBSD behave identically +- ✅ Compares Linux and FreeBSD behavior and surfaces platform drift; FreeBSD + runtime proof still requires FreeBSD validation - ✅ Catches platform-specific regressions early - ✅ Provides clear evidence matrix for each gate - ✅ Reduces manual "is this working on osa?" checks diff --git a/docs/internal/sessions/2026-05-27-colibri-live-daemon-client-smoke.md b/docs/internal/sessions/2026-05-27-colibri-live-daemon-client-smoke.md deleted file mode 100644 index 6008262..0000000 --- a/docs/internal/sessions/2026-05-27-colibri-live-daemon-client-smoke.md +++ /dev/null @@ -1,90 +0,0 @@ -# Colibri Phase 4 Live Daemon/Client Smoke Report - -**Date:** 27.maj.2026 -**Host:** osa.smilepowered.org — FreeBSD 15.0-RELEASE-p8 amd64 -**Repo:** `Clawdie/Colibri` -**Commit tested:** `fbcb7e6` — `Add live daemon client smoke with local fake agent` -**Status:** PASS - -## Purpose - -Prove that Phase 4 is not only unit-level: a real `colibri-daemon` Unix socket -server can run in parallel with existing services, and `colibri-client` can talk -to it over the socket without sudo or production paths. - -## Isolation - -The smoke uses a temp data directory and socket under the current user's temp -area. It does **not** touch production services, global sockets, or privileged -paths. - -No sudo is required. - -## What the smoke covers - -The new integration test: - -```text -crates/colibri-client/tests/live_socket_smoke.rs -``` - -performs this full path: - -1. Creates isolated `DaemonConfig` -2. Starts real `socket::serve(...)` -3. Creates real `DaemonClient` -4. Calls `status` -5. Calls `glasspane_snapshot` before spawn and verifies no panes -6. Writes a fake local Pi JSONL agent script -7. Calls `spawn-agent` with `provider:"local"` -8. Verifies Glasspane state transitions via the daemon socket: - - `Idle` - - `Working` - - `Blocked` - - `Done` -9. Verifies captured `pi_session_id` and `cwd` -10. Calls `kill-agent` -11. Sends daemon shutdown and removes temp data - -## Local provider behavior added - -`Provider::Local` was added for no-network smoke/local tools: - -- no API key required -- no fallback to remote providers -- socket `model` field is treated as executable path -- no `--mode json` args are injected - -This keeps live daemon/client testing deterministic and independent of Pi, -DeepSeek, OpenRouter, Anthropic, or existing agent services. - -## Validation commands - -```sh -cargo fmt --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test --workspace -cargo build --workspace --release -``` - -## Result - -All gates passed on FreeBSD 15. - -```text -50 tests passed, 0 failed -release build OK -``` - -## Current conclusion - -Colibri Phase 4 now has a working typed client plus real socket smoke coverage. -The daemon/client/glasspane path is proven end-to-end with a local fake Pi JSONL -agent, while remaining safe to run in parallel with existing services. - -## Next candidates - -- Add a small CLI wrapper around `colibri-client` for manual operator smoke. -- Promote `GlasspaneSnapshot` to `colibri-contracts` once a second external - consumer needs standalone deserialization. -- Add HTTP/SSE transport later; Unix socket is sufficient for current Phase 4. diff --git a/docs/internal/sessions/2026-05-27-osa-freebsd-colibri-cli-task-smoke.md b/docs/internal/sessions/2026-05-27-osa-freebsd-colibri-cli-task-smoke.md deleted file mode 100644 index cc9ea71..0000000 --- a/docs/internal/sessions/2026-05-27-osa-freebsd-colibri-cli-task-smoke.md +++ /dev/null @@ -1,80 +0,0 @@ -# OSA FreeBSD `colibri` Task CLI Smoke - -**Date:** 2026-05-27 -**Host:** osa.smilepowered.org -**OS:** FreeBSD 15.0-RELEASE-p8 amd64 -**Repo:** `Clawdie/Colibri` -**Commit:** `f434a89` — `feat: add colibri task commands` -**Status:** PASS - -## Scope - -Validated the post-refactor operator CLI task commands, using the renamed binary only: - -```sh -colibri create-task -colibri list-tasks -colibri intake-task -``` - -No raw Python Unix-socket client was used for task operations. - -## Gates - -```sh -cargo fmt --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test --workspace -cargo build --workspace --release -``` - -Workspace gates were green at `f434a89`. - -## Isolated smoke environment - -```text -COLIBRI_DAEMON_DATA_DIR=/tmp/colibri-cli-task-smoke-clawdie-1779913324/data -COLIBRI_DAEMON_SOCKET=/tmp/colibri-cli-task-smoke-clawdie-1779913324/colibri.sock -COLIBRI_DB_PATH=/tmp/colibri-cli-task-smoke-clawdie-1779913324/colibri.sqlite -COLIBRI_HOST=osa-cli-task-smoke -``` - -## Result - -- `colibri status` connected to the daemon socket. -- `colibri create-task --title ... --description ...` created a queued SQLite task. -- `colibri list-tasks --status queued` returned the direct task. -- `colibri intake-task --title ... --description ... --capability freebsd --capabilities sqlite,scheduler` queued intake successfully. -- The daemon scheduler drained intake on the next 30s tick: - -```text -FOUND_ON_POLL=31 -``` - -SQLite verification: - -```text -tasks 2 -journal_mode wal -``` - -Graceful shutdown: - -```text -socket exists after stop? no -process remains? no -``` - -The daemon log showed the socket server and background loop exiting cleanly. - -## Verdict - -The renamed `colibri` operator CLI now covers the immediate task workflow: - -```sh -colibri create-task --title "..." -colibri list-tasks --status queued -colibri intake-task --title "..." --capability freebsd -``` - -The OSA `/tmp` smoke passed without using raw socket helper scripts. diff --git a/docs/internal/sessions/2026-05-27-osa-freebsd-daemon-scheduler-smoke.md b/docs/internal/sessions/2026-05-27-osa-freebsd-daemon-scheduler-smoke.md deleted file mode 100644 index 4b36e30..0000000 --- a/docs/internal/sessions/2026-05-27-osa-freebsd-daemon-scheduler-smoke.md +++ /dev/null @@ -1,243 +0,0 @@ -# OSA FreeBSD Colibri Daemon + Scheduler Smoke - -**Date:** 27.maj.2026 -**Host:** osa.smilepowered.org -**OS:** FreeBSD 15.0-RELEASE-p9 amd64 -**Rust:** rustc 1.94.0 (4a4ef493e 2026-03-02) -**Repo:** `Clawdie/Colibri` -**Commit tested:** `53028a0` — `docs: answer Codex handoff questions — colibri-ctl, scheduler, smoke-agent, WAL` -**Status:** PARTIAL PASS — core daemon smoke passed; scheduler runtime wiring gap found - -## Scope - -Prototype-only `/tmp` smoke. No rc.d install, no service takeover, no production paths. - -Test environment: - -```sh -COLIBRI_DAEMON_DATA_DIR=/tmp/colibri-osa-smoke-clawdie-1779905501/data -COLIBRI_DAEMON_SOCKET=/tmp/colibri-osa-smoke-clawdie-1779905501/colibri.sock -COLIBRI_DB_PATH=/tmp/colibri-osa-smoke-clawdie-1779905501/colibri.sqlite -COLIBRI_HOST=osa-smoke -``` - -Build command: - -```sh -cargo build --workspace --release -``` - -Result: release build OK. - -## Core daemon result - -### Start + files - -`colibri-daemon` started successfully with isolated `/tmp` paths. - -Created: - -```text -colibri.sock -colibri.sqlite -colibri.sqlite-shm -colibri.sqlite-wal -data/sessions/ -daemon.log -``` - -SQLite WAL mode is active on FreeBSD: - -```text -journal_mode wal -``` - -### Status - -```json -{ - "agent_list": [], - "agents": 0, - "daemon": "colibri-daemon", - "host": "osa-smoke", - "sessions": 0, - "version": "0.0.1" -} -``` - -### Snapshot before spawn - -```json -{ - "schema": "clawdie.glasspane.snapshot.v1", - "host": "osa-smoke", - "observed_at": "2026-05-27T18:11:41.890Z", - "panes": [] -} -``` - -### Local fake agent spawn - -Command: - -```sh -target/release/colibri-ctl --socket "$COLIBRI_DAEMON_SOCKET" \ - spawn-local /home/clawdie/colibri/target/release/colibri-smoke-agent \ - --session-id osa-smoke-session -``` - -Response: - -```json -{ - "agent_id": "d2c5fc74-4085-4731-a0e0-f373df9d007b", - "status": "running" -} -``` - -Snapshot after fake agent emitted Pi-compatible JSONL: - -```json -{ - "schema": "clawdie.glasspane.snapshot.v1", - "host": "osa-smoke", - "observed_at": "2026-05-27T18:11:47.450Z", - "panes": [ - { - "id": "d2c5fc74-4085-4731-a0e0-f373df9d007b", - "agent": "/home/clawdie/colibri/target/release/colibri-smoke-agent", - "state": "done", - "pi_session_id": "manual-smoke", - "last_event_at": "2026-05-27T18:11:45.897Z", - "cwd": "/home/clawdie/colibri" - } - ] -} -``` - -Kill command succeeded: - -```json -{ - "agent_id": "d2c5fc74-4085-4731-a0e0-f373df9d007b", - "status": "stopped" -} -``` - -## Coordination store result - -Direct `create-task` over the Unix socket succeeded and persisted to SQLite: - -```json -{ - "ok": true, - "data": { - "agent_id": null, - "created_at": "2026-05-27T18:11:47.578003651+00:00", - "description": "direct socket create-task", - "id": "288887de-c82e-46d7-9f2d-2701299266e6", - "status": "queued", - "title": "osa smoke direct task", - "updated_at": "2026-05-27T18:11:47.578003651+00:00" - } -} -``` - -SQLite counts after smoke: - -```text -tasks 1 -agents 0 -skills 0 -``` - -## Intake-task / scheduler result - -`intake-task` command over the socket returned success: - -```json -{"ok":true,"data":{"status":"queued"}} -``` - -However, an immediate `list-tasks` returned no task from intake: - -```json -{"ok":true,"data":[]} -``` - -The direct `create-task` path then proved the store itself works, so this is not a SQLite failure. - -### Finding - -Scheduler runtime processing is not active in the `colibri-daemon` binary at this commit. - -Evidence: - -- `cmd_intake_task` queues into `state.scheduler` in memory. -- `daemon::run_loop` contains the scheduler tick. -- `crates/colibri-daemon/src/main.rs` starts the socket server but does **not** spawn `daemon::run_loop`. -- Therefore `intake-task` can report `queued`, but queued intake is not drained into SQLite tasks by the running daemon. - -This means T1.3 scheduler unit tests pass, and scheduler code exists, but the live daemon does not yet process intake/scheduled jobs. - -## Restart / teardown behavior - -Graceful interrupt removed the socket: - -```text -after stop socket exists? no -process remains? no -``` - -Restarting the daemon against the same `/tmp` DB/socket path also worked: - -```json -{ - "agent_list": [], - "agents": 0, - "daemon": "colibri-daemon", - "host": "osa-smoke-restart", - "sessions": 0, - "version": "0.0.1" -} -``` - -After restart stop: - -```text -restart after stop socket exists? no -restart process remains? no -``` - -The `/tmp` smoke directory was removed after recording this report. - -## Verdict - -### Passed - -- FreeBSD release build -- daemon starts with isolated `/tmp` data/socket/DB paths -- Unix socket is created and removed on graceful shutdown -- status command works -- glasspane snapshot command works -- SQLite DB + WAL files are created on FreeBSD -- `spawn-local colibri-smoke-agent` works -- Pi JSONL ingestion produces `done` pane state with `pi_session_id` -- direct `create-task` / `list-tasks` store path works -- restart against same temp DB/socket path works -- prototype rc.d file remains review-only and was not installed - -### Partial / failed - -- `intake-task` returns success but is not processed into a SQLite task because the daemon binary does not start `daemon::run_loop`. -- `colibri-ctl` still lacks task/intake helper commands, so task/intake smoke used a small raw Unix-socket Python client. - -## Recommended next fix - -1. Spawn `daemon::run_loop(state.clone(), DaemonLoopConfig::default(), ...)` from `crates/colibri-daemon/src/main.rs` alongside the socket server. -2. Add a deterministic integration test proving `intake-task` over the socket becomes a queued SQLite task after a short scheduler tick. -3. Add `colibri-ctl` commands for at least: - - `list-tasks` - - `create-task` - - `intake-task` -4. Re-run this same FreeBSD `/tmp` smoke. diff --git a/docs/internal/sessions/2026-05-27-osa-freebsd-intake-resmoke.md b/docs/internal/sessions/2026-05-27-osa-freebsd-intake-resmoke.md deleted file mode 100644 index 0da0af8..0000000 --- a/docs/internal/sessions/2026-05-27-osa-freebsd-intake-resmoke.md +++ /dev/null @@ -1,142 +0,0 @@ -# OSA FreeBSD Intake Scheduler Re-smoke - -**Date:** 27.maj.2026 -**Host:** osa.smilepowered.org -**OS:** FreeBSD 15.0-RELEASE-p9 amd64 -**Repo:** `Clawdie/Colibri` -**Base pulled:** `af8c011` — daemon loop wiring landed -**Fix commit:** `d760536` — `fix: avoid scheduler store deadlock on intake drain` -**Status:** PASS after follow-up fix - -## What was checked - -Pulled the daemon-loop wiring (`9717ce7`, plus `af8c011`) and ran the full workspace gates: - -```sh -cargo fmt --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test --workspace -cargo build --workspace --release -``` - -Initial gates were green, but a live `/tmp` FreeBSD re-smoke exposed one more runtime bug. - -## Finding during live re-smoke - -With `daemon::run_loop` now started, `intake-task` did reach the scheduler tick and inserted a SQLite task, but a concurrent `list-tasks` socket request timed out. - -Root cause: `Scheduler::tick` used expressions like: - -```rust -match state.store.lock().unwrap().create_task(...) { - Ok(task) => { - state.store.lock().unwrap().list_agents() - } -} -``` - -The `MutexGuard` temporary from the `match` scrutinee can live through the match arm. Relocking `state.store` inside the arm can deadlock the scheduler thread. On FreeBSD this manifested as: - -- SQLite row was created -- socket request blocked on the store lock -- daemon became unresponsive until killed by the smoke harness timeout - -## Fix - -`d760536` rewrites scheduler store access so every lock is scoped explicitly and dropped before the next lock: - -```rust -let create_result = { - let store = state.store.lock().unwrap(); - store.create_task(...) -}; -``` - -It also adds a regression test: - -```text -scheduler::tests::test_scheduler_tick_drains_intake_without_deadlock -``` - -## Re-smoke result after fix - -Isolated `/tmp` environment: - -```text -COLIBRI_DAEMON_DATA_DIR=/tmp/colibri-osa-resmoke-clawdie-1779908417/data -COLIBRI_DAEMON_SOCKET=/tmp/colibri-osa-resmoke-clawdie-1779908417/colibri.sock -COLIBRI_DB_PATH=/tmp/colibri-osa-resmoke-clawdie-1779908417/colibri.sqlite -COLIBRI_HOST=osa-resmoke -``` - -`intake-task` response: - -```json -{"ok":true,"data":{"status":"queued"}} -``` - -The scheduler drained intake on the 30s tick: - -```text -FOUND_ON_POLL=30 -``` - -`list-tasks` then returned the queued task: - -```json -{ - "ok": true, - "data": [ - { - "agent_id": null, - "created_at": "2026-05-27T19:00:47.360062420+00:00", - "description": "prove scheduler loop drains intake", - "id": "c3dab9df-8a37-47b1-854b-d956fd796d41", - "status": "queued", - "title": "osa resmoke intake", - "updated_at": "2026-05-27T19:00:47.360062420+00:00" - } - ] -} -``` - -SQLite verification: - -```text -tasks 1 -journal_mode wal -``` - -Graceful shutdown: - -```text -socket exists after stop? no -process remains? no -``` - -Daemon log included both task loops exiting cleanly: - -```text -daemon background loop started ... scheduler_secs=30 -received interrupt signal, initiating graceful shutdown -socket server received shutdown signal -daemon loop received shutdown signal -daemon background loop exited -Herdr socket API shut down -colibri-daemon shut down cleanly -``` - -## Final verdict - -The daemon loop wiring is valid after `d760536`. - -Validated on FreeBSD: - -- daemon starts with `/tmp` data/socket/DB paths -- background daemon loop starts beside the socket server -- `intake-task` over Unix socket becomes a queued SQLite task on the scheduler tick -- `list-tasks` remains responsive after the tick -- SQLite WAL works -- graceful shutdown removes socket and exits both socket + loop tasks - -The smoke directory was removed after recording this report. diff --git a/docs/internal/sessions/2026-05-27-scheduler-freebsd-store-isolation-finding.md b/docs/internal/sessions/2026-05-27-scheduler-freebsd-store-isolation-finding.md deleted file mode 100644 index b8d685d..0000000 --- a/docs/internal/sessions/2026-05-27-scheduler-freebsd-store-isolation-finding.md +++ /dev/null @@ -1,65 +0,0 @@ -# Colibri Scheduler / FreeBSD Store Isolation Finding - -**Date:** 27.maj.2026 -**Repo:** `Clawdie/Colibri` -**Finding commit:** `ceaeaee` — scheduler T1.3 landed -**Fix commit:** `a48afa1` — `fix: harden scheduler tests and FreeBSD store isolation` -**Status:** Fixed and verified - -## Finding - -After pulling `ceaeaee`, the Linux-side direction was good, but FreeBSD validation exposed a real test-isolation bug: - -```text -failed to open coordination store at "/var/db/colibri/colibri.sqlite": -I/O error: Permission denied (os error 13) -``` - -Root cause: `daemon::tests::test_daemon_state_creation` used `DaemonConfig::from_env()`, which on FreeBSD resolves the default production/service SQLite path: - -```text -/var/db/colibri/colibri.sqlite -``` - -Unit tests must not require production service paths or root permissions. - -## Fix - -`test_daemon_state_creation` now overrides: - -- `data_dir` -- `socket_path` -- `db_path` - -with an isolated temp directory before constructing `DaemonState`. - -## Additional hardening - -While reviewing the scheduler, the follow-up also hardened edge cases: - -- cron fields accept leading-zero forms such as `00 12 01 06 01` -- cron schedules fire at most once per matching minute, even with a 30s daemon tick -- `pick_agent` no longer assigns required-capability tasks to zero-match agents -- empty required-capability tasks can still select a general available agent - -## Verification - -Commands run after the fix: - -```sh -cargo fmt --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test --workspace -cargo build --workspace --release -``` - -Result: - -```text -89 tests passed, 0 failed -release build OK -``` - -## Conclusion - -T1.3 scheduler remains accepted, but `a48afa1` is the correct green baseline after FreeBSD-safe test isolation and scheduler edge-case hardening. diff --git a/docs/internal/sessions/2026-05-31-colibri-d360dde-baseline.md b/docs/internal/sessions/2026-05-31-colibri-d360dde-baseline.md deleted file mode 100644 index 0362f5f..0000000 --- a/docs/internal/sessions/2026-05-31-colibri-d360dde-baseline.md +++ /dev/null @@ -1,115 +0,0 @@ -# Colibri Baseline — main @ d360dde - -**Date:** 2026-05-31 -**Commit:** `d360dde` — Merge PR #4 (T1.4 PR2: trimming + auto-escalation) -**Status:** All 4 merged PRs validated on Linux + FreeBSD - -## Merged PRs - -| PR | Title | Content | Tests | -| --- | ------------------------------------- | ---------------------------------------------------- | -------- | -| #1 | PromptAssembly + CacheMetrics structs | Structural wrapper, no behavior change | 5 golden | -| #2 | colibri-skills scaffold | Read-only skill consumer crate, 12 tests | 12 | -| #3 | zot runtime event normalization | AgentRuntime enum, zot event mapper, 16 tests | 16 | -| #4 | cost-aware trimming + auto-escalation | trim_to_budget(), EscalationTrigger, auto_escalate() | 11 | - -## Validation - -### Linux (debby, Debian 13) - -``` -rustc: 1.94.0 (via rustup) -cargo fmt --check ✓ -cargo clippy --workspace --all-targets -- -D warnings ✓ -cargo test --workspace ✓ -git diff --check HEAD ✓ -``` - -### FreeBSD (osa, FreeBSD 15.0-RELEASE) - -``` -host: osa.smilepowered.org -rustc: 1.94.0 -cargo: 1.94.0 -freebsd-version -k: 15.0-RELEASE-p9 -freebsd-version -u: 15.0-RELEASE-p9 -uname -r: 15.0-RELEASE-p8 (kernel p9 installed, pending reboot) - -cargo fmt --check ✓ -cargo clippy --workspace --all-targets -- -D warnings ✓ -cargo test --workspace ✓ -git diff --check HEAD ✓ -``` - -### Test counts - -``` -colibri-daemon: 51 unit + 7 glasspane integration + 1 scheduler integration -colibri-contracts: golden tests -colibri-glasspane: 33 unit (17 Pi + 16 zot) -colibri-skills: 12 unit -colibri-store: integration -Workspace total: all passed, no failures -``` - -### Workspace crates (9) - -``` -colibri-contracts, colibri-deepseek, colibri-runtime, -colibri-glasspane, colibri-daemon, colibri-client, -colibri-glasspane-tui, colibri-store, colibri-skills -``` - -## Known caveats - -- OSA kernel is p8 running but p9 installed — reboot needed for kernel update. - Does not affect Rust validation. -- `colibri-skills` and `zot-runtime-event-adapter` are scaffold-only (Phase 1). - No IO, no SQLite, no daemon integration yet. -- `PromptAssembly::trim_to_budget()` is implemented but not yet wired into - the scheduler or session append path — PR 3 (scheduler injection) pending. -- `CacheMetrics` struct exists but is not populated by any API call path yet. -- API token (`FORGEJO_API_TOKEN`) stored in `~/.hermes/.env` with 0600 perms. - Enables Hermes to create/merge PRs via Forgejo API without web UI. - -## Key files added/changed this session - -``` -docs/T1.4-PROMPT-DISCIPLINE-PLAN.md (new — T1.4 implementation plan) -docs/COLIBRI-CUTOVER-PLAN.md (link to T1.4 plan) -crates/colibri-daemon/src/session.rs (PromptAssembly, CacheMetrics, - trim_to_budget, golden tests) -crates/colibri-daemon/src/cost.rs (EscalationTrigger, auto_escalate) -crates/colibri-glasspane/src/lib.rs (AgentRuntime, zot_event_type, - apply_zot_event, 16 tests) -crates/colibri-skills/ (new crate — structs, 12 tests) -doc/COLIBRI-SKILLS-PLAN.md → docs/ moved (skills plan) -``` - -## Next recommended steps - -In priority order for USB/ISO target: - -1. **Real Pi spawn path proof** — validate that Colibri can spawn a Pi agent - and consume its JSONL events on both Linux and FreeBSD. - -2. **ISO service hardening** — Colibri as an `rc.d` service on FreeBSD, - status visibility, firstboot integration. - -3. **T1.4 PR3 — scheduler prompt injection** — wire trim_to_budget() into - the scheduler's spawn-agent path, cost-aware prompt assembly. - -4. **T1.4 PR4 — startup cache warming** — warm DeepSeek prefix cache on - daemon startup, config-gated. Separate from PR3 because of lifecycle/cost - concerns. - -## Branch hygiene - -``` -main: d360dde (baseline, all merged) -t14-pr2-trimming: merged → can delete -t14-pr1-clean: merged → can delete -docs/t14-prompt-discipline-plan: merged → can delete -feat/colibri-skills-scaffold: merged → can delete -feat/zot-runtime-event-adapter: merged → can delete -``` diff --git a/docs/internal/sessions/2026-05-31-osa-real-pi-binary-proof.md b/docs/internal/sessions/2026-05-31-osa-real-pi-binary-proof.md deleted file mode 100644 index fff4993..0000000 --- a/docs/internal/sessions/2026-05-31-osa-real-pi-binary-proof.md +++ /dev/null @@ -1,156 +0,0 @@ -# OSA real Pi binary spawn proof - -**Date:** 2026-05-31 -**Host:** `osa.smilepowered.org` -**Colibri commit:** `44865f3` — main after PR #8 -**Pi binary:** `/home/clawdie/.npm-global/bin/pi`, version `0.76.0` - -## Verdict - -Real Pi binary spawn path was proven on OSA through `colibri-daemon`. - -The daemon spawned a local executable wrapper, the wrapper executed the real -`pi` binary in JSON mode, Colibri streamed Pi JSONL stdout into glasspane, and -the pane reached `done` with a captured Pi session id. - -This proves the runtime path: - -```text -colibri-daemon spawn-local - → wrapper executable - → real pi --mode json --no-tools --no-context-files --no-session -p ... - → Pi JSONL stdout - → colibri glasspane ingestion - → snapshot state=done + pi_session_id captured -``` - -## Environment - -```text -host: osa.smilepowered.org -rustc: rustc 1.94.0 (4a4ef493e 2026-03-02) (built from a source tarball) -cargo: cargo 1.94.0 (85eff7c80 2026-01-15) (built from a source tarball) -freebsd-version -k: 15.0-RELEASE-p9 -freebsd-version -u: 15.0-RELEASE-p9 -uname: FreeBSD osa.smilepowered.org 15.0-RELEASE-p8 FreeBSD 15.0-RELEASE-p8 releng/15.0-n281036-53054229dcb3 GENERIC amd64 -``` - -Caveat: OSA has p9 installed, but the running kernel is still p8 pending -operator reboot. - -## Direct Pi smoke - -Before the daemon proof, direct Pi JSON mode was verified: - -```sh -pi --version -# 0.76.0 - -pi --mode json --no-tools --no-context-files --no-session -p 'Reply with exactly: OK' -``` - -Result: exit status 0, JSONL emitted, including: - -- `session` -- `agent_start` -- `turn_start` -- `message_start` / `message_update` / `message_end` -- `turn_end` -- `agent_end` - -## Daemon proof setup - -A temporary daemon instance was started with temp-only paths: - -```sh -COLIBRI_DAEMON_DATA_DIR=/tmp/colibri-real-pi-daemon-... -COLIBRI_DAEMON_SOCKET=/tmp/colibri-real-pi-daemon-.../colibri.sock -COLIBRI_DB_PATH=/tmp/colibri-real-pi-daemon-.../colibri.sqlite -COLIBRI_HOST=osa-real-pi-proof -target/debug/colibri-daemon -``` - -The local spawn target was a temporary wrapper script: - -```sh -#!/bin/sh -set -eu -WORKDIR="${TMPDIR:-/tmp}/colibri-real-pi-work-$$" -mkdir -p "$WORKDIR" -cd "$WORKDIR" -exec pi --mode json --no-tools --no-context-files --no-session -p 'Reply with exactly: OK' -``` - -Then Colibri CLI spawned it through the daemon: - -```sh -target/debug/colibri --socket "$SOCKET" spawn-local "$WRAPPER" --session-id real-pi-osa-proof -``` - -Spawn response: - -```json -{ - "agent_id": "f8d07954-b66d-4008-9da2-783818935b22", - "status": "running" -} -``` - -## Observed state transitions - -Polling `colibri snapshot` showed: - -```text -poll 1: idle None None -poll 2: idle None None -poll 3: working 019e7e95-cc0b-751a-8c0b-e6fd3ffab08f None -poll 4: working 019e7e95-cc0b-751a-8c0b-e6fd3ffab08f None -poll 5: done 019e7e95-cc0b-751a-8c0b-e6fd3ffab08f None -``` - -Final snapshot excerpt: - -```json -{ - "schema": "clawdie.glasspane.snapshot.v1", - "host": "osa-real-pi-proof", - "panes": [ - { - "id": "f8d07954-b66d-4008-9da2-783818935b22", - "agent": "/tmp/colibri-real-pi-daemon-.../run-real-pi.sh", - "state": "done", - "pi_session_id": "019e7e95-cc0b-751a-8c0b-e6fd3ffab08f", - "cwd": "/tmp/colibri-real-pi-work-62462" - } - ] -} -``` - -## What this proves - -- OSA has a working `pi` binary. -- `pi --mode json` emits JSONL that Colibri glasspane can ingest. -- `colibri-daemon` can spawn an executable that runs real Pi. -- JSONL stdout streaming from real Pi reaches glasspane. -- Glasspane captures Pi session id separately from Colibri pane id. -- The pane reaches `done` after a real Pi turn completes. - -## Remaining limitations - -- This used a wrapper because `spawn-local` currently treats `model` as a single - executable path and does not pass argv. -- This did not prove a first-class Pi provider command shape in the daemon. -- This did not prove a long-running interactive Pi session, only one noninteractive - JSON-mode turn. -- This did not prove cache warming or scheduler injection behavior with real Pi. - -Suggested follow-up if needed: - -- add first-class argv support for `spawn-local`, or -- add a dedicated Pi provider path that invokes: - -```text -pi --mode json --no-tools --no-context-files --no-session -p -``` - -with deterministic prompt/config handling. diff --git a/packaging/freebsd/clawdie.in b/packaging/freebsd/clawdie.in index 0b314af..13e8e57 100644 --- a/packaging/freebsd/clawdie.in +++ b/packaging/freebsd/clawdie.in @@ -1,6 +1,10 @@ #!/bin/sh # -# clawdie — FreeBSD rc.d service for the simplified Colibri agent. +# clawdie — experimental FreeBSD rc.d service candidate. +# +# The supported live-ISO service is colibri_daemon. This script is kept for +# explicit deployed-service experiments only; do not treat it as the final +# installed-system service contract without fresh acceptance criteria. # # Operator-friendly by design: enable it and start it. The two credentials # (Telegram bot token + DeepSeek key) are normally baked into the binary at @@ -101,7 +105,7 @@ clawdie_prestart() clawdie_poststart() { - # Wait for the Herdr socket to appear (daemon forks, child binds socket). + # Wait for the Colibri control-plane socket to appear (daemon forks, child binds socket). local timeout=10 local waited=0 while [ ! -S "${clawdie_socket}" ] && [ $waited -lt $timeout ]; do