Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Surface summary of the most recent release dates. See the release ledger below f
- Refactoring: share lenient JSON parsing; centralize MCP command resolution.

### 2026-07-23
- Features: surface and repair dead summary-job backlog (#901); recover missed native transcripts; inference settings density + hierarchy refactor; select connected providers as backends + compact rows.
- Features: surface and repair dead summary-job backlog (#901); recover missed native transcripts; inference settings density + hierarchy refactor; select connected providers as backends + compact rows; add the constrained ARM edge runtime profile and MiniLM native embeddings.
- Bug fixes: expose macOS physical memory; harden native WASM inference; quiesce background inference on pause; harden sterile OpenCode inference; provider wall completeness, optional keys, OAuth window; support agent-managed models; parse JSON after model preambles; bound llama.cpp inputs; remove invalid hook ownership markers; stop native embedding smoke child within bun's afterEach budget; integrate pi-ai OAuth providers; make maintenance supersession sweep non-fatal without a provider; enable Astro ClientRouter for client-side navigation.
- Docs: update roadmap to reflect current priorities (dashboard, desktop, config UX).

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,10 +393,13 @@ Requirements:

Embeddings (choose one):

- **Built-in** (recommended) — no extra setup, runs locally via ONNX (`nomic-embed-text-v1.5`)
- **Built-in** (recommended) — no extra setup, runs locally via ONNX (`Xenova/all-MiniLM-L6-v2`, 384d)
- **Ollama** — alternative local option, requires `nomic-embed-text` model
- **OpenAI** — cloud option, requires `OPENAI_API_KEY`

For constrained 64-bit ARM devices, including the Raspberry Pi 3B+, use the
documented [edge installation](./docs/EDGE.md).

## Contributing

New to open source? Start with [Your First PR](./docs/FIRST-PR.md).
Expand Down
2 changes: 1 addition & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ Wizard steps:
- OpenAI API
- Skip embeddings
9. **Embedding Model** - Based on provider:
- Built-in: `nomic-embed-text-v1.5`
- Built-in: `Xenova/all-MiniLM-L6-v2` (384d)
- Ollama: `nomic-embed-text`, `all-minilm`, `mxbai-embed-large`
- OpenAI: text-embedding-3-small, text-embedding-3-large
- Ollama selections run preflight checks for binary availability,
Expand Down
28 changes: 20 additions & 8 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,14 @@ harnesses:
- openclaw
- opencode

runtime:
# "standard" or "edge" (fixed-file polling + lazy native embeddings)
profile: standard

embedding:
provider: ollama
model: nomic-embed-text
dimensions: 768
base_url: http://localhost:11434
provider: native
model: Xenova/all-MiniLM-L6-v2
dimensions: 384
promptSubmitTimeoutMs: 1000
# llama.cpp only: truncate inputs to stay below its physical batch limit
llamaCppMaxInputTokens: 1400
Expand Down Expand Up @@ -210,10 +213,10 @@ Vector embedding configuration for semantic memory search.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `provider` | string | `"ollama"` | `"ollama"` or `"openai"` |
| `model` | string | `"nomic-embed-text"` | Embedding model name |
| `dimensions` | number | `768` | Output vector dimensions |
| `base_url` | string | `"http://localhost:11434"` | Ollama API base URL |
| `provider` | string | `"native"` | `"native"`, `"llama-cpp"`, `"ollama"`, `"openai"`, or `"none"` |
| `model` | string | `"Xenova/all-MiniLM-L6-v2"` | Embedding model name; honored by the native worker |
| `dimensions` | number | `384` | Output vector dimensions (1-65536) |
| `base_url` | string | provider-specific | External provider API base URL |
| `api_key` | string | — | API key or `$secret:NAME` reference |
| `promptSubmitTimeoutMs` | number | `1000` | Explicit recall embedding timeout retained for compatibility, range 1000-300000 ms |

Expand All @@ -229,6 +232,15 @@ Recommended Ollama models:
| `all-minilm` | 384 | Faster, smaller vectors |
| `mxbai-embed-large` | 1024 | Better quality, more resource usage |

### runtime

`runtime.profile` accepts `standard` (default) or `edge`. The edge profile is
for the documented source-runtime install on constrained 64-bit ARM devices:
it uses fixed-file polling, skips the startup native-embedding probe, unloads
the isolated embedding child process after 30 seconds idle, and leaves
visualization libraries unloaded until their endpoints are called. See
[EDGE.md](./EDGE.md).

Recommended OpenAI models:

| Model | Dimensions | Notes |
Expand Down
81 changes: 81 additions & 0 deletions docs/EDGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Edge installation

Signet's edge profile supports constrained 64-bit ARM Linux devices, with the Raspberry Pi 3B+ as the minimum target. It uses the Bun/source daemon so self-contained native-binary assets are not mapped into the idle process.

## Install

Use a 64-bit Raspberry Pi OS image, install Git and Bun, then:

```bash
git clone https://github.com/Signet-AI/signetai.git
cd signetai
bun install --frozen-lockfile
bun run --filter '@signet/core' build
bun surfaces/cli/src/cli.ts setup
```

Add the profile to `~/.agents/agent.yaml`:

```yaml
runtime:
profile: edge

embedding:
provider: native
model: Xenova/all-MiniLM-L6-v2
dimensions: 384
```

Start the source runtime:

```bash
bun surfaces/cli/src/cli.ts daemon start
```

Do not replace that command with the globally installed self-contained `signet` binary on the constrained path. The standard binary prioritizes portability and offline assets; the source edge path prioritizes idle RSS. `SIGNET_RUNTIME_PROFILE=edge` is available as a temporary override.

## What the profile changes

- Uses a fixed-file 30-second poller instead of Chokidar/recursive watches.
- Leaves the native embedding model unloaded until first use.
- Runs native embeddings in a child process and terminates it after 30 seconds
idle, allowing the OS to reclaim model and ONNX Runtime RSS completely.
- Loads the cl100k tokenizer and inference/OAuth catalogs only when a request
needs them.
- Loads UMAP and Louvain only when their endpoints are called.
- Avoids pre-spawning the synthesis renderer and threaded extraction helper while idle.
- Uses the configured native model/dimensions; the default is the 384-dimensional MiniLM ONNX model.

The full memory pipeline remains available. Disable optional pipeline features in `agent.yaml` only if your workload does not need them; the edge profile does not silently change memory semantics.

## Verify the Pi target

After startup, wait 60 seconds without opening the embeddings status page, then find the daemon PID:

```bash
cat ~/.agents/.daemon/pid
```

Read RSS:

```bash
bun scripts/check-edge-runtime.ts
```

The idle result must report `idleRssPass: true`. Then run the embedding gate
with a warm model cache:

```bash
bun scripts/check-edge-runtime.ts --with-embedding
```

The Pi 3B+ gate is under five seconds. The command exits non-zero when either
the 100 MB idle limit or five-second embedding limit is missed. Record the
Signet version, OS image, architecture, Bun version, cache state, idle RSS,
and elapsed time when reporting results.

After 30 seconds without another embedding request, re-check `VmRSS`; it must return below 102400 kB.

## Compatibility

Existing installs on the old built-in native default migrate to `Xenova/all-MiniLM-L6-v2`/384d once. Custom native models and external embedding providers are untouched. Stored source memories remain intact; dimension/model mismatches are re-embedded by the existing tracker.
21 changes: 20 additions & 1 deletion docs/UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
# Upgrade Guide — Inference Provisioning Cutover (#947)
# Upgrade Guide

## Native embedding default and edge profile (#921)

Existing configs using the exact built-in `nomic-embed-text-v1.5`/768d
default migrate once to `Xenova/all-MiniLM-L6-v2`/384d. Custom native models,
custom dimensions, Ollama, llama.cpp, OpenAI, and disabled embeddings are not
changed. Source memories remain intact; the existing embedding tracker
re-embeds rows whose model or dimensions no longer match.

The model is now loaded from `embedding.model` and validated against
`embedding.dimensions`, so custom native settings are no longer ignored.
Verify that custom native model IDs are valid Transformers.js repositories and
that their configured dimensions match their output.

The optional `runtime.profile: edge` path is documented in
[EDGE.md](./EDGE.md). It uses the source/Bun daemon rather than the
self-contained release binary.

## Inference provisioning cutover (#947)

**Breaking change.** This release replaces Signet's hand-rolled inference
providers with two backends: **Pi** (`@earendil-works/pi-ai`) for every direct
Expand Down
1 change: 1 addition & 0 deletions docs/specs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ Legend:
| `predictive-memory-scorer` | approved | `docs/specs/approved/predictive-memory-scorer.md` | `memory-pipeline-v2`, `knowledge-architecture-schema`, `session-continuity-protocol` | - | |
| `multi-agent-support` | approved | `docs/specs/approved/multi-agent-support.md` | `memory-pipeline-v2` | - | |
| `signet-runtime` | approved | `docs/specs/approved/signet-runtime.md` | `memory-pipeline-v2` | - | |
| `edge-runtime-profile` | approved | `docs/specs/approved/edge-runtime-profile.md` | `signet-runtime` | - | Raspberry Pi 3B+ resource contract: source-runtime install, lazy MiniLM embeddings, fixed-file polling, and repeatable RSS/latency gates. |
| `model-provider-router` | approved | `docs/specs/approved/model-provider-router.md` | `signet-runtime` | - | Shared inference control plane for per-agent rosters, daemon workloads, native RPC, and OpenAI-compatible gateway routing |
| `pipeline-pause-control` | complete | `docs/specs/complete/pipeline-pause-control.md` | `memory-pipeline-v2` | - | CLI and dashboard pause/resume shipped on top of live daemon pause/resume API, paused-mode observability, and local Ollama unload |
| `daemon-startup-responsiveness` | planning | `docs/specs/planning/daemon-startup-responsiveness.md` | `memory-pipeline-v2` | - | Incident-driven stub for issue #331: keep /health responsive during large-db startup recovery and recover stale managed daemon processes cleanly |
Expand Down
49 changes: 49 additions & 0 deletions docs/specs/approved/edge-runtime-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: "Constrained ARM Edge Runtime Profile"
id: edge-runtime-profile
status: approved
informed_by:
- "https://github.com/Signet-AI/signetai/issues/921"
section: "Runtime"
depends_on:
- "signet-runtime"
success_criteria:
- "A Raspberry Pi 3B+ edge installation settles below 100 MB RSS before the lazily loaded embedding model is used."
- "The built-in embedding provider honors the configured model and dimensions and defaults new and legacy-default installs to Xenova/all-MiniLM-L6-v2 at 384 dimensions."
- "One query embedding completes in low single-digit seconds on a Raspberry Pi 3B+ using the native ONNX Runtime ARM64 backend."
- "The edge profile avoids recursive native file watchers, eagerly loaded visualization libraries, and startup embedding inference."
- "The edge install and measurement procedure is documented and repeatable."
scope_boundary: "Defines the supported source-runtime edge profile for 64-bit Linux ARM. The self-contained compiled binary remains the standard install and may retain embedded release assets."
draft_quality: "implementation contract"
---

# Constrained ARM Edge Runtime Profile

The minimum edge target is a Raspberry Pi 3B+ running a 64-bit Linux userspace. The `edge` profile is an explicit resource contract, not an automatic hardware guess. Operators select it with `runtime.profile: edge` in `agent.yaml` or the `SIGNET_RUNTIME_PROFILE=edge` environment variable.

The edge distribution runs the daemon from the source/Bun runtime tree. It does not use the self-contained compiled executable because that artifact intentionally embeds dashboard, connector, template, skill, worker, and WebAssembly release assets. Keeping the standard binary self-contained and keeping those bytes out of the constrained process are separate packaging contracts.

## Runtime behavior

- Native embeddings default to `Xenova/all-MiniLM-L6-v2` with 384 dimensions. The daemon passes the configured model and dimensions to the embedding worker instead of using hidden constants.
- Existing configs using the exact old native default migrate once to MiniLM. Custom models, non-native providers, and custom dimensions are preserved. Existing source memories are not deleted; the embedding tracker re-embeds model/dimension mismatches.
- Source-mode Transformers.js selects `onnxruntime-node`; its published package includes Linux ARM64 native bindings. Inference remains isolated in the existing worker thread.
- The edge daemon does not probe or load the native model during startup. First embedding use loads it in a process-isolated worker host. Thirty seconds after the last native embedding request, that process is terminated so the operating system reclaims its model/runtime memory.
- The cl100k tokenizer vocabulary and inference/OAuth provider catalogs initialize on first use rather than during daemon module evaluation.
- Workspace change detection polls the fixed canonical configuration/identity files every 30 seconds. It does not instantiate Chokidar or recursively watch the agent tree.
- UMAP and Louvain modules load only when their visualization/repair endpoints are invoked.
- Idle synthesis and extraction helper threads are not pre-spawned; the edge profile uses the existing in-process/on-demand paths while preserving pipeline semantics.

The standard profile retains Chokidar, the startup provider probe, and a five-minute native embedding idle window.

## Acceptance measurement

The supported procedure in `docs/EDGE.md` is the release gate:

1. Use a Raspberry Pi 3B+ with a 64-bit Linux userspace and no swap activity.
2. Start the source runtime with the edge profile and wait 60 seconds without calling an embedding endpoint.
3. Record daemon RSS from `/proc/<pid>/status`; it must be below 100 MB.
4. Run one warm query embedding and record wall time; it must complete in fewer than five seconds.
5. Wait for the 30-second unload window and verify RSS returns below 100 MB.

Measurements must name the Signet version, OS image, architecture, Bun version, and whether the model cache was warm. Results from another machine may inform development but do not substitute for the Pi 3B+ release gate.
17 changes: 17 additions & 0 deletions docs/specs/dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,23 @@ specs:
- "Signet operates as a standalone runtime channel independent of harness-specific connectors"
decision: null

- id: edge-runtime-profile
status: approved
path: docs/specs/approved/edge-runtime-profile.md
hard_depends_on:
- signet-runtime
soft_depends_on: []
blocks: []
informed_by:
- https://github.com/Signet-AI/signetai/issues/921
success_criteria:
- "A Raspberry Pi 3B+ edge installation settles below 100 MB RSS before the lazily loaded embedding model is used"
- "The built-in embedding provider honors the configured model and dimensions and defaults new and legacy-default installs to Xenova/all-MiniLM-L6-v2 at 384 dimensions"
- "One query embedding completes in low single-digit seconds on a Raspberry Pi 3B+ using the native ONNX Runtime ARM64 backend"
- "The edge profile avoids recursive native file watchers, eagerly loaded visualization libraries, and startup embedding inference"
- "The edge install and measurement procedure is documented and repeatable"
decision: null

- id: model-provider-router
status: approved
path: docs/specs/approved/model-provider-router.md
Expand Down
4 changes: 4 additions & 0 deletions platform/core/src/embedding-defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const DEFAULT_NATIVE_EMBEDDING_MODEL = "Xenova/all-MiniLM-L6-v2";
export const DEFAULT_NATIVE_EMBEDDING_DIMENSIONS = 384;
export const DEFAULT_NATIVE_EMBEDDING_IDLE_UNLOAD_MS = 5 * 60 * 1000;
export const EDGE_NATIVE_EMBEDDING_IDLE_UNLOAD_MS = 30 * 1000;
6 changes: 6 additions & 0 deletions platform/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

export { Signet } from "./signet";
export { Database, findSqliteVecExtension, loadSqliteVec } from "./database";
export {
DEFAULT_NATIVE_EMBEDDING_DIMENSIONS,
DEFAULT_NATIVE_EMBEDDING_IDLE_UNLOAD_MS,
DEFAULT_NATIVE_EMBEDDING_MODEL,
EDGE_NATIVE_EMBEDDING_IDLE_UNLOAD_MS,
} from "./embedding-defaults";
export {
MEMORY_TYPES,
EXTRACTION_STATUSES,
Expand Down
2 changes: 2 additions & 0 deletions platform/daemon/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const targets: Array<{
{ entrypoint: "./src/mcp-stdio.ts", outfile: "./dist/mcp-stdio.js" },
{ entrypoint: "./src/index.ts", outfile: "./dist/index.js" },
{ entrypoint: "./src/synthesis-render-worker.ts", outfile: "./dist/synthesis-render-worker.js" },
{ entrypoint: "./src/embedding-process.ts", outfile: "./dist/embedding-process.js" },
{ entrypoint: "./src/embedding-worker.ts", outfile: "./dist/embedding-worker.js" },
{ entrypoint: "./src/pipeline/extraction-thread.ts", outfile: "./dist/extraction-thread.js" },
];

Expand Down
58 changes: 58 additions & 0 deletions platform/daemon/src/config-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { DEFAULT_NATIVE_EMBEDDING_DIMENSIONS, DEFAULT_NATIVE_EMBEDDING_MODEL } from "@signet/core";
import { type Document, isMap, isPair, parseDocument } from "yaml";
import { logger } from "./logger";

Expand Down Expand Up @@ -407,3 +408,60 @@ export function migrateLegacyRoutingToRegistry(agentsDir: string): void {
});
}
}

