Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e2593b2
feat(memory): durable cross-session memory worker
rohitg00 Jul 14, 2026
56e2947
feat(memory): reuse queue + http workers; live-rig validation fixes
rohitg00 Jul 14, 2026
518e44c
feat(memory): incremental extraction cursor + block injection budget
rohitg00 Jul 14, 2026
9437d37
fix(memory): tell the model memory is ambient
rohitg00 Jul 14, 2026
cac3e93
feat(console): memory page with facts, graph, blocks, and recall panels
rohitg00 Jul 14, 2026
d7be20e
merge latest main into memory-worker
rohitg00 Jul 15, 2026
6c0a045
feat(console): scale the memory page to large banks
rohitg00 Jul 15, 2026
bc19950
fix(memory): binding retry on boot races + identity-question recall
rohitg00 Jul 15, 2026
6b19d69
feat(console): memory graph visual redesign + reload-from-disk refresh
rohitg00 Jul 15, 2026
b6ebed4
feat(console): draggable memory graph nodes
rohitg00 Jul 15, 2026
7c41264
docs(console): teach blocks vs facts in the blocks panel copy
rohitg00 Jul 15, 2026
bef512c
feat(console): memory bank picker in the chat composer
rohitg00 Jul 15, 2026
9e1c9cd
feat: memory chips on chat messages (origin exposed through session::…
rohitg00 Jul 15, 2026
5ec2b96
feat(router): router::embed + provider::openai::embed
rohitg00 Jul 15, 2026
27df533
fix(router): discover embed providers from the provider registry
rohitg00 Jul 15, 2026
61a344e
feat(memory): semantic recall via router::embed vector fusion
rohitg00 Jul 15, 2026
606ac01
feat(console): semantic marker on memory chips
rohitg00 Jul 15, 2026
053b12a
feat(console): rules and memories naming per team review
rohitg00 Jul 15, 2026
6821790
docs(memory): rules and memories vocabulary in agent-facing copy
rohitg00 Jul 15, 2026
d64bf97
refactor(memory): rename blocks to rules and facts to memories
rohitg00 Jul 15, 2026
b244ec1
fix(console): restore block display classes clobbered by memory rename
rohitg00 Jul 15, 2026
8786969
feat(memory): learn rules from corrections during extraction
rohitg00 Jul 15, 2026
b6f6ae0
fix(console): show save only when a rule is edited, add per-rule delete
rohitg00 Jul 15, 2026
bb5ef00
fix(memory): address review findings across worker and console
rohitg00 Jul 15, 2026
2383ec4
fix(memory): second review round — vector model tracking and UI edge …
rohitg00 Jul 15, 2026
013d859
fix(provider-openai): derive the embeddings URL from the configured a…
rohitg00 Jul 15, 2026
5aeac2c
docs(memory): bring every touched worker's meta artifacts current
rohitg00 Jul 15, 2026
8d8feb0
docs(memory): name the memory family and the consolidate sibling
rohitg00 Jul 15, 2026
6e71b05
docs(memory): record why store, recall, and observe fused into one wo…
rohitg00 Jul 15, 2026
84a2a4e
feat(memory-consolidate): scheduled dedup sibling with catch-up-on-boot
rohitg00 Jul 15, 2026
d1dc457
refactor(memory-consolidate): schedule on the cron trigger, expose RE…
rohitg00 Jul 15, 2026
da7e083
fix(memory-consolidate): review round — write safety and checkpoint h…
rohitg00 Jul 15, 2026
02d1e3a
fix(memory-consolidate): gate manual runs, boot inert on config failure
rohitg00 Jul 15, 2026
750e90f
test(memory): golden wire-schema snapshots for both memory workers
rohitg00 Jul 16, 2026
8df745b
test(memory): lift both workers to sibling-level coverage
rohitg00 Jul 16, 2026
264adad
feat(memory): tags within banks, LLM-assisted merging, and rule promo…
rohitg00 Jul 16, 2026
135ea73
feat(console): make memory legible — provenance, capture activity, ho…
rohitg00 Jul 16, 2026
80e6963
feat(memory): turn the recall tab into a full injection preview
rohitg00 Jul 16, 2026
d654767
ci(memory): release wiring for both memory workers + version bumps
rohitg00 Jul 16, 2026
c1400bb
Merge remote-tracking branch 'origin/main' into memory-worker
rohitg00 Jul 16, 2026
3b63f97
chore(memory): patch-increment versions over latest releases
rohitg00 Jul 16, 2026
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
1 change: 1 addition & 0 deletions .github/scripts/parse_publish_workers_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"iii-directory",
"image-resize",
"mcp",
"memory",
"scrapling",
"shell",
"storage",
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/create-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ on:
- image-resize
- llm-router
- mcp
- memory
- memory-consolidate
- opencode
- pi
- provider-anthropic
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ on:
- 'image-resize/v*'
- 'llm-router/v*'
- 'mcp/v*'
- 'memory/v*'
- 'memory-consolidate/v*'
- 'opencode/v*'
- 'pi/v*'
- 'provider-anthropic/v*'
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ npx skills add iii-hq/iii --all
| [`lsp`](lsp/) | Rust | Language Server for iii function ids, trigger configs, and worker discovery. Autocomplete / hover across JS/TS, Python, Rust. |
| [`lsp-vscode`](lsp-vscode/) | Node | VS Code extension package `iii-lsp`, embedding the `lsp` server. |
| [`image-resize`](image-resize/) | Rust | Image resize via channel I/O — JPEG/PNG/WebP with EXIF auto-orient, scale-to-fit / crop-to-fit. |
| [`llm-router`](llm-router/) | Rust | One front door + provider protocol in front of every LLM provider — `router::chat`/`router::complete`, provider registry + credentials, model catalog, and routing. See [`llm-router/README.md`](llm-router/README.md). |
| [`llm-router`](llm-router/) | Rust | One front door + provider protocol in front of every LLM provider — `router::chat`/`router::complete`/`router::embed`, provider registry + credentials, model catalog, and routing. See [`llm-router/README.md`](llm-router/README.md). |
| [`mcp`](mcp/) | Rust | MCP 2025-06-18 Streamable HTTP bridge — exposes iii functions tagged `mcp.expose` as MCP tools. |
| [`memory`](memory/) | Rust | Durable cross-session agent memory — named banks of always-injected markdown rules and auto-extracted memories, hybrid BM25 + entity + semantic recall, pinning, supersede-never-delete history, and two live trigger types. Plain files on disk; binds the harness `pre-generate` hook for injection and `turn-completed` for background capture. |
| [`memory-consolidate`](memory-consolidate/) | Rust | Scheduled hygiene sibling of `memory` — deterministic dedup of near-duplicate memories, supersede-only through the public memory functions, pinned untouchable, catch-up-on-boot scheduling. Removable without touching stored memory. |
| [`rbac-proxy`](rbac-proxy/) | Rust | RBAC boundary proxy for the iii worker protocol — opens its own port and reverse-proxies functions + channels to a trusted engine listener, authenticating each connection, gating every invocation and trigger binding, namespacing registrations, running middleware + registration hooks, and filtering the `engine::*` discovery results to the caller's boundaries. The [`console`](console/) reverse-proxy with the engine's RBAC vendored in front, out of process. |
| [`provider-anthropic`](provider-anthropic/) | Rust | Anthropic Messages API provider behind `llm-router` — `provider::anthropic::stream` with prompt caching, thinking, and live model discovery. |
| [`provider-llamacpp`](provider-llamacpp/) | Rust | llama.cpp server (`llama-server`) Chat Completions provider behind `llm-router` — `provider::llamacpp::stream` with optional (no-`--api-key`) auth, real json_schema-constrained output, and live model discovery via `/v1/models` + `/props`. |
| [`provider-openai`](provider-openai/) | Rust | OpenAI Chat Completions provider behind `llm-router` — `provider::openai::stream` with reasoning support and live chat-model discovery. |
| [`provider-openai`](provider-openai/) | Rust | OpenAI Chat Completions provider behind `llm-router` — `provider::openai::stream` with reasoning support and live chat-model discovery, plus `provider::openai::embed` for batch embeddings (OpenAI-compatible endpoints included). |
| [`provider-xai`](provider-xai/) | Rust | xAI (Grok) Chat Completions provider behind `llm-router` — `provider::xai::stream` with grok reasoning support and live model discovery against `api.x.ai`. |
| [`provider-zai`](provider-zai/) | Rust | Z.AI (GLM) Chat Completions provider behind `llm-router` — `provider::zai::stream` with GLM thinking/effort support and a curated catalog against `api.z.ai` (no upstream model listing). |
| [`shell`](shell/) | Rust | Unix shell + filesystem worker — `shell::exec` with denylist/timeout/output caps and background jobs; `fs::ls`/`stat`/`mkdir`/`rm`/`chmod`/`mv`/`grep`/`sed`/`read`/`write` with host jail, denylist, and size caps. |
Expand Down
2 changes: 1 addition & 1 deletion console/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion console/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[package]
name = "console"
version = "1.6.4"
version = "1.6.5"
edition = "2021"
publish = false

Expand Down
12 changes: 12 additions & 0 deletions console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ The human window into the [`worktree`](../worktree) worker: parallel agent check
- **Working-dir badge** — a conversation rooted in a managed worktree shows branch, short id, dirty `*` / ahead `+n` indicators, and a lifecycle dot instead of the plain path chip; the raw path stays reachable as the tooltip
- **Live land notices** — `worktree::landed` / `worktree::land-blocked` events surface in the chat as notices (target branch and merged sha, or the block reason and conflicted files) and refresh the badge

### Memory

The human window into the [`memory`](../memory) worker: named banks of always-injected markdown rules and auto-extracted memories. Lives in [`web/src/pages/Memory/`](web/src/pages/Memory). Presence-gated like Worktrees: the page appears only while the memory worker is connected (a direct `#/memory` hit lands on an install notice).

- **Bank rail** — every bank with live memory / pinned / rule counts and inline create
- **Rules tab (first)** — the bank's markdown rules as in-place editors (save appears only on edit; delete asks to confirm); the agent appends learned standing instructions to the auto-managed `learned` rule as you correct it in chat
- **Memories tab** — server-paged newest-first list with pin / edit-in-place / tombstone delete, a show-history toggle, and search that runs `memory::recall` (the ranked scorer, not a client filter)
- **Graph tab** — entity hubs with memories as spokes: level-of-detail for large banks, draggable nodes, wheel zoom/pan, click a node for an inspect card
- **Preview tab** — the whole turn before it happens: `memory::preview` composes the exact system-prompt rules section and the appended memories (ambient floor + budgets applied) for a hypothetical question, with clickable example questions from the bank's own content
- **Live** — the page re-reads off `memory::item-changed` / `memory::bank-changed` (poll fallback), so memories appear the moment they're learned
- **In chat** — a bank picker in the composer (session metadata `memory_bank`) and a memory chip on each assistant reply naming the bank, how many rules and memories fed the turn, and whether recall ran semantic; click it to expand the exact records

### Live catalogs

The composer's `@`-mentions and the model picker pull from the engine in real time.
Expand Down
12 changes: 10 additions & 2 deletions console/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { buildViewOptions } from '@/lib/nav-options'
import { cn } from '@/lib/utils'
import { Browser } from '@/pages/Browser'
import { Configuration } from '@/pages/Configuration'
import { Memory } from '@/pages/Memory'
import { TracesV2 } from '@/pages/TracesV2'
import { Workers } from '@/pages/Workers'
import { Worktrees } from '@/pages/Worktrees'
Expand Down Expand Up @@ -97,6 +98,8 @@ export function App() {
<Worktrees />
) : view === 'browser' ? (
<Browser />
) : view === 'memory' ? (
<Memory />
) : (
<TracesV2 />
)}
Expand Down Expand Up @@ -126,8 +129,13 @@ function Header({
// Optional-worker entries appear only while their worker is present; a
// direct #/worktrees or #/browser hit still lands on that page's install
// notice.
const { worktreeAvailable, browserAvailable } = useConversationsCtx()
const viewOptions = buildViewOptions(worktreeAvailable, browserAvailable)
const { worktreeAvailable, browserAvailable, memoryAvailable } =
useConversationsCtx()
const viewOptions = buildViewOptions(
worktreeAvailable,
browserAvailable,
memoryAvailable,
)
const onConsoleSettings = view === 'configuration'
return (
<header className="flex items-center justify-between pl-3 pr-6 h-12 border-b border-rule shrink-0">
Expand Down
73 changes: 73 additions & 0 deletions console/web/src/components/chat/BankPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useEffect, useState } from 'react'
import { Select } from '@/components/ui/Select'
import { listBanks, type MemoryBank } from '@/lib/memory'

/**
* In-chat memory bank picker — the "which memory am I using" control.
* Writing blogs? Pick the blog bank and its style rules + memories feed
* every turn. Switch to coding and a different memory applies; contexts
* never bleed. Selecting `auto` defers to the memory worker's configured
* default bank. Lives next to the composer as one compact dropdown and
* commits through session metadata (`memory_bank`), so it applies to the
* NEXT turn immediately, mid-conversation switches included.
*/

interface BankPickerProps {
/** Bank for THIS conversation; null = the worker's default bank. */
value: string | null
onChange: (next: string | null) => void
disabled?: boolean
}

const AUTO = '(auto)'

export function BankPicker({ value, onChange, disabled }: BankPickerProps) {
const [banks, setBanks] = useState<MemoryBank[]>([])

useEffect(() => {
let cancelled = false
void listBanks()
.then((next) => {
if (!cancelled) setBanks(next)
})
.catch(() => {})
return () => {
cancelled = true
}
}, [])

const known = banks.some((b) => b.name === value)
const options = [
{
value: AUTO,
label: 'memory: auto',
title: "use the memory worker's default bank",
},
...banks.map((b) => ({
value: b.name,
label: `memory: ${b.name}`,
title: `${b.memories} memories · ${b.rules} rules — rules + recalled memories from this bank feed every turn`,
})),
// A bank set elsewhere (CLI, another client) that we haven't listed.
...(value && !known
? [
{
value,
label: `memory: ${value}`,
title: 'set outside this console',
},
]
: []),
]

return (
<Select<string>
value={value ?? AUTO}
onChange={(next) => onChange(next === AUTO ? null : next)}
disabled={disabled}
aria-label="memory bank"
className="min-w-[120px] shrink-0"
options={options}
/>
)
}
8 changes: 8 additions & 0 deletions console/web/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1705,6 +1705,14 @@ export function ChatView({
onModelChange={(next) => onUpdateModel(conversation.id, next)}
showWorkingDir={workingDirEnabled}
workingDir={conversation.workingDir ?? null}
showMemoryBank={
backend.id === 'real' &&
(conversationsCtx?.memoryAvailable ?? false)
}
memoryBank={conversation.memoryBank ?? null}
onMemoryBankChange={(next) =>
conversationsCtx?.setMemoryBank(conversation.id, next)
}
workingDirLocked={false}
workingDirError={workingDirError}
defaultWorkingDir={defaultWorkingDir}
Expand Down
16 changes: 16 additions & 0 deletions console/web/src/components/chat/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
} from '@/types/chat'
import { AttachmentButton } from './AttachmentButton'
import { AttachmentChip } from './AttachmentChip'
import { BankPicker } from './BankPicker'
import { DirectoryPicker, type WorktreePickerOptions } from './DirectoryPicker'
import { LexicalShell } from './LexicalShell'
import { ModelPicker } from './ModelPicker'
Expand Down Expand Up @@ -61,6 +62,11 @@ interface ComposerProps {
/** Show the per-session working-directory picker (real backend only). */
showWorkingDir?: boolean
workingDir?: string | null
/** Show the memory bank picker (memory worker present, real backend). */
showMemoryBank?: boolean
/** This chat's memory bank; null = the worker's default bank. */
memoryBank?: string | null
onMemoryBankChange?: (next: string | null) => void
/**
* When true, the picker renders read-only. NOT set after the first send —
* the working dir stays re-scopable mid-conversation (ChatView always
Expand Down Expand Up @@ -140,6 +146,9 @@ export function Composer({
thinkingLevel,
showWorkingDir,
workingDir,
showMemoryBank,
memoryBank,
onMemoryBankChange,
workingDirLocked,
workingDirError,
defaultWorkingDir,
Expand Down Expand Up @@ -339,6 +348,13 @@ export function Composer({
className="shrink-0"
/>
) : null}
{showMemoryBank && onMemoryBankChange ? (
<BankPicker
value={memoryBank ?? null}
onChange={onMemoryBankChange}
disabled={optionsDisabled}
/>
) : null}
{showPermissionMode ? (
<PermissionModePicker
value={permissionMode}
Expand Down
107 changes: 107 additions & 0 deletions console/web/src/components/chat/MemoryChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useState } from 'react'
import { getMemory, type MemoryItem } from '@/lib/memory'
import { cn } from '@/lib/utils'

/**
* The "what fed this reply" chip on assistant messages: bank name + how
* many memories the memory worker injected into the generate. Click to
* expand the exact memories (fetched by id on first open) — memory acting
* visibly inside the conversation, not just in traces.
*/

interface MemoryChipProps {
memory: {
bank: string
memories: number
memoryIds: string[]
rules?: number
truncated?: boolean
semantic?: boolean
}
}

export function MemoryChip({ memory }: MemoryChipProps) {
const [open, setOpen] = useState(false)
const [memories, setMemories] = useState<MemoryItem[] | null>(null)
const [loading, setLoading] = useState(false)
const [loadError, setLoadError] = useState(false)

const parts = []
if (memory.memories > 0)
parts.push(`${memory.memories} memor${memory.memories === 1 ? 'y' : 'ies'}`)
if (memory.rules)
parts.push(`${memory.rules} rule${memory.rules === 1 ? '' : 's'}`)
if (parts.length === 0) return null

const toggle = async () => {
const next = !open
setOpen(next)
if (next && memories === null && memory.memoryIds.length > 0) {
setLoading(true)
setLoadError(false)
try {
const loaded = await Promise.all(
memory.memoryIds.map((id) => getMemory(memory.bank, id)),
)
setMemories(loaded.filter((m): m is MemoryItem => m !== null))
} catch {
// A failed fetch is not "no memories": show it as an error and
// let the next open retry.
setLoadError(true)
} finally {
setLoading(false)
}
}
}

return (
<span className="inline-flex flex-col items-start normal-case tracking-normal">
<button
type="button"
onClick={() => void toggle()}
title="what the memory worker fed this reply — click for the exact memories"
className={cn(
'font-mono text-[10px] lowercase px-1.5 py-0.5 border transition-colors',
open
? 'border-accent text-ink'
: 'border-rule text-ink-faint hover:border-ink hover:text-ink',
)}
>
memory: {memory.bank} · {parts.join(' · ')}
{memory.semantic ? ' · semantic' : ''}
{memory.truncated ? ' · truncated' : ''}
</button>
{open ? (
<span className="mt-1 flex flex-col gap-1 border border-rule-2 bg-panel px-2 py-1.5 max-w-md">
{loading ? (
<span className="font-mono text-[10px] lowercase text-ink-ghost">
loading memories…
</span>
) : loadError ? (
<span className="font-mono text-[10px] lowercase text-alert">
could not load memories — click to retry
</span>
) : memories && memories.length > 0 ? (
memories.map((memory) => (
<span
key={memory.id}
className="font-mono text-[11px] text-ink leading-snug"
>
- {memory.text}
{memory.pinned ? (
<span className="text-accent"> ·pinned</span>
) : null}
</span>
))
) : (
<span className="font-mono text-[10px] lowercase text-ink-ghost">
{memory.memoryIds.length === 0
? 'only rules fed this turn (always-injected markdown)'
: 'memories no longer available (superseded or bank changed)'}
</span>
)}
</span>
) : null}
</span>
)
}
4 changes: 3 additions & 1 deletion console/web/src/components/chat/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
UserMessage as UserMessageType,
} from '@/types/chat'
import { AttachmentChip } from './AttachmentChip'
import { MemoryChip } from './MemoryChip'
import { ThoughtMessage } from './ThoughtMessage'

interface MessageProps {
Expand Down Expand Up @@ -292,14 +293,15 @@ function AssistantMessage({ message }: { message: AssistantMessageType }) {
const showCaret = !!message.streaming
return (
<article className="flex flex-col gap-2">
<header className="font-mono text-[11px] uppercase tracking-[0.06em] text-ink-ghost flex items-center gap-2">
<header className="font-mono text-[11px] uppercase tracking-[0.06em] text-ink-ghost flex items-center gap-2 flex-wrap">
<Prompt symbol=">">assistant</Prompt>
{message.model ? (
<span className="text-ink-ghost">· {message.model}</span>
) : null}
{message.mode ? (
<span className="text-ink-ghost">· {message.mode}</span>
) : null}
{message.memory ? <MemoryChip memory={message.memory} /> : null}
</header>
<div className="pr-1">
{message.content ? (
Expand Down
Loading
Loading