chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:12:33 +08:00
commit aacb60a4af
3387 changed files with 981307 additions and 0 deletions
+453
View File
@@ -0,0 +1,453 @@
# LLM Ensemble Design: Static Lineups & Dynamic Router Selection
`llm_ensemble` runs a **B5 fusion** turn: several *proposer* models each draft
an answer, and one *aggregator* model fuses those drafts into the final
response. This document describes how the **set of models** is chosen for a
turn. It does not cover the ensemble runtime mechanics (streaming, timeouts,
quorum, fallback) — only model selection.
## Why an ensemble instead of a single model
Any single model has a fixed set of blind spots: the failure modes of its
training data, its decoding randomness, and its particular biases. Asking that
one model again doesn't remove them — it re-rolls the *same* distribution. An
ensemble attacks the problem from a different angle: draft an answer with
several *different* models, then have an aggregator reconcile them. The wins
over a single-model turn:
- **Error cancellation / higher accuracy.** Independent models rarely make the
*same* mistake on the same input. When drafts disagree, the aggregator can
cross-check them and keep the answer the majority supports; when they agree,
that agreement is real signal that the answer is solid. Idiosyncratic
one-off errors get outvoted instead of shipped.
- **Coverage through diversity.** Different vendors/families/architectures have
genuinely different strengths — one is better at code, another at long-form
reasoning, another at careful instruction-following. A lineup that spans them
covers more of the input space than any single model's strong suit. This is
exactly why selection *rewards diversity* (distinct vendor/family/
architecture) rather than picking the top-N by raw quality.
- **Robustness & availability.** A single model is a single point of failure —
one timeout, rate-limit, or degraded response fails the whole turn. With a
quorum of proposers the turn still succeeds as long as *enough* drafts come
back, and the aggregator simply fuses what arrived.
- **Reduced variance.** Fusing several drafts smooths out per-call sampling
noise, so repeated runs of the same prompt are more stable and less
sensitive to an unlucky roll of the dice on any one model.
- **A critic pass, not just a vote.** The aggregator is a model in its own
right: it can spot a draft that's confidently wrong, prefer the
better-reasoned answer over the more verbose one, and synthesize the best
parts of several drafts — a step a single-model turn never gets.
The cost is real — an N-proposer turn spends roughly N+1 model calls and its
latency is bounded by the slowest proposer plus the aggregator. The selection
strategies below exist to spend that budget well: match ensemble size and
composition to how hard the turn actually is, rather than always paying for the
largest lineup. Lower-difficulty turns get a small, cheap lineup; harder turns
get more proposers and stronger critics.
## Selection strategies
There are three selection strategies, dispatched by
`llm_ensemble.selection_mode` in
`build_ensemble_provider_from_config`
(`src/opensquilla/provider/ensemble.py`):
| `selection_mode` | Family | Status |
|------------------|--------|--------|
| `static_openrouter_b5` | Static lineup | Default for fresh configs |
| `static_tokenrhythm_b5` | Static lineup | Supported |
| `custom_b5` | Static lineup (user-authored) | Supported |
| `router_dynamic` | Dynamic selection | Legacy |
The first two families are **static**: the lineup is fixed ahead of the turn,
either from a packaged preset or from an explicit user-authored list. The last
is **dynamic**: the lineup is scored and assembled per turn from the router's
own tier decision.
Fresh configs default to `static_openrouter_b5`. The Web UI offers only the
static families (preset + custom); `router_dynamic` is no longer offered there
and stored configs surface a one-click migration to `custom_b5`. Direct
TOML/RPC configuration keeps working for every mode.
---
# Part 1 — Static Lineups (current design)
A static lineup is fixed before the turn runs: a set of proposer models plus one
aggregator model, all known ahead of time. No per-turn scoring happens. Two
variants share this shape:
- **Presets** — `static_openrouter_b5` / `static_tokenrhythm_b5`: packaged,
hard-coded lineups on a single provider.
- **Custom** — `custom_b5`: an explicit user-authored lineup with
role-labelled candidates and a single aggregator.
Both variants belong to the same **fixed-lineup defaults family**
(`is_static_b5` in the builder) and therefore inherit the same runtime defaults
(quorum, timeouts, no shuffle, quorum grace) — see
[Shared fixed-lineup defaults](#shared-fixed-lineup-defaults).
## 1.1 Static presets
Source: `_build_static_b5_members`, `STATIC_B5_PROFILES`
(`src/opensquilla/provider/ensemble.py`).
Each preset is a `StaticB5Profile` — four fixed proposers plus one aggregator,
all bound to a single provider:
| Profile | Provider | Proposers | Aggregator |
|---------|----------|-----------|------------|
| `static_openrouter_b5` | `openrouter` | `deepseek/deepseek-v4-pro`, `z-ai/glm-5.2`, `moonshotai/kimi-k2.7-code`, `qwen/qwen3.7-max` | `z-ai/glm-5.2` |
| `static_tokenrhythm_b5` | `tokenrhythm` | `deepseek-v4-pro`, `glm-5.2`, `kimi-k2.7-code`, `qwen3.7-max` | `glm-5.2` |
The TokenRhythm profile is a mirror of the OpenRouter one: same aggregation
shape and defaults, the same four models, only the provider and the model-id
naming differ (OpenRouter-style `vendor/model` slugs vs. TokenRhythm's bare
names).
`_build_static_b5_members` simply materializes the profile: each proposer model
becomes an `EnsembleMemberConfig` labeled `proposer_1..N`, the aggregator model
becomes one labeled `aggregator`, and the selection plan records the profile
name, proposer/aggregator models, and proposer count. There is nothing to score
— the lineup is the profile.
### Credential gate
`static_b5_credential_available` decides whether the ensemble may run: it
resolves an API key for every member (all four proposers + aggregator) using the
same key-resolution order as the runtime (see
[Member provider resolution](#member-provider-resolution)). A user whose active
provider differs but whose environment carries the profile provider's env key
(e.g. `OPENROUTER_API_KEY`, `TOKENRHYTHM_API_KEY`) is treated as opted in. If any
member cannot resolve a key, the ensemble is skipped rather than posting a turn
upstream with an empty bearer token.
## 1.2 Custom lineup (`custom_b5`)
Source: `_build_custom_b5_members`, `_custom_b5_candidates`
(`src/opensquilla/provider/ensemble.py`); schema `LlmEnsembleCandidateConfig`
(`src/opensquilla/gateway/config.py`).
`custom_b5` lets an operator author the lineup explicitly via
`llm_ensemble.candidates`. Each candidate row carries:
- **`provider`** / **`model`** — required, non-empty; provider is lower-cased.
- **`role`** — advisory label, one of `""` (unassigned), `primary`, `contrast`,
`fast_check`, `critic`, or the structural `aggregator`. Unknown values coerce
to `""` instead of failing, so a hand-edited config never blocks boot.
- **`enabled`** — disabled rows are kept for read compatibility but never
counted or run.
Lineup assembly (`_build_custom_b5_members`):
1. Every enabled row whose role is **not** `aggregator` runs as a proposer,
labeled by its role (or `proposer_N` when unassigned).
2. The single row with role `aggregator` fuses the drafts. Proposer rows dedupe
on `(provider, model)`; the aggregator row may legitimately reuse a
proposer's model (a model both drafts and fuses).
3. **Fallback:** if no `aggregator` row exists, the aggregator falls back to the
currently routed model — the same model the user would have gotten without
the ensemble — so a proposer-only config still runs instead of erroring at
turn time. The plan records `aggregator.source` as `candidate_role` or
`inherited_model` accordingly.
### Lineup bounds & validation
Enforced by `LlmEnsembleConfig._validate_custom_b5_lineup`
(`src/opensquilla/gateway/config.py`), checked **only** when
`selection_mode == "custom_b5"` (presets carry fixed lineups; `router_dynamic`
selects per turn):
- At most **one** enabled candidate may carry role `aggregator`.
- Enabled proposer count must stay within
`[CUSTOM_B5_MIN_PROPOSERS=2, CUSTOM_B5_MAX_PROPOSERS=6]`.
- Total per-turn calls are capped at `CUSTOM_B5_MAX_TOTAL_CALLS=8`
(proposers + aggregator).
### Readiness gate
`custom_b5_lineup_ready` returns `(ready, reason)` before wrapping the turn. It
fails closed with a machine-readable reason when the lineup is not runnable:
`no_proposers`, `unknown_provider:<p>`, or `missing_credential:<p>` (a member
whose provider requires a key but resolves none). This mirrors the static-preset
gate — a member with an empty bearer token would post the conversation upstream
unauthenticated, so the wrap is skipped.
## 1.3 Shared fixed-lineup defaults
Both static families set `is_static_b5 = True` in
`build_ensemble_provider_from_config`, which swaps the legacy per-turn defaults
for the fixed-lineup family defaults. The swap is **only** applied when the
configured value still equals the legacy default (`_static_default_if_legacy`),
so any operator override is preserved:
| Parameter | Legacy (`router_dynamic`) | Fixed-lineup default |
|-----------|---------------------------|----------------------|
| `min_successful_proposers` | 1 | 3 (presets) / `N-1` (custom, "all but one") |
| `proposer_timeout_seconds` | 3600 | 300 |
| `aggregator_timeout_seconds` | 3600 | 480 |
| `shuffle_candidates` | `True` | `False` |
| `quorum_grace_seconds` | 0 | 30 |
`min_successful_proposers` is additionally clamped down to the actual proposer
count. Both the configured and effective values (min-success, timeouts, shuffle)
are recorded in the selection plan for debugging.
## 1.4 Member provider resolution
Every member (static or custom) resolves its concrete `ProviderConfig` through
`_member_provider_config`, which layers member intent over the inherited/routed
provider config:
- **API key** — a member-level `api_key_env` env var if set; else the inherited
key when the member's provider matches the active provider; else the provider
registry's env key (e.g. `OPENROUTER_API_KEY`).
- **`base_url`** — member override, else the inherited base URL (same provider)
or the provider spec's default base URL.
- **`proxy` / `org_id` / `provider_routing`** — inherited only when the member
shares the active provider; otherwise reset.
This is what lets a static/custom lineup run against a provider the user isn't
actively routing to, as long as that provider's credential is present in the
environment.
## 1.5 Configuration surface
```toml
[llm_ensemble]
enabled = true
selection_mode = "static_openrouter_b5" # or static_tokenrhythm_b5 / custom_b5
```
Custom lineup:
```toml
[llm_ensemble]
enabled = true
selection_mode = "custom_b5"
[[llm_ensemble.candidates]]
provider = "openrouter"
model = "deepseek/deepseek-v4-pro"
role = "primary"
[[llm_ensemble.candidates]]
provider = "openrouter"
model = "z-ai/glm-5.2"
role = "contrast"
[[llm_ensemble.candidates]]
provider = "openrouter"
model = "z-ai/glm-5.2"
role = "aggregator"
```
Static presets expose no lineup tuning — the models are fixed in code. Custom
lineups are tuned entirely through the `candidates` list (subject to the bounds
above). Both share the fixed-lineup runtime defaults, which an operator may
still override explicitly (`min_successful_proposers`,
`proposer_timeout_seconds`, `aggregator_timeout_seconds`, `shuffle_candidates`).
---
# Part 2 — `router_dynamic` Selection (legacy)
> **Status: legacy.** `router_dynamic` remains fully supported for existing
> configs but is no longer offered in the Web UI. Stored `router_dynamic`
> configs surface a one-click migration to `custom_b5`. Direct TOML/RPC
> configuration keeps working as described below.
`router_dynamic` is the dynamic model-selection strategy: instead of a fixed
lineup, it picks proposers and the aggregator **per turn**, driven by
SquillaRouter's tier decision for that turn. Enable it with
`llm_ensemble.selection_mode = "router_dynamic"`.
Source: `src/opensquilla/provider/ensemble.py`
(`_candidate_pool`, `_score_dynamic_candidate`, `_select_dynamic_candidate`,
`_build_router_dynamic_members`).
## 2.1 Why dynamic selection
A fixed proposer/aggregator list can't adapt to the model actually chosen for a
turn, and forces operators to hand-tune which models pair well together at each
router tier. `router_dynamic` instead:
- reuses the model SquillaRouter already picked for the turn as the **anchor**
proposer, so the ensemble never contradicts the router's own tier decision;
- fills the remaining proposer slots and the aggregator slot by scoring a pool
of candidate models against a per-tier "slot template";
- penalizes re-selecting a model that's already in the ensemble, so proposers
stay diverse instead of collapsing onto a few high-quality models.
## 2.2 Inputs
`_build_router_dynamic_members` takes three things:
1. **`inherited_provider_config`** — the provider/model SquillaRouter already
resolved for this turn (becomes the anchor).
2. **`turn_metadata`** — carries `routed_tier` (`c0``c3`), `routing_confidence`
(0.01.0), and `routing_extra` (`final_tier`/`base_tier` fallbacks used if
`routed_tier` is missing). Defaults to tier `c1` if nothing usable is found.
3. **`config`** — `llm_ensemble.model_options` and `squilla_router.tiers`,
used to build the candidate pool.
## 2.3 Candidate pool
`_candidate_pool` assembles a deduplicated list of `(provider, model)`
candidates, in this order:
1. **Router anchor** — the inherited provider/model (`source="router_anchor"`).
This is always `pool[0]` and always becomes the first proposer.
2. **`llm_ensemble.model_options`** — the operator-configured candidate list
(`source="model_options"`). If a model string contains `/` it's assumed to
be an OpenRouter-style id and routed via `openrouter`; otherwise it inherits
the anchor's provider.
3. **`squilla_router.tiers[*].model`** — every model configured for a
SquillaRouter tier (`source="router_tier:<tier>"`), so tier-specific models
the operator has wired into the router are eligible even if not listed in
`model_options`.
Each candidate is annotated with priors from `_DYNAMIC_MODEL_CATALOG` — a
built-in table of ~14 known models with `tier`, `quality` (01), `cost_latency`
(01, higher = cheaper/faster), `family`, `vendor`, and `architecture`. Models
not in the catalog fall back to tier-average priors (`_tier_quality_prior`,
`_tier_cost_latency_prior`) derived from the model string or tier hint.
## 2.4 Slot templates
Each router tier maps to an ordered list of proposer "slots"
(`_DYNAMIC_TIER_SLOTS`):
| Tier | Slots |
|------|-------|
| `c0` | `anchor`, `cheap_contrast` |
| `c1` | `anchor`, `balanced_contrast` |
| `c2` | `anchor`, `adjacent_tier_check`, `orthogonal_family` |
| `c3` | `anchor`, `strong_critic`, `orthogonal_family`, `fast_sanity` |
Lower tiers (cheap/simple turns) get a small, cost-biased ensemble; higher
tiers (hard turns) get more proposers with slots biased toward quality and
contrast. The `anchor` slot is always filled by the router's own model and is
never scored — it's taken as-is.
Each tier also maps to an aggregator slot (`_DYNAMIC_AGGREGATOR_SLOT`):
`c0→aggregator_fast`, `c1→aggregator_balanced`, `c2`/`c3→aggregator_strong`.
## 2.5 Scoring a candidate for a slot
For every non-anchor slot, every pool candidate is scored and the best one is
selected (`_select_dynamic_candidate``_score_dynamic_candidate`):
```
score = weights.quality * quality_prior
+ weights.affinity * router_affinity_score
+ weights.diversity * diversity_score
+ weights.cost * cost_latency_prior
+ weights.role * role_match_score(slot)
- duplicate_penalty
```
Each slot has its own weight vector (`_DYNAMIC_SLOT_WEIGHTS`), e.g.
`cheap_contrast` weights `cost` and `role` heavily and `affinity` lightly,
while `strong_critic` weights `quality` and `role` heavily and `cost` almost
not at all.
### Score components
- **`router_affinity_score`** — how close the candidate's tier prior is to the
turn's `routed_tier`, scaled by `routing_confidence`. Low router confidence
relaxes tier matching instead of forcing a brittle lock, since a
low-confidence route is itself uncertain about the right tier.
- **`diversity_score`** — rewards a candidate whose family/vendor/provider/
tier/architecture aren't already represented among the proposers picked so
far in this turn (checked incrementally, slot by slot).
- **`role_match_score`** — slot-specific logic (see below), combining tier
targeting, contrast against the anchor, quality, or cost depending on what
that slot is supposed to contribute.
- **`duplicate_penalty`** — `_DYNAMIC_SELECTED_PENALTY[slot] * times_already_selected`.
Selecting the same `(provider, model)` again is allowed but costs
increasingly more as the same model keeps winning slots.
### Role match by slot
`_role_match_score` differs by slot — this is where each slot's intent is
actually encoded:
- **`cheap_contrast`** — favors tier `c0`/`c1`, contrast with the anchor, and
cost/latency. A cheap "second opinion."
- **`balanced_contrast`** — favors tier `c1`/`c2`, contrast, and quality.
- **`adjacent_tier_check`** — favors a tier one step above/below the routed
tier (`adjacent_distance == 1`), plus quality. Checks whether a
slightly-different-strength model agrees.
- **`orthogonal_family`** — favors contrast and diversity above all — a
model from a different vendor/family/architecture than the anchor.
- **`strong_critic`** — favors tier `c3` and quality heavily — the strongest
available model as a critic, used only at higher tiers.
- **`fast_sanity`** — favors tier `c0`/`c1` and cost/latency — a fast,
cheap sanity check, used only at `c3`.
- **`aggregator_fast` / `aggregator_balanced` / `aggregator_strong`** — each
balances tier targeting and quality differently; `aggregator_strong`
weights quality highest and cost lowest, since the aggregator's output is
the final response.
### Tie-breaking
Candidates are sorted by `(score, quality_prior, cost_latency_prior,
-pool_index)` descending, so ties fall back to higher quality, then higher
cost/latency score, then earlier pool position (closer to the anchor/operator-
configured list) wins.
## 2.6 Selection order
`_build_router_dynamic_members` runs slots in the tier's template order:
1. `anchor` — taken directly, no scoring.
2. Remaining proposer slots, in order — each selection is added to `selected`
and `selected_counts` before the next slot is scored, so later slots see
updated diversity/duplicate state.
3. The aggregator slot, scored last, against the same accumulated `selected`
state as the proposers (so it also gets a duplicate penalty if it repeats
a proposer's model).
## 2.7 Output
The function returns `(profile_name, proposers, aggregator, selection_plan)`:
- `profile_name``"router_dynamic/<tier>"`, e.g. `"router_dynamic/c2"`.
- `proposers` — one `EnsembleMemberConfig` per slot, labeled by slot name
(`anchor`, `cheap_contrast`, ...).
- `aggregator` — one `EnsembleMemberConfig`, labeled `aggregator`.
- `selection_plan` — a full trace for observability, including the resolved
tier/confidence, the anchor, the slot template, per-slot score breakdowns
(`_score_trace`, including the top-3 scored candidates per slot for
debugging near-misses), the aggregator's score breakdown, the full
candidate pool, and `duplicate_policy: "selected_penalty"`.
`build_ensemble_provider_from_config` (the public entrypoint) additionally
clamps `min_successful_proposers` down to `len(proposers)` if the configured
value exceeds how many proposer slots the tier's template actually produced
— e.g. configuring `min_successful_proposers=4` at tier `c0` (2 slots) yields
an effective minimum of 2. Both the configured and effective values are
recorded in `selection_plan` for debugging.
## 2.8 Configuration surface
```toml
[llm_ensemble]
enabled = true
selection_mode = "router_dynamic"
```
What operators can tune:
- `llm_ensemble.model_options` — extends the candidate pool beyond the router
anchor and configured router tiers.
- `llm_ensemble.min_successful_proposers` — desired minimum successful
proposers (clamped per-turn as described above).
- `squilla_router.tiers[*].model` — indirectly expands the candidate pool and
determines which model becomes the anchor for a given tier.
There is no operator control over slot templates, weights, or the model
catalog priors — those are fixed in code. Unlike the static families,
`router_dynamic` keeps the legacy runtime defaults (timeouts 3600s,
`shuffle_candidates=True`, `min_successful_proposers=1`).
@@ -0,0 +1,395 @@
# Attachment Drag-and-Drop Upload Spec
Date: 2026-06-29
Status: Draft
## Problem
OpenSquilla should let users attach files by dragging them into the chat surface
in both the browser Web UI and the Electron desktop client. The behavior must be
consistent across surfaces and must use the existing gateway attachment
ingestion path rather than adding a parallel desktop-only file pipeline.
The current code already contains most of the upload path:
- `opensquilla-webui/src/views/ChatView.vue` listens for drag/drop on the chat
thread and passes dropped files to `addAttachment`.
- `opensquilla-webui/src/composables/chat/useChatAttachments.ts` validates file
type and size, inlines small files, and stages large files through
`/api/v1/files/upload`.
- `opensquilla-webui/src/composables/chat/useChatSend.ts` sends staged files as
`{ type, file_uuid, mime, name }`.
- `src/opensquilla/gateway/uploads.py` exposes the multipart upload route and
stores staged bytes behind an opaque `file_uuid`.
- `src/opensquilla/gateway/attachment_ingest.py` resolves `file_uuid` entries
into transcript material references before a turn runs.
- `desktop/electron/src/main.ts` creates a sandboxed, context-isolated browser
window; `desktop/electron/src/preload.cts` exposes only explicit desktop APIs.
The missing product-quality layer is a complete, reviewed drag-upload UX and
test contract that covers both browser and desktop surfaces, including auth,
error states, retry behavior, and cross-platform desktop validation.
## Goals
- Support dragging files from the OS/browser file picker into the Web UI chat
surface.
- Support the same drag behavior in the Electron desktop client without a
separate upload implementation.
- Preserve the existing attachment policy: max count, per-category size caps,
total size cap, MIME sniffing, and staged upload TTL. Any file type is
admitted; the MIME set only routes representation (rendered families are
extracted or inlined, everything else stages as an opaque workspace file).
- Make upload progress and failure states visible in the composer.
- Prevent sending while any attachment is still reading or uploading.
- Ensure token-authenticated gateways can upload files by using the same bearer
token source as the WebSocket/RPC client.
- Keep raw local file paths out of renderer code and out of chat payloads.
- Persist only stable transcript material references after `chat.send` accepts
the turn; do not persist `file_uuid` in transcripts.
## Non-Goals
- Do not implement folder drag-and-drop.
- Do not upload directory trees or recursively enumerate local paths.
- Do not expose arbitrary local filesystem reads through Electron preload.
- Do not make Electron bypass the gateway upload endpoint.
- Do not add resumable/chunked uploads in this iteration.
- Do not add per-type client-side gating: admission is any-type (opaque
staging), and rejections happen only on size/count policy, mirroring the
gateway's category router in `contracts/attachments.py`.
- Do not support remote URL drag/drop as file upload unless the browser supplies
a real `File` object.
## Existing Surfaces
### Web UI
- `ChatView.vue` owns high-level chat surface events, including drag/drop and
paste.
- `ChatComposer.vue` renders pending attachment chips and the file input.
- `useChatAttachments.ts` owns attachment state, MIME resolution, inline reads,
staged uploads, and local validation.
- `useChatSend.ts` serializes pending attachments into the `chat.send` RPC
payload.
### Gateway
- `contracts/attachments.py` defines allowed media types and size limits.
- `uploads.py` handles multipart staging and returns `file_uuid`.
- `attachment_ingest.py` validates inline and staged attachments, writes
transcript material, and returns consumed staged UUIDs.
- `rpc_sessions.py` evicts consumed `file_uuid` values only after the turn has
been accepted into the runtime.
### Desktop
- The Electron renderer runs the same Vue Web UI as the browser.
- The Electron window is sandboxed and context-isolated.
- The preload API currently exposes gateway status, settings, onboarding, and
artifact open helpers; it does not expose raw file reads.
## Recommended Design
Use one shared Web UI upload path for browser and Electron:
```text
User drops files
-> ChatView drop handler
-> useChatAttachments.addAttachments(files)
-> inline small files OR POST large files to /api/v1/files/upload
-> ChatComposer renders pending chips
-> useChatSend serializes attachments
-> chat.send RPC
-> gateway attachment_ingest resolves file_uuid
-> turn runtime receives stable attachment refs
```
Electron should not get a special upload bridge in the first iteration. When a
file is dragged from the OS into the Electron browser window, Chromium exposes
it to renderer code as a `File`. That is the same object shape the browser path
uses, so the renderer can call the same composable.
Add a desktop-specific bridge only if manual validation proves a platform cannot
produce usable `DataTransfer.files`, or if a later feature explicitly needs
native behavior such as folder selection, revealing local file paths, or
watching local files for changes.
## User Experience
### Drop Zone
The primary drop zone is the chat thread/composer area.
Expected behavior:
- `dragenter` / `dragover` with at least one file item shows a visible drop
affordance.
- `dragleave` uses a drag-depth counter or equivalent containment check so the
affordance does not flicker when moving across child elements.
- `drop` extracts only `File` objects and ignores non-file drag data.
- Dropping files focuses the composer and appends attachments to the pending
list.
- Unsupported items produce a toast and do not block valid files in the same
drop.
### Attachment Chips
Each pending attachment chip should show:
- name;
- MIME or concise type label;
- size;
- state: reading, uploading, ready, failed;
- thumbnail for image inline attachments when available;
- remove button;
- retry button for staged upload failures.
Sending is disabled while any attachment is `reading` or `uploading`.
### Empty Message Behavior
If the user sends with attachments and no text, the existing fallback message
`Describe these attachments` remains acceptable.
The visible bubble should still show the user's display text when present and
attachment chips/previews when attachments exist.
## Attachment State Model
Use an explicit state machine in `useChatAttachments.ts`.
Recommended internal states:
| State | Meaning | Sendable |
| --- | --- | --- |
| `inline_pending` | FileReader is reading a small file. | No |
| `inline` | Base64 data is ready in the pending payload. | Yes |
| `uploading` | Multipart upload is in progress. | No |
| `staged` | Gateway returned a `file_uuid`. | Yes |
| `failed` | Read or upload failed and can be removed or retried. | No |
The public type may keep the existing names, but code paths should treat them as
states rather than ad-hoc variants.
`addAttachment(file)` should be implemented in terms of
`addAttachments(files: File[])` so file picker, paste, and drag/drop share batch
validation behavior.
## Data Contracts
### Inline Attachment Payload
Small files are sent through `chat.send` as:
```json
{
"type": "image/png",
"mime": "image/png",
"name": "screenshot.png",
"data": "<base64>"
}
```
### Staged Upload Request
Large files are uploaded first:
```http
POST /api/v1/files/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>
```
Multipart fields:
- `file`: the file bytes and filename;
- `mime`: the client-resolved MIME, used only as a claim; gateway revalidates.
Expected success response:
```json
{
"file_uuid": "u-...",
"filename": "report.pdf",
"mime": "application/pdf",
"size": 12345
}
```
### Staged `chat.send` Payload
After staging, `chat.send` sends:
```json
{
"type": "application/pdf",
"mime": "application/pdf",
"name": "report.pdf",
"file_uuid": "u-..."
}
```
The gateway resolves this into a stable attachment ref and must not persist
`file_uuid` into transcript envelopes.
## Auth Requirements
The multipart upload route intentionally requires header-based auth in token
mode. Query-string token auth must remain rejected for uploads.
Vue upload requests must therefore include the same token source used by the
WebSocket/RPC connection:
- read `sessionStorage.getItem("opensquilla.wsToken")`;
- when present, set `Authorization: Bearer <token>`;
- keep `credentials: "same-origin"` for same-origin cookies and browser policy;
- never place the token in the upload URL.
This is important because the current Vue staged upload path uses
`credentials: "same-origin"` but does not include an authorization header. The
legacy static chat upload path already includes the bearer token and should be
used as the behavior reference.
## Security and Safety Rules
- Frontend validation is advisory UX only; gateway validation remains
authoritative.
- The renderer must never send local filesystem paths in chat payloads.
- Electron preload must not expose an arbitrary `readFile(path)` API for this
feature.
- `file_uuid` is a short-lived upload-store identifier, not a durable reference.
- Staged upload bytes are evicted only after the turn is accepted.
- Failed `chat.send` after successful staging must keep the staged file retryable
until TTL expiry.
- MIME sniffing mismatches are handled by gateway policy, not by trusting the
browser-provided `File.type`.
- Directory entries and zero-byte files should be rejected with clear UX.
## Implementation Plan
### Frontend
1. Add `addAttachments(files: File[])` in `useChatAttachments.ts`.
2. Route file picker, paste, and drop through `addAttachments`.
3. Add drag-depth or containment-safe drop-zone state in `ChatView.vue`.
4. Show a drop affordance only when the drag payload contains files.
5. Add upload auth headers to staged upload requests.
6. Add retry handling for staged upload failures.
7. Keep `ChatComposer.vue` presentational: props down, events up.
8. Ensure `useChatSend.ts` does not clear failed attachments as if they were
successfully queued.
### Gateway
1. Keep `/api/v1/files/upload` as the only staged upload endpoint.
2. Keep the upload route header-token requirement unchanged.
3. Confirm `UploadStore` and `attachment_ingest` reject unsupported MIME, over
size, unknown UUID, restart-lost UUID, and total size overflow.
4. Add targeted tests only where the new frontend contract exposes gaps.
### Desktop
1. Validate drag/drop in packaged or dev Electron on macOS, Windows, and Linux
where available.
2. Keep Electron preload unchanged unless validation proves native bridging is
required.
3. If a bridge becomes required, expose only a narrow capability such as
`stageDroppedFile(handle)` and keep all byte validation in the gateway.
## Test Plan
### Frontend Unit Tests
- `resolveAttachmentMime()` prefers allowed browser MIME and falls back to
extension.
- Unknown UTF-8 text degrades to `text/plain`.
- Unknown binary files are rejected.
- Oversize files are rejected by per-MIME cap.
- `addAttachments()` handles mixed valid and invalid files without dropping the
valid ones.
- Inline files transition from `inline_pending` to `inline`.
- Large stageable files transition from `uploading` to `staged`.
- Failed staged upload leaves a failed/removable state.
- Upload requests include `Authorization: Bearer <token>` when the token exists.
- Upload requests do not put the token in the URL.
### Vue Component Tests
- Dragging files over the chat surface shows the drop affordance.
- Dragging non-file data does not show the drop affordance.
- Dropping files calls the attachment composable once with all dropped files.
- Sending is disabled while any attachment is reading or uploading.
- Removing an attachment updates the pending list without mutating child props.
### Gateway Tests
- Multipart upload rejects missing auth header in token mode.
- Multipart upload accepts bearer token in token mode.
- Multipart upload rejects query-token-only auth.
- Staged `file_uuid` resolution returns an attachment ref with no `file_uuid` or
inline `data`.
- Restart-lost UUID produces the re-upload error path.
- Successful turn acceptance evicts consumed UUIDs.
- Failed turn acceptance does not evict consumed UUIDs.
### Browser E2E
- Drop one small image into Web UI, send, and assert `chat.send` carries inline
base64 attachment data.
- Drop one large PDF into Web UI, wait for staged chip, send, and assert
`chat.send` carries `file_uuid`.
- Drop a valid file and an invalid file together; valid file remains pending and
invalid file produces a toast.
- Verify mobile and desktop layouts do not overflow with long filenames or wide
image thumbnails.
### Desktop Manual Smoke
Run on Electron dev and packaged builds:
1. Start the desktop client.
2. Drag a small PNG from the OS file manager into the chat thread.
3. Confirm the composer shows a thumbnail chip.
4. Send and confirm the user bubble renders the attachment.
5. Drag a PDF larger than the inline threshold.
6. Confirm upload progress changes to staged/ready.
7. Send and confirm the gateway accepts the turn.
8. Repeat with token auth enabled.
9. Confirm no local file path appears in the chat payload, transcript, or logs.
## Acceptance Criteria
- Browser Web UI supports drag-and-drop upload for every MIME currently allowed
by `contracts/attachments.py`.
- Electron desktop supports the same drag-and-drop behavior without a separate
upload code path.
- Token-authenticated uploads succeed because the frontend sends bearer auth
headers.
- Query-token-only multipart upload remains rejected.
- Users cannot send while attachments are still reading or uploading.
- Failed uploads are visible and removable or retryable.
- Staged attachments are sent as `file_uuid` and are materialized into stable
transcript attachment refs before runtime execution.
- `file_uuid` never appears in persisted transcript envelopes.
- Successful turn acceptance evicts consumed staged uploads.
- Frontend and backend tests cover valid, invalid, oversize, auth, retry, and
layout cases.
## Rollout
- Ship behind the normal chat composer behavior with no user-facing setting.
- Keep existing file input and paste upload behavior working during rollout.
- Validate Web UI first, then Electron dev, then packaged desktop.
- Add troubleshooting guidance only if desktop platform differences require
user-visible explanation.
## Open Questions
- Should failed staged upload chips offer retry, or should failure remove the
chip and rely on toast only?
- Should duplicate files in the same drop be deduplicated by
`name + size + lastModified`, or should duplicates be allowed?
- Should the drop affordance cover only the chat thread or the full chat view?
- Should future folder drag support be handled through a separate desktop-only
import flow rather than this attachment pipeline?
@@ -0,0 +1,198 @@
# Bocha Search Provider Design
Date: 2026-06-25
## Goal
Add Bocha as a first-class OpenSquilla search provider using the existing search
runtime, provider catalog, onboarding, settings, diagnostics, and test surfaces.
The user experience should be: configure a Bocha key, then normal `web_search`
and `web_discover` flows can use it automatically when it is the best available
provider.
This design intentionally avoids a visible China-region strategy or profile. Bocha
is a normal provider with capabilities, credentials, ordering, and diagnostics.
## Non-Goals
- Do not add a visible `cn`, `domestic`, or region routing profile.
- Do not add Zhipu, a separate reader abstraction, or multi-provider result fusion.
- Do not change `search_provider` into a hard routing promise. It remains the
credential anchor for `search_api_key` and `search_api_key_env`.
- Do not add user-facing provider priority controls in the MVP.
- Do not broaden fallback to empty-result or low-quality-result retries by default.
## Existing Runtime Boundaries
OpenSquilla already has the right extension points:
- Provider specs and provider factories live in `opensquilla.search.registry`.
- Runtime availability and provider ordering live in `SearchRuntimeConfig` and
`ResolvedSearchRuntime`.
- `web_search` uses the canonical search pipeline.
- `web_discover` has a lighter provider path and must be updated separately.
- Onboarding and settings consume the provider catalog payload.
- `search_fallback_policy` currently supports `off` and `network`.
The implementation should use those surfaces instead of introducing a second
search router.
## Provider Behavior
Add `BochaSearchProvider` under `src/opensquilla/search/providers/bocha.py`.
Expected API shape:
- Endpoint: `https://api.bochaai.com/v1/web-search`
- Authentication: `Authorization: Bearer <api key>`
- Default environment variable: `BOCHA_SEARCH_API_KEY`
- Request options:
- query text
- max result count
- freshness mapping when `SearchOptions.recency` is set
- summary enabled when supported
The provider should normalize Bocha results into `SearchResult` fields:
- title from `name`
- url from `url`
- snippet from `snippet`
- provider content from `summary` when present
- published timestamp from `datePublished`
- source/site metadata from `siteName`, `displayUrl`, or equivalent fields
Bocha summaries should count as useful provider content so the canonical pipeline
does not fetch pages unnecessarily when Bocha already returned enough source
text.
## Provider Spec
Register Bocha with a `SearchProviderSpec`:
```python
SearchProviderSpec(
provider_id="bocha",
requires_api_key=True,
env_key="BOCHA_SEARCH_API_KEY",
capabilities=frozenset({"web", "freshness", "content"}),
)
```
Do not claim `domain_filter` unless the Bocha API supports an equivalent feature
and tests cover it.
## Runtime Ordering
Bocha should participate in the existing automatic ordering. Suggested MVP
ordering:
```python
_GENERAL_TIE_BREAKER = ("bocha", "tavily", "brave", "exa", "duckduckgo")
_TECHNICAL_TIE_BREAKER = ("exa", "bocha", "brave", "tavily", "duckduckgo")
_FRESHNESS_TIE_BREAKER = ("bocha", "tavily", "brave", "exa")
```
Rationale:
- General and freshness searches should prefer Bocha when it is configured,
because it is the targeted improvement for reliable China-accessible search.
- Technical searches should keep Exa first because its existing role is stronger
for semantic and content-oriented research.
- No-key and partial-key users are protected by the existing availability filter:
unavailable keyed providers are skipped before ordering is returned.
## Fallback Policy
Keep fallback semantics narrow:
- `off`: surface the original provider error.
- `network`: fallback only after network, timeout, rate-limit, retryable, or HTTP
provider errors.
Do not fallback on:
- empty results
- low-quality results
- parse errors, unless those are explicitly classified as retryable provider
failures later
This preserves cost, latency, and predictability for users who configured
multiple paid providers.
## Configuration And Setup
Supported key resolution should remain layered:
1. active provider inline configured key
2. active provider configured env var
3. provider spec default env var
Bocha should support all existing search setup paths:
- CLI configuration
- onboarding setup engine
- gateway setup payload
- desktop settings provider catalog
- web UI setup provider catalog
The MVP should update fallback/static provider lists where the UI has offline
defaults, but the canonical source of truth remains the backend provider catalog.
## Diagnostics
Search runtime status should show Bocha like other providers:
- available/unavailable
- credential source
- credential configured boolean
- skipped reason
- capabilities
Diagnostics must not expose the raw API key.
A live provider probe can be added later, but it is not required for the MVP if
existing status remains buildability/configuration based.
## Testing
Required tests:
- Provider response normalization from a representative Bocha payload.
- Missing-key behavior and credential source resolution for Bocha.
- Runtime ordering for:
- no keyed providers
- only Bocha configured
- Bocha plus existing keyed providers
- technical mode
- freshness/news mode
- `web_search` accepts `provider="bocha"` and `provider="auto"` can select Bocha.
- `web_discover` accepts and can build Bocha when configured.
- Onboarding catalog includes Bocha with `BOCHA_SEARCH_API_KEY`.
- Documentation and frontend catalog contract tests include Bocha.
Optional live tests:
- A manually gated Bocha smoke test using `BOCHA_SEARCH_API_KEY`.
- The live test must be opt-in and must not run in normal CI.
## Documentation
Update:
- `docs/search.md`
- `docs/configuration.md`
- relevant onboarding or setup docs if provider lists are repeated there
Document Bocha as a normal runtime-supported provider. Avoid describing it as a
region strategy.
## Acceptance Criteria
- A user with only `BOCHA_SEARCH_API_KEY` configured can run normal automatic web
search successfully.
- Existing users with only Brave, Tavily, Exa, or DuckDuckGo keep the same
behavior except for Bocha appearing in provider catalogs when available.
- All current fallback semantics remain unchanged.
- Bocha appears in CLI/onboarding/settings provider lists.
- Tests cover provider mapping, ordering, configuration, and documentation
contracts.
+114
View File
@@ -0,0 +1,114 @@
# Compaction and Cache Continuity
Long agent sessions need context management. OpenSquilla uses compaction,
bounded history, tool-result projection, and cache-aware prompt placement to
keep long-running tasks moving.
Compaction is separate from memory. Memory is durable recall. Compaction is an
active-session continuity tool.
## What Compaction Does
When session history approaches the configured context budget, OpenSquilla can
compact older transcript entries into a durable summary and keep the recent
tail active.
The goal is to preserve:
- user goal;
- current status;
- open steps;
- changed files and artifacts;
- known failures;
- important tool results;
- next action.
Compaction is not a guarantee that every old word remains model-visible. Export
sessions or save files when exact historical text matters.
## User-Visible Lifecycle
Depending on surface and trigger, users may see:
- compaction started;
- compaction skipped;
- compaction completed;
- compaction failed.
When no compaction is needed, OpenSquilla uses this stable message:
```text
Already within context budget; no compact was applied
```
That message is a no-op, not a failure.
## When to Compact Manually
Manual compaction is useful when:
- the session is long and you are about to start a new phase;
- a previous tool-heavy turn produced a lot of context;
- the UI indicates context pressure;
- you want the next answer to focus on the current state rather than the whole
transcript.
Avoid compact loops when the runtime says the session is already within budget.
## Passive Compaction
Passive compaction can happen when OpenSquilla detects context pressure before
or during agent work. The exact trigger depends on model context limits,
configured budgets, current history, and tool output size.
If passive compaction fails, the safest user response is usually:
1. let the current turn finish or fail cleanly;
2. export the session if exact history matters;
3. retry with a narrower request or manually save key artifacts;
4. enable diagnostics if the failure repeats.
## Prompt Cache Continuity
Prompt caching works best when stable prompt parts stay stable. OpenSquilla
tries to keep:
- stable system prompt and tool definitions early;
- current request, volatile runtime context, retrieved history, and tool results
near the tail;
- model/provider switches visible through diagnostics when they may affect
cache continuity.
Cache continuity is best-effort. Routing, tools, attachments, provider changes,
or a large new context can reduce cache reuse.
## Related Commands and Surfaces
Manual compaction is primarily surfaced in chat and Web UI flows. For
inspection and recovery:
```sh
opensquilla sessions show <session-key>
opensquilla sessions export <session-key>
opensquilla diagnostics on
```
For memory repair surfaces related to degraded compaction records:
```sh
opensquilla memory repair list
opensquilla memory repair show --summary-id <id>
opensquilla memory raw-fallbacks list
```
## Best Practices
- Keep important final artifacts in files or published artifacts.
- Use memory for durable preferences and reusable project facts.
- Use session export for exact old transcripts.
- Use manual compaction before a new phase in a very long session.
- Do not repeatedly compact a short or already-within-budget session.
---
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
@@ -0,0 +1,207 @@
# Desktop DMG Startup Window Recovery Spec
## Background
Users reported two macOS DMG installation issues:
1. After installing the latest DMG, the first launch shows no window.
2. In an earlier DMG, closing the client window with the red close button leaves the OpenSquilla app running in the Dock, but clicking the Dock icon does not reopen any window.
This spec covers the desktop startup and packaging fix. It is separate from the gateway/yoyo migration lock recovery spec, although both can surface during first-run gateway startup.
## Evidence
Local packaged logs showed the bundled gateway crashing during first launch:
```text
AttributeError: '_AsyncConnection' object has no attribute 'create_function'
```
The crash path is:
```text
opensquilla/session/storage.py -> SessionStorage.connect()
```
The packaged runtime under the built app was missing `create_function` in `opensquilla.compat.aiosqlite._AsyncConnection`, while current source has the compatibility method.
The packaged Electron app also still had this startup order:
```ts
const gateway = await startGateway()
const window = await createMainWindow()
```
That means any gateway crash or long wait can prevent the first visible desktop window from being created.
For the Dock behavior, macOS intentionally keeps the application process alive after all windows are closed. The existing behavior is acceptable only if `activate` reliably recreates or focuses the main window and reuses the already-running gateway instead of trying to perform a full duplicate startup.
## Goals
- Show a desktop window immediately on launch and Dock activation, before gateway startup can block or fail.
- Surface gateway startup failure inside the desktop UI instead of leaving the user with no visible window.
- Ensure the packaged gateway runtime contains the aiosqlite compatibility API needed by session storage.
- Reuse an already-owned, healthy gateway on Dock activation instead of spawning a duplicate process.
- Prevent duplicate gateway startup while a startup attempt is already in progress.
- Make the release validation catch stale packaged runtime contents before distributing a DMG.
## Non-Goals
- Do not change macOS close-button semantics by default. Closing the last window may keep the app running unless product decides to switch to quit-on-close.
- Do not disable yoyo migration locking.
- Do not change gateway state directory, auth token, or persistence layout.
- Do not treat a previously built DMG as fixed unless it is rebuilt from the fixed source and revalidated.
- Do not treat unsigned local validation as equivalent to a distributable release validation.
## Root Causes
### 1. Packaged Gateway Runtime Was Stale
The built app included a runtime copy of `opensquilla.compat.aiosqlite` without `_AsyncConnection.create_function`. Source had moved forward, but the packaged runtime did not contain that fix.
When session storage calls `create_function`, the packaged gateway crashes before becoming healthy.
### 2. Electron Created the Window After Gateway Startup
The app waited for `startGateway()` before calling `createMainWindow()`. If gateway startup crashes, hangs, or waits on a lock, the desktop app can remain running without any visible window.
### 3. macOS Activation Path Did Not Reliably Resume UI
On macOS, `window-all-closed` does not quit the app. After the red close button, the Dock dot remains because the app process and gateway may still be alive.
Clicking the Dock icon must focus an existing window or create a new one and attach it to the existing gateway state. If activation re-enters a full startup path, it can wait unnecessarily, hit duplicate startup guards, or fail silently.
## Design
### Electron Startup
`bootDesktopApp()` must create the main window before gateway startup:
```ts
const window = await createMainWindow()
const gateway = await startGateway()
```
The initial window should display a boot/loading state. If gateway startup succeeds, it loads the gateway URL. If gateway startup fails, it renders the existing startup error state with log details and retry actions.
### Idempotent Gateway Startup
`startGateway()` should become explicitly idempotent:
- If an owned gateway process is already healthy, return the existing `gatewayState`.
- If startup is already in progress, do not spawn another gateway. Focus or keep the boot window visible.
- If the owned process exited, clear stale owned state before starting a new process.
- If a gateway URL is present, health-check it before deciding to reuse it.
This prevents Dock activation, second-instance activation, and retry actions from racing into duplicate gateway processes.
### Dock Activation
The `activate` handler should call a single helper that:
1. Focuses an existing non-destroyed main window, or creates a new boot window.
2. If `gatewayState.status === "ready"` and the URL passes health check, loads that URL.
3. If startup is in progress, keeps the boot window visible.
4. Otherwise starts gateway once and then loads or shows an error state.
The same helper should be used by `second-instance` handling so a second launch focuses the original instance and does not start another gateway against the same state directory.
### Window Close Semantics
The macOS red close button can keep the process alive, but the implementation must clear stale window references on `closed` and make activation recreate the window.
If product later chooses quit-on-close, that should be a separate product decision. It changes expected macOS app behavior and gateway lifetime, so it should not be bundled into this bug fix.
### Gateway Runtime Compatibility
The compatibility layer must provide `create_function` on both the protocol and wrapper implementation:
```py
class Connection(Protocol):
async def create_function(...)
class _AsyncConnection:
async def create_function(...)
```
The wrapper should delegate to the underlying sqlite connection in the same thread-safe style as the other compatibility methods.
### Packaging Guard
Before building a distributable DMG:
1. Rebuild the gateway runtime from current source.
2. Fail the release if `desktop/electron/runtime/gateway` is empty or stale.
3. Build the Electron app from current source.
4. Inspect the packaged runtime and assert `create_function` exists.
5. Inspect packaged `app.asar` and assert the main window is created before gateway startup.
6. Sign, notarize, staple, and validate the final DMG.
The previously generated DMG in `dist/desktop-electron` should be treated as stale for this bug because it was built before the latest desktop/runtime fixes.
### Unsigned Functional Testing
It is acceptable to build an unsigned or ad-hoc-signed DMG first for fast local functional testing. This phase should verify application behavior only:
- first launch shows a window immediately;
- advanced configuration can start the gateway;
- closing the window with the red close button and clicking the Dock icon reopens the UI;
- no duplicate gateway process is created;
- logs do not contain the `create_function` crash or duplicate lock failures.
Code signing and notarization do not rewrite Electron or Python business logic, so they should not change the intended desktop/gateway behavior. However, they can change the effective macOS runtime environment through Gatekeeper, quarantine handling, Hardened Runtime, entitlements, nested executable signing, dynamic library loading, and child process launch policy.
Therefore, unsigned validation is only a pre-release smoke test. The final artifact sent to external users must still be rebuilt, signed, notarized, stapled, and validated end to end.
The final release must not sign only the outer `.dmg`. The `.app` bundle and nested executables must be signed before creating/signing/notarizing the DMG.
## Validation Plan
### Automated
- Run gateway compatibility tests covering `_AsyncConnection.create_function`.
- Run gateway startup/lock tests to ensure duplicate startup and stale yoyo lock handling still work.
- Add or keep an Electron-side regression check for startup order, ideally asserting `createMainWindow()` runs before `startGateway()` in the desktop boot path.
- Add an Electron-side test or factored helper test for activation reuse:
- ready gateway state is reused;
- startup-in-progress does not spawn a second gateway;
- exited owned process is cleared before restart.
### Manual DMG Smoke Test
For the first local pass, this test may use an unsigned or ad-hoc-signed DMG. For external distribution, repeat the same test with a freshly built, signed, notarized, and stapled DMG.
1. Remove or move aside existing user data for a clean first-run test.
2. Install the app from the DMG.
3. Launch from `/Applications`; a window must appear immediately.
4. Complete or enter advanced configuration; the gateway should start and the UI should load.
5. Close the window with the red close button.
6. Confirm the Dock dot remains.
7. Click the Dock icon; the window must reopen and load the existing gateway UI.
8. Confirm only one `opensquilla-gateway` process is running.
9. Confirm logs do not contain the `create_function` AttributeError.
10. Confirm logs do not show duplicate gateway state-dir lock failures during Dock activation.
### Release Validation
Release validation applies only to the final signed and notarized artifact.
Run these checks on the final artifact:
```bash
spctl --assess --type open --context context:primary-signature -v dist/desktop-electron/OpenSquilla-*.dmg
spctl --assess --type execute -v dist/desktop-electron/mac-arm64/OpenSquilla.app
xcrun stapler validate dist/desktop-electron/OpenSquilla-*.dmg
hdiutil verify dist/desktop-electron/OpenSquilla-*.dmg
```
Also mount the DMG and inspect the installation window layout.
## Acceptance Criteria
- First launch from a clean DMG install always shows a desktop window before gateway health checks finish.
- Gateway startup failures are visible in the app window with actionable error UI.
- Packaged gateway runtime contains `_AsyncConnection.create_function`.
- Dock activation after red-window-close reopens the UI and reuses the existing healthy gateway.
- Second launch/focus behavior does not spawn a duplicate gateway process.
- Final DMG is signed, notarized, stapled, and passes Gatekeeper validation.
@@ -0,0 +1,219 @@
# Gateway Startup Locking Spec
Date: 2026-06-27
Status: Draft
## Problem
Desktop first-run startup can leave the bundled gateway unable to boot when the
initial process is interrupted or a second gateway process is started while the
first process is still in schema migration.
The observed failure pattern is:
- A fresh desktop state starts successfully enough to seed the agent workspace.
- The first bundled gateway process does not reach `gateway.started`.
- Later gateway processes fail in `apply_pending()` with yoyo `LockTimeout`.
- The yoyo error reports the same earlier process id for every retry.
The direct failure is a stale or active row in the yoyo migration lock table for
the desktop `sessions.db`. Once this state exists, repeated desktop retries do
not recover by themselves.
## Goals
- Prevent the Electron shell from launching duplicate gateway processes for the
same desktop profile.
- Keep the gateway pid lock alive for the full server lifetime and release it
on graceful shutdown.
- Recover from a stale yoyo migration lock only when the recorded pid is proven
dead.
- Preserve yoyo's safety guarantee when another process may still be migrating.
- Surface actionable startup errors instead of an opaque PyInstaller traceback.
## Non-Goals
- Do not disable yoyo migration locking.
- Do not blindly run `break-lock` on every migration lock timeout.
- Do not change the session database schema as part of this fix.
- Do not make the desktop gateway share a process with Electron.
- Do not broaden gateway binding or auth behavior.
## Existing Surfaces
- Electron launches the bundled gateway in `desktop/electron/src/main.ts` via
`spawn(...)`.
- Electron waits up to 45 seconds for `/healthz` before reporting that the
gateway did not become healthy.
- The gateway acquires `GatewayPidLock` before `build_services()` and before
session database migration.
- `build_services()` runs `apply_pending(session_db_path, migrations_dir)` before
opening `SessionStorage`.
- `apply_pending()` delegates locking to `with backend.lock():`.
- yoyo implements the migration lock as a row in `yoyo_lock`, keyed by pid, and
removes it in a `finally` block. A hard process exit can leave the row behind.
## Design
Implement three defensive layers. Each layer addresses a different failure mode
and should be independently testable.
### Layer 1: Electron Single Instance Guard
Add `app.requestSingleInstanceLock()` near Electron app startup, before any
gateway startup work can run.
Expected behavior:
- If the lock cannot be acquired, the new Electron process exits immediately.
- On `second-instance`, the existing main window is restored and focused.
- `bootDesktopApp()` must not run in the second Electron process.
- The existing process remains the only owner allowed to spawn a gateway.
This reduces duplicate gateway starts caused by double-clicking the app,
reopening from Finder, or launching from a DMG while a first run is still in
progress.
### Layer 2: Gateway PID Lock Lifetime
Store the acquired `GatewayPidLock` on the returned `GatewayServer` object and
release it in `GatewayServer.close()`.
Expected behavior:
- `start_gateway_server()` acquires the lock before database migration, as it
does today.
- The lock object remains strongly referenced for the whole server lifetime.
- `GatewayServer.close()` calls `release()` exactly once, after shutdown work is
complete enough that a new gateway may safely start.
- `release()` remains idempotent.
- Existing `atexit` and signal cleanup remain as best-effort fallback paths.
This makes normal gateway shutdown explicit instead of relying only on process
exit behavior.
### Layer 3: Conservative Yoyo Stale Lock Recovery
Wrap yoyo `LockTimeout` handling inside `apply_pending()`.
When yoyo reports a lock timeout:
1. Inspect the lock table for recorded pids.
2. If any recorded pid is alive, fail without clearing the lock.
3. If every recorded pid is dead or invalid, delete the yoyo lock rows.
4. Retry migration once.
5. If the retry fails, surface the second failure without another recovery loop.
The liveness check should use platform-appropriate process probing:
- POSIX: `os.kill(pid, 0)`.
- Windows: `OpenProcess` or equivalent existing helper behavior.
The lock recovery path must log structured events:
- `migrator.lock_timeout`
- `migrator.lock_held_by_live_process`
- `migrator.stale_lock_cleared`
- `migrator.stale_lock_retry_failed`
The operator-facing error should explain whether the gateway is still starting,
another gateway is running, or a stale migration lock could not be recovered.
## Safety Rules
- Never clear a yoyo lock held by a live pid.
- Never clear the lock if the database cannot be inspected safely.
- Never retry migration more than once after clearing a stale lock.
- Keep migration failure loud if schema state is uncertain.
- Prefer false negatives over false positives: failing startup is safer than
corrupting a migration.
## Implementation Notes
Electron:
- Add single-instance handling in `desktop/electron/src/main.ts`.
- Keep the current `startupInProgress` guard for the existing process.
- On a second instance, restore and focus `mainWindow` if it exists.
Gateway:
- Extend `GatewayServer` with an optional pid lock field.
- Assign the acquired lock after `GatewayPidLock.acquire()` succeeds.
- Release the lock in `GatewayServer.close()` in a `finally`-safe path.
Migrator:
- Import yoyo `LockTimeout` explicitly.
- Add a small helper to query `yoyo_lock` rows through the yoyo backend or a
separate SQLite connection to the same local database.
- Add a small helper to clear the yoyo lock table.
- Keep `:memory:` behavior unchanged.
- Keep normal no-pending migration behavior unchanged.
## Test Plan
### Unit Tests
- `GatewayPidLock` rejects a second lock acquisition for the same state dir while
the first lock is held.
- `GatewayServer.close()` releases the stored gateway pid lock.
- `apply_pending()` does not clear a yoyo lock for a live pid.
- `apply_pending()` clears a yoyo lock for a dead pid and retries once.
- `apply_pending()` does not enter an unbounded retry loop after stale-lock
recovery fails.
- `apply_pending()` preserves normal migration success and no-op behavior.
### Electron Tests
- A second Electron instance does not call `bootDesktopApp()`.
- A second Electron instance focuses the existing window.
- Desktop retry does not spawn a new gateway while startup is already in
progress.
### Integration Tests
- Create a temporary desktop state with `sessions.db` containing a stale
`yoyo_lock` row. Gateway startup clears it and completes migration.
- Create a temporary desktop state with a live helper process recorded in
`yoyo_lock`. Gateway startup fails with a clear, non-destructive error.
- Start two gateway processes against the same state dir. The second process
fails before database migration.
### Manual DMG Smoke
- Install the DMG into `/Applications`.
- Start from a clean desktop user data directory.
- Double-click the app repeatedly during first-run startup.
- Confirm only one gateway process is launched.
- Confirm the app becomes ready or surfaces a clear single startup error.
- Force quit during first-run migration, relaunch, and confirm stale-lock
recovery works when the recorded pid is no longer alive.
## Acceptance Criteria
- Fresh DMG first run cannot spawn two owned gateway processes from two Electron
instances.
- A stale yoyo migration lock whose pid is dead is cleared automatically and the
gateway starts.
- A yoyo migration lock whose pid is alive is never cleared automatically.
- A second gateway using the same desktop state fails before migration work.
- Graceful gateway shutdown removes `gateway.pid` and releases
`gateway.pid.lock`.
- Failure messages distinguish active startup, already-running gateway, and
unrecoverable migration lock states.
- CI covers stale-lock recovery and live-lock non-recovery.
## Rollout
- Ship behind normal startup behavior, with no user-facing setting.
- Keep structured logs in the desktop gateway log for post-incident diagnosis.
- Add a short troubleshooting entry once the behavior is implemented.
## Open Questions
- Whether yoyo lock inspection should use yoyo backend APIs only or direct
SQLite for local file databases.
- Whether desktop startup should extend the first-run health timeout after it
detects schema migration is active.
- Whether the boot splash should show a distinct "Preparing database" phase.
+129
View File
@@ -0,0 +1,129 @@
# Memory
OpenSquilla memory helps the agent recall durable context without replaying
every old conversation. Use it for stable preferences, reusable project facts,
previous decisions, and notes that should survive across sessions.
Memory is separate from skills. Skills teach the agent how to do a task; memory
stores useful facts and context the agent may need later.
## What to Store
Good memory entries are stable and reusable:
- user preferences;
- project conventions;
- recurring output formats;
- names of important repositories, directories, or services;
- decisions the user wants reused;
- brief notes from completed tasks.
Avoid memory for:
- API keys or secrets;
- raw private data that does not need long-term recall;
- one-off instructions for the current turn;
- noisy dumps that would pollute future retrieval;
- exact transcripts that should instead be exported as session records.
## Common Commands
Inspect memory health:
```sh
opensquilla memory status
opensquilla memory status --deep
```
Index and list memory sources:
```sh
opensquilla memory index
opensquilla memory list
```
Search and inspect memory:
```sh
opensquilla memory search "release note format"
opensquilla memory show <path>
```
Search previous sessions as well as memory:
```sh
opensquilla memory search "deployment decision" --source all
```
## Natural Chat Usage
Ask naturally when something should be remembered:
```text
Remember that I prefer concise release notes with a risk section.
```
Later, refer to the preference:
```text
Use my usual release-note format for this changelog.
```
When memory seems stale, ask the agent to search explicitly:
```text
Search memory for my release-note preferences before drafting this.
```
## Session-Derived Memory
For long or important sessions, flush session state into memory before
archiving, compacting, or switching tasks:
```sh
opensquilla memory flush-session <session-key>
```
Use session export when exact old wording matters:
```sh
opensquilla sessions export <session-key>
```
Memory is for useful recall. Session export is for exact records.
## Maintenance and Repair
Refresh the index after editing memory files or changing memory configuration:
```sh
opensquilla memory index --force
```
Inspect fallback and repair surfaces:
```sh
opensquilla memory raw-fallbacks list
opensquilla memory repair list
```
Show or repair a degraded compaction memory record when instructed by
diagnostics:
```sh
opensquilla memory repair show --summary-id <id>
opensquilla memory repair run --summary-id <id>
```
## Best Practices
- Keep entries short and sourceable.
- Prefer "Remember X for project Y" over vague "remember this."
- Search memory before assuming the agent forgot.
- Remove or revise stale preferences instead of adding contradictory ones.
- Keep secrets out of memory.
- Use artifacts or files for large reference material.
---
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
+439
View File
@@ -0,0 +1,439 @@
# OpenSquilla MetaSkill User Guide
MetaSkill lets OpenSquilla move from figuring out complex work from scratch on
every turn to reusable, explicitly launchable, auditable, and improvable task
protocols.
A normal conversation solves one request. A MetaSkill preserves a way of doing
high-value work.
## Important Notice
Some MetaSkills in OpenSquilla, and some of the skills they call, are authored,
revised, or composed with AI assistance based on intended functionality,
available capabilities, and usage scenarios.
This means:
- MetaSkills are not merely a collection of fully hand-written scripts. They are
part of a system where AI can help formalize and evolve reusable task
protocols.
- AI-authored or AI-assisted MetaSkills should be reviewed through structural
validation, trigger-surface checks, runtime testing, human review, and
safety-boundary assessment before they are treated as ready for use.
- MetaSkill outputs are decision-support materials and work-product drafts. They
are not final professional advice in legal, medical, financial, hiring,
academic, security, or other high-stakes contexts.
- Actions such as publishing, applying, installing, paying, signing, messaging,
or modifying production systems require explicit user authorization and remain
the user's responsibility.
- When a MetaSkill relies on search, document parsing, LLM judgment, or
third-party tools, the result may be affected by source quality, model
limitations, tool availability, context completeness, and time-sensitive
changes.
- Users should review facts, citations, assumptions, risks, and unverifiable
claims, especially in high-stakes situations.
In short: MetaSkill turns high-value work into reusable, auditable, and
improvable AI collaboration protocols. It does not remove the need for review,
judgment, or accountability.
## What It Is
OpenSquilla is an open-source AI agent runtime. MetaSkill is its task-protocol
layer.
A MetaSkill does not introduce new execution atoms. It defines a way to organize
existing atoms, such as skills, tools, LLM calls, and sub-agents, into a
reusable task protocol.
The analogy is a Makefile and shell commands. A Makefile does not replace
commands; it defines how commands are composed. A MetaSkill does not replace
skills or tools; it tells OpenSquilla how a class of high-value work should be
understood, structured, checked, and delivered.
MetaSkill provides four main advantages:
- protocolized capability captured in a `SKILL.md` file with `kind: meta` and
`composition.steps`;
- explicit launch through `/meta`, with optional automatic triggering only when
`meta_skill.auto_trigger = true`;
- auditable and replayable step inputs, outputs, status, and results;
- improvable over time as repeated collaboration patterns become proposals.
## Default Launch Model
MetaSkills are manual-only by default. On supported chat surfaces, use `/meta`
to list available workflows and `/meta <name>` to run one. This keeps workflow
launches deliberate, reviewable, and easier to explain.
Web chat and the CLI gateway TUI support both list and run:
```text
/meta
/meta meta-kid-project-planner
```
Channel surfaces support `/meta` listing only. Standalone CLI chat requires
gateway mode for `/meta`.
To restore the older automatic behavior, set:
```toml
[meta_skill]
auto_trigger = true
```
With `auto_trigger = true`, OpenSquilla may consider MetaSkills during ordinary
natural-language turns. Leave it off when you want workflows to run only after
an explicit `/meta <name>` command.
## User Mental Model
Using a MetaSkill is not just asking a question. It is delegating OpenSquilla to
produce a reviewable result.
A strong MetaSkill request contains four things:
1. Outcome: what you want to receive.
2. Context: materials, entities, time range, and constraints that matter.
3. Standard: what "good" means for this task.
4. Boundaries: what must not happen, what must not be invented, and what requires
confirmation.
Example:
```text
/meta meta-kid-project-planner
I need a safe weekend project plan, not a generic list of ideas.
Use only materials that are easy to buy locally.
Separate adult-only steps from child-safe steps.
Do not include flames, blades, solvents, or risky chemicals.
```
The user defines the target and standard; OpenSquilla organizes the execution.
## Current Built-In MetaSkills
The retained built-in MetaSkills cover a focused set of high-value task classes.
| MetaSkill | Positioning |
| --- | --- |
| `meta-kid-project-planner` | Produces safe, age-appropriate plans for school projects, show-and-tell, or science activities. |
| `meta-paper-write` | Supports academic drafts, manuscript structure, citation planning, experiment placeholders, and LaTeX/PDF paths. |
| `meta-short-drama` | Produces short-drama scripts, visual prompts, video assembly plans, subtitles, and rendered local video artifacts. |
| `meta-skill-creator` | Turns repeated multi-skill collaboration patterns into new MetaSkill proposals. |
These are designed around quality over quantity. Immature, duplicate, or
single-skill wrapper MetaSkills should not remain in the bundled catalog.
## Requirements Before Running MetaSkills
The Skill page is the source of truth for current readiness. Open the skill
detail dialog and check the **Requirements** section before running workflows
that export files, compile PDFs, or render video.
Common setup surfaces:
- Paper/PDF workflows such as `meta-paper-write` require `xelatex` and
`bibtex` on `PATH`. Install a TeX distribution such as TeX Live, MiKTeX, or
BasicTeX before requesting compiled PDFs.
- Video workflows such as `meta-short-drama` require `ffmpeg` and `ffprobe` on
`PATH` for clip animation, merging, and subtitle burn-in.
- Office-document workflows roll up requirements from child skills such as
`docx`, `xlsx`, `pdf-toolkit`, and `pptx`; these usually surface Python
package requirements in the Skill page.
- Search, weather, image, and video-provider steps may require configured API
keys or provider credentials. The workflow should treat missing credentials as
setup blockers rather than silently degrading output.
## Two Ways to Use MetaSkill
### Default: Explicit Command
Start the workflow with `/meta <name>` and then describe the outcome:
```text
/meta meta-kid-project-planner
Plan a safe 20-minute balcony plant science project for a 7-year-old. Include
materials, steps, safety notes, and a simple presentation outline.
```
This is the normal 0.4 release-line path. It is best for important, expensive,
or easily confused tasks because the workflow launch is explicit.
### Compatibility: Automatic Triggering
If `meta_skill.auto_trigger = true` is set, OpenSquilla can consider MetaSkills
from natural-language intent:
```text
Use meta-skill `meta-kid-project-planner`.
Plan a safe 20-minute balcony plant science project for a 7-year-old. Include
materials, steps, safety notes, and a simple presentation outline.
```
This mode is for users who intentionally want the older auto-trigger behavior.
It is not the default.
## Low-Cost, High-Quality Request Template
Recommended template:
```text
/meta <name>
Outcome:
Context:
Decision standard:
Expected output:
Constraints:
Do not:
```
Example:
```text
/meta meta-kid-project-planner
Outcome: plan a child-safe weekend science project.
Context: 7-year-old, balcony plants, 20 minutes of activity, ordinary household
materials only.
Decision standard: safe, age-appropriate, low mess, and easy to present at
school.
Expected output: materials list, adult setup, child steps, safety notes, and a
presentation outline.
Constraints: avoid flames, blades, solvents, and risky chemicals.
Do not: ask the child to do adult-only setup alone.
```
Useful constraints:
- Do not invent missing facts.
- Separate facts, assumptions, and recommendations.
- Use only pasted material unless sources are available.
- Do not submit, publish, install, pay, send, or sign automatically.
- Ask me if a decision depends on missing information.
## Built-In MetaSkill Usage Patterns
### `meta-kid-project-planner`
Use for child school projects, show-and-tell, science demos, and safe creative
activities.
Good fit:
- science fair;
- show-and-tell;
- classroom demonstration;
- child-safe craft or experiment;
- low-burden parent preparation.
High-quality request:
```text
/meta meta-kid-project-planner
Help my child prepare a second-grade science fair project about plant growth. We
have beans, paper cups, cotton, water, and a sunny windowsill.
Keep it safe and simple.
Give me:
- materials list
- 3-day plan
- what the child should observe
- short presentation script
- what remains unknown
```
Expected result: safe, age-appropriate, source-strict output. It should not
invent weather, school requirements, or child preferences.
### `meta-paper-write`
Use for academic papers, research manuscripts, and LaTeX-oriented deliverables.
Good fit:
- compact paper skeleton;
- section structure;
- citation plan;
- experiment and figure/table placeholders;
- LaTeX/PDF path when explicitly requested.
PDF compilation requires `xelatex` and `bibtex` on `PATH`. If those binaries are
missing, use the LaTeX source output or install TeX Live, MiKTeX, or BasicTeX
before asking for a compiled PDF.
High-quality request:
```text
/meta meta-paper-write
Draft a compact research paper skeleton on retrieval-augmented generation for
customer-support knowledge bases.
Include:
- title
- abstract
- related work plan
- method outline
- experiment placeholders
- figure/table placeholders
- citation plan
Keep it compact first. Do not write a full manuscript unless I ask.
```
Expected result: a paper-shaped deliverable, not a generic essay. Citations
should not be presented as verified sources unless actually verified.
### `meta-skill-creator`
Use to create a new MetaSkill proposal.
Good fit:
- turning repeated multi-skill collaboration into a reusable capability;
- defining trigger surfaces;
- composing existing skills;
- adding validation and risk checks;
- producing a proposal for review.
Poor fit:
- creating a normal single-purpose skill;
- analyzing existing skill lists without creating anything;
- asking what MetaSkill is;
- pasting old pages for diagnosis.
High-quality request:
```text
/meta meta-skill-creator
Create a new meta-skill for product launch briefs. It should search current
sources, collect product context, draft a launch memo, generate a DOCX handoff,
check evidence gaps, and avoid publishing anything automatically.
Please propose:
- name
- description
- triggers
- steps
- validation gates
- collision checks
```
Expected result: a proposal, not an immediate unreviewed production rollout.
## Avoiding Accidental Activation
If you paste old chat history, Web UI dumps, prompt examples, skill lists, or
test material, mark it as quoted context:
```text
The following is quoted context, not my current request.
Do not run any skill.
Do not create or persist any proposal.
Only analyze this text.
```
This matters because historical material may contain trigger words. Without a
clear boundary, the system may confuse quoted content with current intent.
If you only want to analyze a MetaSkill and do not want proposal creation:
```text
Only analyze. Do not create, assemble, preview, or persist any meta-skill
proposal.
```
## Run Progress Ribbon
While a MetaSkill runs, the WebUI shows a horizontal ribbon at the top
of the agent reply listing every step in the workflow. The currently
running chip is highlighted; succeeded steps show ✓, skipped ↷, failed
✗, and `on_failure` substitutes show ⇄. Click any chip to scroll to
that step's tool card. If a step fails, the ribbon also surfaces
"Retry run", "Switch meta-skill", and "Show error detail" actions
inline.
The ribbon survives disconnects: when the browser reconnects, the gateway
replays the announce → state → completed events so the ribbon rebuilds
to the latest state.
## Reading the Result
A strong MetaSkill result should explain:
- what it produced;
- what facts or sources it used;
- what is inferred or assumed;
- what risks remain;
- what the next action is;
- what could not be verified;
- whether any artifact or proposal was actually created.
Be cautious if the output:
- claims current facts without sources;
- claims a file was created but no artifact exists;
- hides tool failures as success;
- gives generic advice instead of the requested deliverable;
- ignores "do not create", "do not send", "do not publish", or "do not install".
## Correcting a Bad Run
If the wrong MetaSkill triggered:
```text
Stop using the previous MetaSkill. Treat my earlier text as context only. Now
use meta-skill `<correct_name>` for this goal: ...
```
If no MetaSkill triggered:
```text
Please rerun and explicitly use meta-skill `<name>`.
```
If the output is too generic:
```text
Redo this as a decision-ready deliverable with evidence, assumptions, risks, and
next actions.
```
If creator starts creating but you do not want creation:
```text
Do not create, assemble, preview, or persist any meta-skill proposal. Only
analyze.
```
## Building Your Own MetaSkill
A task is a good MetaSkill candidate when:
- you repeatedly perform the same high-value task;
- each run has multiple steps;
- inputs are similar but details vary;
- the output format is relatively stable;
- review, audit, replay, or confirmation matters;
- ordinary prompts require you to restate too many rules every time.
Poor candidates include one-line fact queries, single tool calls, casual
conversation, brainstorming without stable output criteria, and high-risk
automated action without human confirmation.
For the authoring protocol, read [`../authoring/meta-skills.md`](../authoring/meta-skills.md).
---
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
+177
View File
@@ -0,0 +1,177 @@
# Meta-Skills
Meta-skills package repeatable multi-step work as reusable, inspectable
workflows. Use them when a request needs more than one normal skill, tool,
checkpoint, or final synthesis pass.
For the full user-facing guide, read
[`meta-skill-user-guide.md`](meta-skill-user-guide.md). For authoring rules,
read [`../authoring/meta-skills.md`](../authoring/meta-skills.md).
## Skills vs Meta-Skills
| Capability | Use it for |
| --- | --- |
| Skill | One focused task pattern, instruction set, script, or tool helper. |
| Meta-skill | A reusable workflow made of multiple steps, skills, checks, or outputs. |
For example, "summarize this document" is skill-shaped. "Plan a safe child
science project with materials, adult setup, child steps, presentation notes, and
final safety review" is meta-skill-shaped.
## Stable Built-In MetaSkills
The retained stable catalog is intentionally small:
| MetaSkill | Positioning |
| --- | --- |
| `meta-kid-project-planner` | Produces safe, age-appropriate plans for school projects, show-and-tell, or science activities. |
| `meta-paper-write` | Supports academic drafts, manuscript structure, citation planning, experiment placeholders, and LaTeX/PDF paths. |
| `meta-short-drama` | Produces short-drama scripts, visual prompts, subtitles, and local video artifacts. |
| `meta-skill-creator` | Turns repeated multi-skill collaboration patterns into new MetaSkill proposals. |
Experimental meta-skills may exist under development trees, but this page lists
only bundled built-ins that should be presented as retained product
capabilities.
## Requirements
Use the Skill page detail dialog before running a MetaSkill. Its
**Requirements** section shows the MetaSkill's own requirements plus one-hop
requirements from child skills.
- `meta-paper-write` needs `xelatex` and `bibtex` for PDF compilation.
- `meta-short-drama` needs `ffmpeg` and `ffprobe` for local video rendering,
merge, and subtitle steps.
- MetaSkills inherit readiness from their child skills; for example,
`meta-paper-write` surfaces LaTeX/PDF requirements and
`meta-short-drama` surfaces local video-tool requirements.
## Run MetaSkills
MetaSkills are manual-only by default. They do not auto-trigger from message
keywords or appear in the runtime prompt unless you explicitly opt into the old
automatic behavior.
In Web chat and the CLI gateway TUI:
```text
/meta
/meta meta-kid-project-planner
```
`/meta` lists available MetaSkills. `/meta <name>` starts the selected
workflow. Channel surfaces can list MetaSkills with `/meta`, but they do not run
MetaSkills from chat text. Standalone CLI chat requires gateway mode for
`/meta`.
To restore automatic model-triggered behavior, set:
```toml
[meta_skill]
auto_trigger = true
```
Use this compatibility mode only when you want MetaSkills to be considered by
the model during ordinary chat turns.
## How to Prepare the Request
Ask for the outcome and the standard:
```text
Plan a safe 20-minute balcony plant science project for a 7-year-old. Include
materials, adult setup, child steps, safety notes, and a presentation outline.
```
When you start a workflow, include the task after the command:
```text
/meta meta-kid-project-planner
Plan a safe 20-minute balcony plant science project for a 7-year-old. Include
materials, adult setup, child steps, safety notes, and a presentation outline.
```
A strong request usually includes:
- outcome;
- context;
- decision standard;
- expected output;
- constraints;
- actions the agent must not take.
## Discover Meta-Skills
Use chat slash commands for the runtime list:
```text
/meta
```
Use the CLI for inventory and inspection:
```sh
opensquilla skills list
opensquilla skills search meta
```
Inspect a meta-skill composition:
```sh
opensquilla skills inspect <meta-skill-name>
```
The inspect command shows the compiled step shape before you rely on a workflow.
## Inspect Run History
List recent runs:
```sh
opensquilla skills meta runs list
```
Inspect one run:
```sh
opensquilla skills meta runs show <run-id>
opensquilla skills meta runs steps <run-id>
opensquilla skills meta runs failures --since 24h
```
Preview replay shape without executing live work:
```sh
opensquilla skills meta runs replay <run-id> --dry-run
```
## Proposals
Meta-skill creation workflows may write proposals before they become managed
skills. Inspect proposals:
```sh
opensquilla skills meta proposals list
opensquilla skills meta proposals show <proposal-id>
```
Accept a proposal only after review:
```sh
opensquilla skills meta proposals accept <proposal-id>
```
## Safety Model
MetaSkill outputs are reviewable work products and decision-support drafts. They
are not final professional advice in legal, medical, financial, hiring,
academic, security, or other high-stakes contexts.
Actions such as publishing, applying, installing, paying, signing, messaging, or
modifying production systems require explicit user authorization.
---
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
+159
View File
@@ -0,0 +1,159 @@
# Skills
Skills are task-specific instruction packages and scripts. They let OpenSquilla
load relevant guidance only when a task needs it, instead of putting every
possible instruction into every prompt.
Skills are separate from memory. Memory stores facts; skills describe repeatable
ways to work.
## What Skills Are For
Use skills for repeatable work patterns such as:
- deep research;
- summarization;
- GitHub and PR workflows;
- document generation;
- spreadsheet, slide, PDF, and DOCX work;
- web search;
- weather lookup;
- terminal or tmux monitoring;
- subagent delegation;
- skill creation and review.
If the workflow combines multiple skills or a reusable multi-step plan, use a
meta-skill instead.
## Discover Installed Skills
List skills available in the current install:
```sh
opensquilla skills list
```
View one skill:
```sh
opensquilla skills view <skill-name>
```
Search community sources:
```sh
opensquilla skills search pdf
```
Some skills may be ineligible when optional dependencies are missing or when the
skill is intentionally demo-only. `skills list` is the source of truth for your
current install.
## Install, Update, and Remove Skills
Install a managed skill:
```sh
opensquilla skills install <skill-name>
```
Update one skill or all managed skills:
```sh
opensquilla skills update <skill-name>
opensquilla skills update --all
```
Remove a managed skill:
```sh
opensquilla skills uninstall <skill-name>
```
## Manage Skill Sources
Custom source repositories are called taps:
```sh
opensquilla skills tap list
opensquilla skills tap add <owner/repo>
opensquilla skills tap remove <owner/repo>
```
Use taps when your team maintains its own skill catalog.
## Publish and Inspect
Publish a skill directory:
```sh
opensquilla skills publish <path-to-skill>
```
Inspect the compiled composition for a meta-skill:
```sh
opensquilla skills inspect <meta-skill-name>
```
For ordinary skill content, use:
```sh
opensquilla skills view <skill-name>
```
## How to Ask for a Skill
Ask for the outcome:
```text
Create a PowerPoint deck summarizing this report.
```
Better than:
```text
Load the pptx skill and run its script.
```
OpenSquilla can choose eligible skills from the current catalog when the task
matches their description and triggers.
## Bundled Skill Families
| Family | Examples |
| --- | --- |
| Research | deep research, multi-source search, summarization |
| Documents | DOCX, PPTX, XLSX, PDF, HTML-to-PDF |
| Operations | cron, GitHub, terminal monitoring, subagents |
| Memory | memory-oriented helpers and history exploration |
| Creation | skill creator, skill review, proposal helpers |
## Troubleshooting
If a skill is not selected:
1. Confirm it appears in the installed catalog:
```sh
opensquilla skills list
```
2. Inspect its description and eligibility:
```sh
opensquilla skills view <skill-name>
```
3. Ask for the outcome in normal language. Skill names can help, but user
intent should still be clear.
4. If optional dependencies are missing, install or update the skill and retry.
For composed workflows, read [`meta-skills.md`](meta-skills.md). For the full
MetaSkill user guide, read [`meta-skill-user-guide.md`](meta-skill-user-guide.md).
For authoring rules, read [`../authoring/meta-skills.md`](../authoring/meta-skills.md).
---
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
+172
View File
@@ -0,0 +1,172 @@
# SquillaRouter
SquillaRouter is OpenSquilla's local model-routing layer. It helps the agent
choose an appropriate model tier for each turn so routine work does not always
run on the most expensive model.
Use this page when you want to enable routing, understand what it changes, or
decide whether a fixed provider/model is better for a specific run.
## Why Use It
SquillaRouter is useful when you want:
- lower cost for simple chat, edits, summaries, and routine tool work;
- stronger models reserved for hard reasoning, recovery, and long tasks;
- one OpenSquilla workflow that can route across provider profiles;
- local routing decisions without sending prompts to a separate external
classifier just to choose the model.
It is not required. OpenSquilla can also run in direct single-model mode.
## Enable Routing
Recommended first-run setup:
```sh
opensquilla onboard --router recommended
```
Reconfigure an existing install:
```sh
opensquilla configure router --router recommended
```
Use the OpenRouter mixed defaults:
```sh
opensquilla configure router --router openrouter-mix
```
Disable routing and use the configured provider/model directly:
```sh
opensquilla configure router --router disabled
```
## Inspect Provider Support
Check the provider catalog available in your install:
```sh
opensquilla providers list
```
If the gateway is running, inspect runtime provider health:
```sh
opensquilla providers status
```
Router-supported profiles depend on the installed OpenSquilla version,
optional dependencies, and configured provider credentials. Common profiles
include OpenRouter, OpenAI, DeepSeek, Gemini, DashScope, Moonshot, Volcengine,
Zhipu, and compatible provider tiers exposed by the local catalog.
## What the Router Can Affect
Depending on configuration, SquillaRouter may influence:
- selected model tier;
- direct model fallback;
- reasoning level;
- response policy;
- image-capable model selection;
- cache-continuity safeguards for recent higher-tier turns.
The exact decision is available through runtime metadata and diagnostics
surfaces. Turn on diagnostics when you need to understand why a turn was routed
to a particular model:
```sh
opensquilla diagnostics on
```
## Terminal Router HUD
Interactive terminal chat can surface routing decisions through a TUI Router HUD
when router metadata is present and the selected backend supports the structured
UI/plugin surface. In the current implementation, the OpenTUI preview footer is
the primary terminal display for this HUD. The HUD is display-only: it consumes
the same turn metadata and does not change model selection.
The HUD can show the selected tier, selected model, baseline model, route
source, confidence, estimated savings, fallback state, thinking mode, prompt
policy, whether routing was applied, and rollout phase.
Full routing is shown as an active route. Observe-only routing is shown as an
observe decision, which means OpenSquilla recorded what the router would have
chosen while keeping the configured baseline behavior. Fallback decisions use a
warning style so provider or policy recovery is visible during the turn.
## Recommended Operating Modes
| Goal | Suggested mode |
| --- | --- |
| General personal-agent use | `recommended` |
| Multi-provider cost optimization through OpenRouter | `openrouter-mix` |
| Provider evaluation, billing audit, or reproducible benchmark run | `disabled` |
| Debugging one provider-specific behavior | `disabled` |
For routine use, start with `recommended`. Disable routing only when the model
choice itself is the thing you are testing.
## Example Requests
Good router-friendly requests describe the outcome, not the tier:
```text
Summarize this long issue thread and list the decision points.
```
```text
Review my current diff and point out the highest-risk changes.
```
Avoid asking the router to behave like a manual model picker unless you are
debugging:
```text
Use exactly this one model for every turn.
```
For exact-model work, configure direct routing instead.
## Troubleshooting
If routing does not appear to work:
1. Confirm the router is enabled:
```sh
opensquilla config get router.enabled
opensquilla config get llm.provider
```
2. Check provider readiness:
```sh
opensquilla providers status
opensquilla doctor
```
3. If SquillaRouter optional dependencies are missing, OpenSquilla can still run
with direct single-model routing. On Windows, ONNX Runtime may require the
Visual C++ Redistributable. On macOS terminal installs, LightGBM may require
`libomp` from Homebrew:
```sh
brew install libomp
opensquilla gateway restart
```
4. If you need deterministic model behavior for a run, disable routing:
```sh
opensquilla configure router --router disabled
```
---
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
+118
View File
@@ -0,0 +1,118 @@
# Tool Compression
OpenSquilla agents use tools. Tool calls can produce large outputs: command
logs, JSON, web pages, search results, diffs, file contents, tables, and
artifacts. Tool compression keeps those outputs useful without letting them
consume the whole model context.
This is a user-facing context-management feature. It does not change what the
tool returned; it changes how much of that result is shown to the model for the
next step.
## Why It Matters
Tool compression helps when:
- a command prints a large log;
- a web page or search result is too long for a useful prompt;
- a file read returns more text than the next step needs;
- a long session is close to the context budget;
- you want raw results preserved while the model sees a compact preview.
Without compression, one large tool result can crowd out the user's goal,
recent conversation, and next action.
## What Users May See
In long or tool-heavy turns, the model-visible result may include:
- a compact preview;
- a note that a result was shortened;
- a `tool_result_handle` for an out-of-band stored result;
- estimated token-saving diagnostics when diagnostics are enabled.
This is expected. It means OpenSquilla is protecting the active context window.
## Product-Level Model
OpenSquilla separates two views:
| View | Purpose |
| --- | --- |
| Runtime view | The durable result OpenSquilla can preserve, inspect, or export. |
| Provider view | The bounded text sent back to the model for the next reasoning step. |
The agent can continue from the important facts while large raw material stays
available through files, session export, diagnostics, or tool-result handles
when configured.
## Compression Modes
OpenSquilla supports several compression styles depending on configuration and
tool output shape.
| Mode | Best for | Tradeoff |
| --- | --- | --- |
| `truncate` | Fast deterministic previews. | May omit useful middle sections. |
| `summarize` | Slower/background workflows that benefit from semantic summaries. | Adds another model call and should be opt-in. |
| Structured projection | Logs, diffs, JSON, tables, and known tool shapes. | Depends on reducer coverage for that output type. |
Most users should keep the default behavior and use diagnostics only when a
workflow is still too large.
## How to Work With Large Outputs
Ask for focused follow-up reads:
```text
Look at the failing test names and the last 80 lines of the log.
```
Prefer handles, paths, and summaries:
```text
Use the compacted result to identify likely causes, then read the exact file
sections you need.
```
Avoid asking the agent to paste every line of a huge result unless exact text is
the deliverable:
```text
Paste the entire 50,000-line log into chat.
```
## Inspect and Debug
Turn on diagnostics when you need to understand context growth:
```sh
opensquilla diagnostics on
```
Export the session when you need to inspect durable history outside the chat
surface:
```sh
opensquilla sessions export <session-key>
```
Review cost and usage after a large tool-heavy run:
```sh
opensquilla cost
```
## Best Practices
- Keep tool requests specific.
- Ask for the smallest file ranges, log tail, or JSON fields that answer the
question.
- Use artifacts for large deliverables instead of forcing everything into chat.
- Use session export for audit and debugging.
- Treat tool compression as a continuity feature, not as a substitute for
storing important files.
---
[Docs index](../README.md) · [Product guide](../../README.product.md) · [Improve this page](../contributing-docs.md) · [Report a docs issue](https://github.com/opensquilla/opensquilla/issues/new?template=docs_report.yml)
+105
View File
@@ -0,0 +1,105 @@
# TUI Frontend
OpenSquilla terminal chat has one stable default backend and one opt-in preview
backend:
| Backend or target | Status | How to use | Requirements |
| --- | --- | --- | --- |
| `native` | Stable default | `opensquilla chat` | Python package only |
| `opentui` | Preview opt-in | `OPENSQUILLA_TUI_BACKEND=opentui uv run opensquilla chat` | Source checkout, Bun, and local OpenTUI package dependencies |
| `live-opentui` | Manual harness target | Real-terminal harness only | tmux, OpenTUI deps, and live provider config |
`live-opentui` is not an `OPENSQUILLA_TUI_BACKEND` value. It is a guarded test
target that launches the OpenTUI preview path through the real CLI.
The TUI contracts are renderer-independent and built around two separate planes:
- **Streaming plane:** batches token deltas before writing to the terminal, so
long answers do not redraw the whole interface for every token.
- **Structured UI plane:** sends normalized TUI domain events to plugins. Plugin
snapshots can be rendered by capable TUI backends and by future renderers.
The stable default terminal chat is Python-native and does not require Bun,
npm, or OpenTUI node modules. OpenTUI is a source-checkout preview backend
selected explicitly with `OPENSQUILLA_TUI_BACKEND=opentui`.
## Plugin Slots
Plugins consume renderer-independent events and publish small snapshots through
named slots. Current slots include:
| Slot | Purpose |
| --- | --- |
| `router_hud` | Active-turn model-routing decision. |
| `status` | Compact status or queue notices. |
| `tool_activity` | Tool cards and tool summary history. |
| `usage` | Token, cache, and cost summary. |
| `inspector` | Optional detail panel state for selected items. |
The first plugin is `RouterHudPlugin`. It listens for
`router_decision` events and updates the bottom toolbar without changing router
selection behavior.
## Router HUD
When routing metadata is available, capable TUI backends can render a Router
HUD. In the current implementation, the OpenTUI footer is the primary preview
display for this HUD. The HUD is display-only: it consumes turn metadata and
does not change model selection.
The HUD can show:
- selected tier and model;
- baseline model;
- route source;
- confidence;
- estimated savings;
- fallback state;
- thinking mode;
- prompt policy;
- whether routing was applied;
- rollout phase.
`routing_applied=true` with a full rollout is shown as an active route.
`routing_applied=false` or an observe rollout is shown as observe-only. Fallback
routes use warning styling.
## Backend Selection
The default backend is stable Python-native terminal chat.
The internal backend selector reads `OPENSQUILLA_TUI_BACKEND`. Unset or empty
values select stable terminal chat. Set the variable to `opentui` only in a
source checkout when evaluating the preview backend. Legacy values fail before
chat launch with a clear unsupported-backend error.
```sh
bun install --frozen-lockfile --cwd=src/opensquilla/cli/tui/opentui/package
OPENSQUILLA_TUI_BACKEND=opentui uv run opensquilla chat
```
The preview backend is loaded from the OpenTUI package next to the running
source tree; it is not required for normal terminal chat.
Do not add parallel terminal/frontend implementations without fresh product
direction and replay plus real-terminal evidence.
## Replay Benchmarks
The replay harness measures the OpenTUI rendering path without a live provider:
```sh
uv run python scripts/bench_tui_replay.py --renderer opentui --fixture long-stream --summary-json .artifacts/tui/opentui-long-stream.json
uv run python scripts/bench_tui_replay.py --renderer opentui --fixture dense-history --summary-json .artifacts/tui/opentui-dense-history.json
```
Summary fields include `renderer`, `fixture`, `available`, `skip_reason`,
`event_count`, `text_chars`, `tool_count`, `router_decision_count`, `wall_ms`,
`flush_count`, `max_buffer_chars`, `coalescing_ratio`, `transcript_items`,
`visible_items`, `expanded_tools`, `projection_wall_ms`,
`rendered_text_matches`, `plugin_error_count`, and `errors`.
Use the OpenTUI results as preview backend evidence.
For terminal-level launch and rendering evidence, use the
[real-terminal TUI harness](../tui-real-terminal-harness.md).