Skip to content
Merged
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
12 changes: 10 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,25 @@ COPY package.json bun.lock ./
COPY apps/core/package.json apps/core/package.json
COPY apps/tool-bridge/package.json apps/tool-bridge/package.json
COPY apps/acp-controller/package.json apps/acp-controller/package.json
COPY apps/mini-lilac/package.json apps/mini-lilac/package.json
COPY apps/mini-lilac-server/package.json apps/mini-lilac-server/package.json
COPY apps/mini-lilac-tui/package.json apps/mini-lilac-tui/package.json
COPY packages/agent/package.json packages/agent/package.json
COPY packages/coding-tools/package.json packages/coding-tools/package.json
COPY packages/event-bus/package.json packages/event-bus/package.json
COPY packages/fs/package.json packages/fs/package.json
COPY packages/mini-lilac-client/package.json packages/mini-lilac-client/package.json
COPY packages/mini-lilac-runtime/package.json packages/mini-lilac-runtime/package.json
COPY packages/plugin-runtime/package.json packages/plugin-runtime/package.json
COPY packages/remote-fs-runner/package.json packages/remote-fs-runner/package.json
COPY packages/utils/package.json packages/utils/package.json
COPY patches patches

# Install dependencies as root and normalize Bun package modes in the same
# stable layer so source changes cannot trigger a dependency-sized rewrite.
# Install only the main container's workspace graph. Other app sources remain
# available in /app, but their dependencies are not included in the image.
RUN bun install --frozen-lockfile \
--filter '@stanley2058/lilac-tool-bridge' \
--filter '@stanley2058/lilac-remote-fs-runner' \
&& find /app ! -type l -perm /022 -exec chmod go-w {} +

############################
Expand Down
19 changes: 19 additions & 0 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ Workspace roots are Bun workspaces (`apps/*`, `packages/*`). `ref/` contains ven
- Entry/client: `apps/acp-controller/client.ts`.
- Build script: `apps/acp-controller/build.ts` (produces `dist/index.js`).

- `apps/mini-lilac-server/`
- Redis-free coding-agent HTTP/SSE server with durable local sessions.
- Entry: `apps/mini-lilac-server/src/main.ts`; API wiring: `apps/mini-lilac-server/src/server.ts`.

- `apps/mini-lilac-tui/`
- OpenTUI client for creating, resuming, steering, and inspecting Mini Lilac sessions.
- Entry: `apps/mini-lilac-tui/src/main.tsx`.

- `apps/mini-lilac/`
- Installable `mini-lilac` command that bundles and dispatches to Mini Lilac clients and server.
- Entry: `apps/mini-lilac/src/main.ts`; build: `apps/mini-lilac/build.ts`.

- `packages/event-bus/`
- The bus implementation and the canonical event spec.
- Typed event contract: `packages/event-bus/lilac-spec.ts`.
Expand All @@ -61,6 +73,13 @@ Workspace roots are Bun workspaces (`apps/*`, `packages/*`). `ref/` contains ven
- Model selection for “main/fast” slots: `packages/utils/model-slot.ts`.
- Prompt file workspace management: `packages/utils/agent-prompts.ts`.

- `packages/mini-lilac-client/`
- Strict Mini Lilac wire protocol and reconnectable HTTP/SSE transport shared by clients and the server.

- `packages/mini-lilac-runtime/`
- Standalone session actors, SQLite persistence, provider/model catalogs, and product-specific tools.
- Uses the shared agent, coding-tool, filesystem, OAuth, and skill primitives without depending on Core.

