Nightly Build / build (push) Successful in 7m34s
An `execute_task mode="async"` was invisible from the chat that started it.
The only trace was the receipt in the transcript and a row on the Tasks page
— which does not say *which* of those rows the assistant just spawned — so
"is it still going?" had no answer where the question is asked.
Worse, a task that did not simply succeed never came back at all. `run_job`
branched on `Ok`/`Err` first and routed by `job.kind` only inside the `Ok`
arm, so a failure or a kill left through the `Err` arm's unconditional
`hub.notify` — the home source (`/sethome`), worded "Cron job … failed" —
while the parent conversation sat waiting for a `task_completed` that would
never arrive. The wrong chat, and a wedged one.
The fix is a shape, not a branch: one `JobOutcome` classification, then one
`match job.kind` delivery site for every ending. An async task now ends in
its parent conversation whatever happened to it. The sink has a single
channel deliberately — to the model reading it, "it broke" is a result like
any other and must not be overlookable — so a failure is delivered as prose,
carrying whatever partial output the run produced, which is usually the only
clue about why. A cron job keeps the home notification: it belongs to nobody's
conversation. Cancellation becomes a third outcome rather than a flavour of
failure (`job_runs.status` has always had `'cancelled'` in its CHECK and
nothing ever wrote it), classified off the new typed `TurnCancelled` error so
nothing keys on a message string.
The strip above the composer is the visible half. `ServerEvent::TaskUpdate`
announces state to the source of the parent conversation only; the list is
`renderTaskStrip` (shared by the desktop copilot and the mobile chat), fed by
state on `ChatSession`. Each row links to `#session/{id}` — the page that
already shows, live, what a background agent is doing, and without which
"a task is running" is a fact you can do nothing with. Stopping is the
existing kill endpoint. A finished row clears itself after 20 s (its result
is in the conversation by then); a failed one stays until dismissed, and the
dismissal is remembered across reloads.
`GET /api/{source}/tasks` is what makes the strip survive a browser refresh:
the event is a broadcast with no replay, so without a load-time read a reload
would empty a chat that still has work running under it. It answers with the
running tasks plus failures from the last 30 minutes — the two states a person
can still act on. Successes are absent on purpose. Its window compares through
`datetime()` on both sides: `completed_at` is RFC 3339 and the cutoff is
SQLite-shaped, and `'T' > ' '` would let every same-day row through a window
meant to exclude it.
Not addressed, and worth doing next: a cron job's result should go where its
creator says, not always to the home chat.
87 lines
3.8 KiB
TOML
87 lines
3.8 KiB
TOML
[workspace]
|
|
members = [
|
|
".",
|
|
"crates/agent-loop",
|
|
"crates/skald-core",
|
|
"crates/skald-setup",
|
|
"crates/honcho-client",
|
|
"crates/core-api",
|
|
"crates/mcp-client",
|
|
"crates/plugin-tailscale-remote",
|
|
"crates/plugin-telegram-bot",
|
|
"crates/plugin-mobile-connector",
|
|
"crates/plugin-transcribe-whisper-local",
|
|
"crates/plugin-comfyui",
|
|
"crates/plugin-tts-orpheus-3b",
|
|
"crates/plugin-tts-kokoro",
|
|
"crates/plugin-elevenlabs",
|
|
"crates/plugin-honcho",
|
|
"crates/skald-relay-common",
|
|
"crates/skald-relay-server",
|
|
"crates/skald-relay-client",
|
|
]
|
|
resolver = "2"
|
|
|
|
[package]
|
|
name = "skald"
|
|
version = "0.2.0"
|
|
edition = "2024"
|
|
|
|
[features]
|
|
default = ["whisper-local"]
|
|
whisper-local = ["dep:plugin-transcribe-whisper-local"]
|
|
# Embedded (pure-Rust) Tailscale provider. Off by default: the `tailscale` crate
|
|
# forces the `aws-lc-rs` crypto backend (a cmake/NASM C build) back into the
|
|
# tree, defeating the ring-only crypto path. The recommended `tailscale_sys`
|
|
# provider (system tailscaled) stays available without it. Enable only for a
|
|
# self-contained embedded mesh (re-introduces the aws-lc-rs C build).
|
|
embedded-tailscale = ["plugin-tailscale-remote/remote-tailscale"]
|
|
|
|
[dependencies]
|
|
skald-core = { path = "crates/skald-core" }
|
|
|
|
axum = { version = "0.8", features = ["ws", "multipart"] }
|
|
tokio = { version = "1.52.3", features = ["full"] }
|
|
tokio-util = { version = "0.7", features = ["rt"] }
|
|
futures = "0.3"
|
|
tower-http = { version = "0.7.0", features = ["fs", "compression-gzip", "compression-br", "set-header"] }
|
|
tower = "0.5"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_yaml = "0.9"
|
|
anyhow = "1"
|
|
# Verifies the SHA-256 digests the connector marketplace declares for each file
|
|
# it serves (src/frontend/api/marketplace.rs).
|
|
sha2 = "0.10"
|
|
sqlx = { version = "0.9.0", features = ["runtime-tokio", "sqlite"] }
|
|
reqwest = { version = "0.13.4", default-features = false, features = ["rustls-no-provider", "charset", "http2", "system-proxy", "json", "multipart"] }
|
|
# rustls is pinned as a direct dependency solely to select the crypto provider:
|
|
# `ring` instead of the default `aws-lc-rs`, avoiding aws-lc's cmake/NASM build.
|
|
# Every reqwest client uses `rustls-no-provider`, so exactly one process-wide
|
|
# provider is installed in main() before any TLS handshake.
|
|
#
|
|
# TLS therefore never touches OpenSSL. libcrypto is nonetheless in the tree now,
|
|
# vendored and statically linked for SQLCipher (see `libsqlite3-sys` in
|
|
# crates/skald-core) — so the binary stays self-contained, but "no OpenSSL
|
|
# anywhere" is no longer true.
|
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
|
|
async-trait = "0.1"
|
|
serde_json = "1"
|
|
indexmap = { version = "2", features = ["serde"] }
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
tracing-appender = "0.2"
|
|
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
|
notify = "8"
|
|
honcho-client = { path = "crates/honcho-client" }
|
|
core-api = { path = "crates/core-api" }
|
|
mcp-client = { path = "crates/mcp-client" }
|
|
plugin-tailscale-remote = { path = "crates/plugin-tailscale-remote" }
|
|
plugin-telegram-bot = { path = "crates/plugin-telegram-bot" }
|
|
plugin-mobile-connector = { path = "crates/plugin-mobile-connector" }
|
|
plugin-transcribe-whisper-local = { path = "crates/plugin-transcribe-whisper-local", optional = true }
|
|
plugin-comfyui = { path = "crates/plugin-comfyui" }
|
|
plugin-tts-orpheus-3b = { path = "crates/plugin-tts-orpheus-3b" }
|
|
plugin-tts-kokoro = { path = "crates/plugin-tts-kokoro" }
|
|
plugin-elevenlabs = { path = "crates/plugin-elevenlabs" }
|
|
plugin-honcho = { path = "crates/plugin-honcho" }
|