// ---------------------------------------------------------------------------
// v5: move the built-in embedding default to MiniLM (#921)
// ---------------------------------------------------------------------------

const LEGACY_NATIVE_MODELS = new Set(["nomic-embed-text-v1.5", "nomic-ai/nomic-embed-text-v1.5"]);

export function migrateNativeEmbeddingModel(agentsDir: string): void {
const path = findConfigPath(agentsDir);
if (!path) return;

let text: string;
try {
text = readFileSync(path, "utf-8");
} catch {
return;
}
const vMatch = /^configVersion:\s*(\d+)/m.exec(text);
if (vMatch && Number(vMatch[1]) >= 5) return;

const doc = parseDocument(text);
if (doc.errors.length > 0) {
logger.warn("config-migration", "Skipping native embedding migration: agent.yaml has parse errors", {
file: path,
errors: doc.errors.map((error) => error.message).slice(0, 3),
});
return;
}

const embedding = doc.getIn(["embedding"], true);
let migrated = false;
if (isMap(embedding)) {
const provider = String(embedding.get("provider") ?? "");
const model = String(embedding.get("model") ?? "");
const dimensions = embedding.get("dimensions");
if (
(provider === "native" || provider === "local") &&
LEGACY_NATIVE_MODELS.has(model) &&
(dimensions === undefined || dimensions === 768)
) {
embedding.set("model", DEFAULT_NATIVE_EMBEDDING_MODEL);
embedding.set("dimensions", DEFAULT_NATIVE_EMBEDDING_DIMENSIONS);
migrated = true;
}
}

stampConfigVersion(doc, 5);
writeAtomic(path, doc.toString());
if (migrated) {
logger.info("config-migration", "Migrated native embedding model to the edge default", {
file: path,
model: DEFAULT_NATIVE_EMBEDDING_MODEL,
dimensions: DEFAULT_NATIVE_EMBEDDING_DIMENSIONS,
note: "Existing rows are re-embedded by the embedding tracker without deleting source memories.",
});
}
}
6 changes: 3 additions & 3 deletions platform/daemon/src/context-budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export function selectWithTokenBudget<T extends { content: string }>(rows: Reado
}

const TRUNCATED_MARKER = "\n[context truncated]";
const TRUNCATED_MARKER_TOKENS = countTokens(TRUNCATED_MARKER);

/**
* Truncate `inject` to fit within `mainBudget` tokens.
Expand All @@ -52,7 +51,8 @@ const TRUNCATED_MARKER_TOKENS = countTokens(TRUNCATED_MARKER);
export function applyTokenBudget(inject: string, mainBudget: number): string {
if (mainBudget <= 0) return "";
if (countTokens(inject) <= mainBudget) return inject;
const truncatedMarkerTokens = countTokens(TRUNCATED_MARKER);
// Budget too tight to fit content + marker — truncate without marker.
if (mainBudget <= TRUNCATED_MARKER_TOKENS) return truncateToTokens(inject, mainBudget);
return truncateToTokens(inject, mainBudget - TRUNCATED_MARKER_TOKENS) + TRUNCATED_MARKER;
if (mainBudget <= truncatedMarkerTokens) return truncateToTokens(inject, mainBudget);
return truncateToTokens(inject, mainBudget - truncatedMarkerTokens) + TRUNCATED_MARKER;
}
Loading
Loading