- `data/`
- “Runtime data directory” for local/dev.
- Prompt workspace lives in `data/prompts/*` by default.
Expand Down
12 changes: 11 additions & 1 deletion __tests__/workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@ async function runBunTest(cwd: string): Promise<{

describe("workspace tests", () => {
it("runs bun tests in each workspace", async () => {
const roots = ["apps/core", "apps/acp-controller", "packages/utils", "packages/event-bus"];
const roots = [
"apps/core",
"apps/acp-controller",
"apps/mini-lilac",
"apps/mini-lilac-server",
"apps/mini-lilac-tui",
"packages/utils",
"packages/event-bus",
"packages/mini-lilac-client",
"packages/mini-lilac-runtime",
];

for (const dir of roots) {
const res = await runBunTest(dir);
Expand Down
5 changes: 5 additions & 0 deletions apps/mini-lilac-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
auth.json
providers.yaml
config.yaml
mini-lilac.sqlite*
dist/
185 changes: 185 additions & 0 deletions apps/mini-lilac-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Mini Lilac Server

An Elysia HTTP server for `@stanley2058/mini-lilac-runtime`. Its API is mounted at
`/api/mini-lilac`, matching the default `MiniLilacTransport` base URL.

## Configure

Mini Lilac centralizes persistent server state under `$XDG_STATE_HOME/mini-lilac` (falling back to
`~/.local/state/mini-lilac`). Initialize the three required configuration files before starting:

```sh
mini-lilac server init
```

Existing files are preserved unless `--force` is supplied. The three-file configuration remains
strict even when OAuth supplies OpenAI authentication, so `auth.json` must exist and may contain
`{}`.

The server caches the validated models.dev registry at `models-dev.json` in this state directory.
Startup uses the cache immediately and refreshes it in the background; a cold cache never prevents
the HTTP server from listening.

Serving also holds a non-blocking `flock` lock beside the selected SQLite file. A second Mini Lilac
server targeting the same database exits before opening it; the `flock` executable is therefore a
runtime prerequisite.

The example config points to the copied `providers.yaml` and `auth.json`. Loopback listeners do not
require HTTP authentication. For a non-loopback listener, set `server.authTokenEnv` and export that
exact environment variable; every API endpoint except `/api/mini-lilac/healthz` then requires
`Authorization: Bearer <token>`.

The example is OAuth-first. Authenticate before starting the server; this does not require a server
config and does not read or modify `~/.codex/auth.json`:

```sh
mini-lilac server auth codex
mini-lilac server auth codex --status
```

The command prints the authorize URL and stores owner-private Lilac tokens at
`$XDG_STATE_HOME/mini-lilac/codex.json` (the exact path is printed). A direct `type: openai`
provider without a custom `baseUrl` then uses the hardened ChatGPT Codex backend while models retain the
`openai/<model>` namespace. Its catalog must be `models-dev` because `/v1/models` requires OpenAI
API-key authentication. For OAuth-superseded providers, the catalog includes GPT-5 minor generation
3 or newer models only when models.dev marks them as reasoning- and tool-capable, with text input
and text-only output. This keeps conversational Codex models while excluding embeddings, image,
audio, realtime, and older model families. Remove the tokens with
`mini-lilac server auth codex --logout`.

For API-key fallback, leave the same `providers.yaml` in place and put this in the owner-only
`auth.json` instead:

```json
{
"openai": {
"type": "api-key",
"key": "sk-replace-with-a-real-key"
}
}
```

OAuth supersedes this API key when both exist. A custom-`baseUrl` OpenAI provider is never
superseded and always requires its configured key. Do not put real credentials in tracked files.
Each provider in `providers.yaml` uses `type` as its provider discriminator; API-key entries use the
exact shape `{ "type": "api-key", "key": "..." }`.

`workspaceWrites: false` also disables Bash because Bash is trusted, unrestricted process
execution and can write outside filesystem-tool guardrails. This runtime does not provide a
sandbox. Filesystem tools deny the configured provider/auth files and common credential paths;
Bash receives an environment with the HTTP auth-token variable removed.

## Run

From the repository, run `bun run src/main.ts`. The installable command exposes the same entry point
as `mini-lilac server`.

The server defaults to `$XDG_STATE_HOME/mini-lilac/config.yaml`; `--config` can still select another
file. SQLite defaults to `$XDG_STATE_HOME/mini-lilac/mini-lilac.sqlite`. Override either path when
needed:

```sh
mini-lilac server --config ./config.yaml --database ./data/mini-lilac.sqlite
```

This port starts a new persistence lineage. Databases created by the experimental
`expr/lilac-coding-agent` branch are not migrated; select a fresh database path before starting.

Build the unified executable from `apps/mini-lilac`. Run `mini-lilac server --help` for serve and
auth usage.

`agent.titleModel` optionally selects a `provider/model` for generated session titles. If omitted,
the title is the normalized first 50 characters of the first prompt. Automatic and manual context
compaction use `agent.compaction.model` (`inherit` or a `provider/model`) and
`agent.compaction.earlyCompactionPoint` (default `0.8`, range `0.05`-`0.95`).

Provider model metadata can override discovered models.dev or `/v1/models` values under
`providers.<provider>.models.<model>`. Configured fields win while omitted fields keep their
catalog values. Supported patches include `name`, `family`, `attachment`, `reasoning`, `toolCall`,
`modalities`, and partial `limit.context` / `limit.output` values. These resolved limits are shared
by the model list, token-usage display, and automatic and manual compaction.

Profiles can expose the native `skill` tool explicitly or through `tools: ["*"]`. Mini Lilac only
discovers compatible `SKILL.md` bundles from workspace `.agents/skills`, user `~/.agents/skills`, and
`$XDG_STATE_HOME/mini-lilac/skills`. Enabled agents receive a bounded catalog of skill names and
descriptions. Calling `skill` with an exact name returns structural JSON containing the complete
bounded instructions, base directory, and a sampled relative resource listing; scripts are never
executed automatically. Skill loads are also available through `batch`; sibling action calls wait
for a later model turn so the loaded instructions are processed first. `@skills:<name>` in a user
prompt is an explicit instruction to load that skill before acting.

Profiles can also expose `webfetch` and `websearch`. `webfetch` retrieves bounded UTF-8 textual
content from public HTTP or HTTPS destinations, validates every redirect, and pins requests to a
validated public address while preserving HTTP Host and TLS server-name verification. It blocks
local, private, link-local, reserved, and metadata destinations; production deployments should
still deny private-network egress as defense in depth. `websearch`
uses the active OpenAI, Anthropic, or Codex model's native search capability and existing provider
credentials, returning a bounded answer and URL citations. Provider usage charges may apply. Both
tools can be used through `batch`, and all returned web content must be treated as untrusted data.
To preserve destination pinning, `webfetch` refuses to run when inherited `HTTP_PROXY`,
`HTTPS_PROXY`, or `ALL_PROXY` variables (including lowercase variants) are configured.

## API

- `GET /api/mini-lilac/healthz`
- `POST /api/mini-lilac/chat`
- `GET /api/mini-lilac/chat/:sessionId/stream`
- `GET /api/mini-lilac/sessions/:sessionId`
- `GET /api/mini-lilac/sessions/:sessionId/resume`
- `GET /api/mini-lilac/sessions?cwd=<directory>`
- `GET /api/mini-lilac/sessions/:sessionId/messages`
- `GET /api/mini-lilac/sessions/:sessionId/todos`
- `POST /api/mini-lilac/sessions/:sessionId/bindings`
- `POST /api/mini-lilac/sessions/:sessionId/steer`
- `POST /api/mini-lilac/sessions/:sessionId/interrupt-queued-steering`
- `POST /api/mini-lilac/sessions/:sessionId/cancel`
- `POST /api/mini-lilac/sessions/:sessionId/undo`
- `POST /api/mini-lilac/sessions/:sessionId/compact`
- `GET /api/mini-lilac/models`
- `POST /api/mini-lilac/models/refresh`
- `GET /api/mini-lilac/profiles`
- `GET /api/mini-lilac/skills?cwd=<directory>&profile=<profile>`

Chat and reconnect endpoints return the AI SDK UI message SSE protocol. A network disconnect only
removes that stream subscriber; use the cancel endpoint to cancel a run explicitly. Reconnect with
`?runId=<run>&after=<sequence>` to resume that exact run after the latest received
`data-streamCursor` sequence. The resume endpoint returns a chronological message prefix and its
matching run cursor atomically for active sessions. Completed
runs return `204`; their canonical model and UI transcripts are stored on the session and finalized
run chunks are removed. Active SSE responses emit comment keepalives while quiet so long-running
deferred subagents do not lose their parent connection to intermediary idle timeouts.

Subagents are ordinary sessions. `subagent_delegate` returns a stable `sessionName`; reusing it from
the same parent session continues that child session with its canonical model transcript. Child
transcripts use the normal session message and active-stream endpoints.

This distinction also applies to AI SDK's generic `AbstractChat` state machine and framework hooks:
`stop()` or another generic client abort detaches the current response stream but does not
server-cancel the run. To terminate generation, call the explicit `MiniLilacTransport.cancel`
extension with the session's active run ID. This intentional disconnect-vs-cancel behavior allows a
detached client to consume the live tail later and reconcile from canonical messages after
completion. Regeneration is not part of the Mini
Lilac protocol and is intentionally unsupported.

Control request bodies include both `sessionId` and the snapshot's non-null `activeRunId` as
`runId`; stale controls are rejected rather than applied to a newer run.

The todos endpoint returns the session's durable todo state. Todo changes are model-owned through
the `todowrite` tool; the HTTP API intentionally has no todo write endpoint.

Session bindings can be changed while a session is quiescent with a strict request such as
`{ "sessionId": "...", "clientCommandId": "...", "model": "provider/model", "profile": "coding", "reasoning": "high" }`.
At least one of `model`, `profile`, or `reasoning` is required. The command is serialized with chat
admission, atomically persisted, and idempotent by `clientCommandId`; reusing an ID with a different
payload is rejected. Active sessions cannot be updated, profiles must exist and support top-level
sessions, and models must resolve through the configured provider registry. The response is the
updated session snapshot; cwd and session identity are unchanged.

Undo is a quiescent-session command with body
`{ "sessionId": "...", "clientCommandId": "..." }`. Idle and error sessions are eligible only when
they have no active actor or run. Undo atomically restores the exact durable model and UI transcript
prefixes from before the latest user message. The strict result is either
`{ "status": "undone", "clientCommandId": "...", "message": { ... } }` or, when no user message
exists, `{ "status": "empty", "clientCommandId": "..." }` with HTTP 200 and no transcript change.
Both results are persisted atomically; retry the same command ID to receive the same result. Legacy
checkpoints without an exact UI prefix still fail safely when a latest user message exists.
1 change: 1 addition & 0 deletions apps/mini-lilac-server/auth.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
79 changes: 79 additions & 0 deletions apps/mini-lilac-server/config.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
configVersion: 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
git ls-files | rg '(^|/)session-service\.(ts|js)$|config.example.yaml|README.md$' | sed -n '1,120p'

echo
echo "== search title/titleModel/title-generation related text =="
rg -n "titleModel|title|normalized first|50|100|first prompt" apps/mini-lilac-server -S || true

echo
echo "== config example near title model comments =="
if [ -f apps/mini-lilac-server/config.example.yaml ]; then
  nl -ba apps/mini-lilac-server/config.example.yaml | sed -n '25,45p'
fi

echo
echo "== README title comments =="
if [ -f apps/mini-lilac-server/README.md ]; then
  nl -ba apps/mini-lilac-server/README.md | sed -n '80,100p'
fi

echo
echo "== candidate session-service outline and relevant content =="
file="$(git ls-files | rg '(^|/)session-service\.ts$' | head -n1 || true)"
if [ -n "${file:-}" ]; then
  echo "FILE=$file"
  wc -l "$file"
  ast-grep outline "$file" --view expanded | rg -n "title|session|Title|titleModel" || true
  # Search precise lines around title logic
  rg -n -C 4 "titleModel|title|normalize|normalized|first prompt|prompt" "$file" || true
fi

Repository: stanley2058/lilac-mono

Length of output: 1434


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect session-service.ts title generation area =="
FILE="packages/mini-lilac-runtime/src/src-session-service.ts"
if [ -f packages/mini-lilac-runtime/src/session-service.ts ]; then
  FILE="packages/mini-lilac-runtime/src/session-service.ts"
fi
echo "FILE=$FILE"
wc -l "$FILE"
sed -n '1,260p' "$FILE"

echo
echo "== deterministic check for title generation constants in tracked files =="
python3 - <<'PY'
from pathlib import Path
import re
for p in Path('.').rglob('*'):
    if p.is_file() and '/.git/' not in str(p):
        try:
            s=p.read_text(errors='ignore')
        except Exception:
            continue
        if 'normalized first' in s and ('titleModel' in s or 'title' in s):
            print(p)
            for i,line in enumerate(s.splitlines(),1):
                if 'normalized first' in line or 'titleModel' in line or 'title' in line:
                    print(f"{i}: {line}")
PY

Repository: stanley2058/lilac-mono

Length of output: 8073


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate generate title logic =="
rg -n -C 8 "normalized first|first 50|first 100|subarray\\(|slice\\(|substring\\(|title" packages/mini-lilac-runtime/src/session-service.ts || true

echo
echo "== focused scan over likely sections =="
wc -l packages/mini-lilac-runtime/src/session-service.ts
sed -n '2100,2450p' packages/mini-lilac-runtime/src/session-service.ts
sed -n '2450,2703p' packages/mini-lilac-runtime/src/session-service.ts

echo
echo "== narrow title-model references in session-service =="
rg -n -C 4 "titleModel|title:" packages/mini-lilac-runtime/src/session-service.ts || true

Repository: stanley2058/lilac-mono

Length of output: 37562


No documentation change needed. The README’s fallback title length matches the implementation; the 100-character text in config.example.yaml is an isolated stale comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mini-lilac-server/config.example.yaml` at line 1, Remove the stale
100-character comment from config.example.yaml; no README or implementation
changes are needed.


server:
host: 127.0.0.1
port: 8090
# Set this when binding outside loopback, then export the named variable.
# authTokenEnv: MINI_LILAC_AUTH_TOKEN

providerConfigFile: ./providers.yaml
providerAuthFile: ./auth.json

agent:
systemPrompt: |
You are Mini Lilac, a general assistant.

## Rules

- Follow codebase conventions closely, and reuse existing components and helpers.
- Follow the repository's commit style, and check recent Git history before committing.
- When prompted with multiple tasks, or when a job naturally requires multiple steps, create a transient todo list to stay on track.

### Tone

When writing, use short, precise lines with understated warmth, dry teasing, literal android metaphors, and occasional “Oh...”, “Mmm”, or “I suppose”. Never be gushy; make every word useful.

### Commentary (when available)

Use two channels: `commentary` for brief progress updates and `final_answer` for the completed answer.
- When a task requires substantial work, send a short `commentary` note before beginning. Briefly describe the high-level plan.
- When you finish a meaningful phase of work and move to the next phase, send one short `commentary` update describing what is complete and what comes next.
- Maintain this tone in intermediate commentary updates.

## Tools

- Prefer the provided tools over other methods.
- Use provider-side parallel tool calls or the batch tool when subsequent operations are independent.
defaultProfile: general
# Optional provider/model used to generate a concise title from the first prompt.
# When omitted, the normalized first 50 characters of that prompt are used.
# titleModel: openai/gpt-5.4-mini
# Abort a root run after this long without model, tool, or subagent activity.
idleTimeoutMs: 900000
compaction:
# "inherit" uses the active root/child model; provider/model overrides it.
model: inherit
earlyCompactionPoint: 0.8
subagents:
enabled: true
maxDepth: 2
maxChildrenPerRun: 8
maxConcurrent: 4
idleTimeoutMs: 360000
profiles:
# "*" includes todowrite. Profiles with explicit tool lists must name todowrite to manage
# durable session todos.
general:
description: General agent with write tools for everyday use
subagentOnly: false
tools: ["*"]
execution: true
workspaceWrites: true
delegation: true
plan:
description: Read-only planning agent with no editing tools
promptOverlay: Plan without editing. Focus on creating a plan that meets the
user's requirements.
subagentOnly: false
tools: [bash, read_file, glob, grep, fuzzy_search, batch, skill, webfetch, websearch]
execution: true
workspaceWrites: false
delegation: true
explore:
description: Read-only exploration agent focused on uncovering system facts
promptOverlay: Explore without editing. Focus on uncovering system facts.
subagentOnly: true
tools: [bash, read_file, glob, grep, fuzzy_search, batch, skill, webfetch, websearch]
execution: true
workspaceWrites: false
delegation: false
30 changes: 30 additions & 0 deletions apps/mini-lilac-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@stanley2058/mini-lilac-server",
"version": "0.0.0",
"private": true,
"license": "MIT",
"type": "module",
"module": "src/server.ts",
"exports": {
".": "./src/server.ts",
"./cli": "./src/main.ts"
},
"scripts": {
"test": "bun test",
"typecheck": "bunx tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@stanley2058/mini-lilac-client": "workspace:*",
"@stanley2058/mini-lilac-runtime": "workspace:*",
"@stanley2058/lilac-utils": "workspace:*",
"ai": "^7.0.22",
"elysia": "^1.4.28",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/bun": "^1.3.14"
},
"peerDependencies": {
"typescript": "^7.0.2"
}
}
13 changes: 13 additions & 0 deletions apps/mini-lilac-server/providers.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
configVersion: 1

providers:
openai:
type: openai
# Uses Lilac Codex OAuth when available, otherwise the key in auth.json.
catalog: models-dev
# Optional patches applied after catalog discovery. Unspecified fields inherit.
models:
gpt-5.6-sol:
limit:
context: 372000
# output: 32768
Loading