From ac8826d0b1d5cd6d9998340418404d27b59c4235 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 11:12:32 +0300 Subject: [PATCH 01/24] fix: show real platform adapter status on Settings page (#1031) The Platform Connections section showed all adapters as "Not configured" because statuses were hardcoded to false. The /api/health endpoint also lacked adapter state information. - Add `adapters` field to health endpoint with real adapter status - Pass adapter state from index.ts via lazy callback (Telegram initializes after server.listen(), so eager read would miss it) - Update frontend HealthResponse type and PlatformConnectionsSection to consume real adapter data with optional fallback Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/server/src/index.ts | 10 ++++++- packages/server/src/routes/api.ts | 33 +++++++++++++++++++++++- packages/web/src/lib/api.ts | 8 ++++++ packages/web/src/routes/SettingsPage.tsx | 14 +++++----- 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 4d405b63ba..89cb176026 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -469,7 +469,15 @@ async function main(): Promise { }); // Register Web UI API routes - registerApiRoutes(app, webAdapter, lockManager); + // Lazy callback: telegram is initialized after server.listen(), so we read at request time + registerApiRoutes(app, webAdapter, lockManager, () => ({ + slack: slack !== null, + telegram: telegram !== null, + discord: discord !== null, + github: github !== null, + gitea: gitea !== null, + gitlab: gitlab !== null, + })); // GitHub webhook endpoint if (github) { diff --git a/packages/server/src/routes/api.ts b/packages/server/src/routes/api.ts index 81afc6db3d..c2c26cc4fa 100644 --- a/packages/server/src/routes/api.ts +++ b/packages/server/src/routes/api.ts @@ -805,6 +805,17 @@ const getCodebaseEnvironmentsRoute = createRoute({ }, }); +const adaptersSchema = z + .object({ + slack: z.boolean(), + telegram: z.boolean(), + discord: z.boolean(), + github: z.boolean(), + gitea: z.boolean(), + gitlab: z.boolean(), + }) + .openapi('Adapters'); + const getHealthRoute = createRoute({ method: 'get', path: '/api/health', @@ -818,6 +829,7 @@ const getHealthRoute = createRoute({ .object({ status: z.string(), adapter: z.string(), + adapters: adaptersSchema, concurrency: z.record(z.unknown()), runningWorkflows: z.number(), version: z.string().optional(), @@ -834,10 +846,28 @@ const getHealthRoute = createRoute({ /** * Register all /api/* routes on the Hono app. */ +/** Which platform adapters are currently active (instantiated and started). */ +export interface ActiveAdapters { + slack: boolean; + telegram: boolean; + discord: boolean; + github: boolean; + gitea: boolean; + gitlab: boolean; +} + export function registerApiRoutes( app: OpenAPIHono, webAdapter: WebAdapter, - lockManager: ConversationLockManager + lockManager: ConversationLockManager, + getActiveAdapters: () => ActiveAdapters = () => ({ + slack: false, + telegram: false, + discord: false, + github: false, + gitea: false, + gitlab: false, + }) ): void { function apiError( c: Context, @@ -2579,6 +2609,7 @@ export function registerApiRoutes( return c.json({ status: 'ok', adapter: 'web', + adapters: getActiveAdapters(), concurrency: { ...stats, active: allActiveIds.length, diff --git a/packages/web/src/lib/api.ts b/packages/web/src/lib/api.ts index f13034f274..b07614ea69 100644 --- a/packages/web/src/lib/api.ts +++ b/packages/web/src/lib/api.ts @@ -47,6 +47,14 @@ export interface CodebaseResponse { export interface HealthResponse { status: string; adapter: string; + adapters?: { + slack: boolean; + telegram: boolean; + discord: boolean; + github: boolean; + gitea: boolean; + gitlab: boolean; + }; concurrency: { active: number; queuedTotal: number; diff --git a/packages/web/src/routes/SettingsPage.tsx b/packages/web/src/routes/SettingsPage.tsx index 07a07690fc..2e873900cf 100644 --- a/packages/web/src/routes/SettingsPage.tsx +++ b/packages/web/src/routes/SettingsPage.tsx @@ -17,7 +17,7 @@ import { setCodebaseEnvVar, deleteCodebaseEnvVar, } from '@/lib/api'; -import type { SafeConfigResponse, CodebaseResponse } from '@/lib/api'; +import type { SafeConfigResponse, CodebaseResponse, HealthResponse } from '@/lib/api'; const selectClass = 'h-9 rounded-md border border-border bg-surface-elevated text-text-primary px-3 text-sm focus:outline-none focus:ring-1 focus:ring-ring [&>option]:bg-surface-elevated [&>option]:text-text-primary'; @@ -607,15 +607,17 @@ function AssistantConfigSection({ config }: { config: SafeConfigResponse }): Rea function PlatformConnectionsSection({ adapter, + adapters, }: { adapter: string | undefined; + adapters: HealthResponse['adapters'] | undefined; }): React.ReactElement { const platforms = [ { name: 'Web', connected: adapter === 'web' }, - { name: 'Slack', connected: false }, - { name: 'Telegram', connected: false }, - { name: 'Discord', connected: false }, - { name: 'GitHub', connected: false }, + { name: 'Slack', connected: adapters?.slack ?? false }, + { name: 'Telegram', connected: adapters?.telegram ?? false }, + { name: 'Discord', connected: adapters?.discord ?? false }, + { name: 'GitHub', connected: adapters?.github ?? false }, ]; return ( @@ -716,7 +718,7 @@ export function SettingsPage(): React.ReactElement {
{configData && } - +
From 30848b9bd758cd936a59a034c9e27041b0ff7018 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 15:30:45 +0300 Subject: [PATCH 02/24] bd init: initialize beads issue tracking --- .beads/.gitignore | 72 ++++++++++++++++++++++++++++ .beads/README.md | 81 +++++++++++++++++++++++++++++++ .beads/config.yaml | 54 +++++++++++++++++++++ .beads/hooks/applypatch-msg | 2 + .beads/hooks/commit-msg | 2 + .beads/hooks/h | 22 +++++++++ .beads/hooks/husky.sh | 9 ++++ .beads/hooks/post-applypatch | 2 + .beads/hooks/post-checkout | 26 ++++++++++ .beads/hooks/post-commit | 2 + .beads/hooks/post-merge | 26 ++++++++++ .beads/hooks/post-rewrite | 2 + .beads/hooks/pre-applypatch | 2 + .beads/hooks/pre-auto-gc | 2 + .beads/hooks/pre-commit | 26 ++++++++++ .beads/hooks/pre-merge-commit | 2 + .beads/hooks/pre-push | 26 ++++++++++ .beads/hooks/pre-rebase | 2 + .beads/hooks/prepare-commit-msg | 26 ++++++++++ .beads/metadata.json | 7 +++ .claude/settings.json | 60 +++++++++++++++-------- .gitignore | 5 ++ AGENTS.md | 84 +++++++++++++++++++++++++++++++++ CLAUDE.md | 48 +++++++++++++++++++ 24 files changed, 570 insertions(+), 20 deletions(-) create mode 100644 .beads/.gitignore create mode 100644 .beads/README.md create mode 100644 .beads/config.yaml create mode 100755 .beads/hooks/applypatch-msg create mode 100755 .beads/hooks/commit-msg create mode 100755 .beads/hooks/h create mode 100755 .beads/hooks/husky.sh create mode 100755 .beads/hooks/post-applypatch create mode 100755 .beads/hooks/post-checkout create mode 100755 .beads/hooks/post-commit create mode 100755 .beads/hooks/post-merge create mode 100755 .beads/hooks/post-rewrite create mode 100755 .beads/hooks/pre-applypatch create mode 100755 .beads/hooks/pre-auto-gc create mode 100755 .beads/hooks/pre-commit create mode 100755 .beads/hooks/pre-merge-commit create mode 100755 .beads/hooks/pre-push create mode 100755 .beads/hooks/pre-rebase create mode 100755 .beads/hooks/prepare-commit-msg create mode 100644 .beads/metadata.json create mode 100644 AGENTS.md diff --git a/.beads/.gitignore b/.beads/.gitignore new file mode 100644 index 0000000000..eb82c48f41 --- /dev/null +++ b/.beads/.gitignore @@ -0,0 +1,72 @@ +# Dolt database (managed by Dolt, not git) +dolt/ + +# Runtime files +bd.sock +bd.sock.startlock +sync-state.json +last-touched +.exclusive-lock + +# Daemon runtime (lock, log, pid) +daemon.* + +# Interactions log (runtime, not versioned) +interactions.jsonl + +# Push state (runtime, per-machine) +push-state.json + +# Lock files (various runtime locks) +*.lock + +# Credential key (encryption key for federation peer auth — never commit) +.beads-credential-key + +# Local version tracking (prevents upgrade notification spam after git ops) +.local_version + +# Worktree redirect file (contains relative path to main repo's .beads/) +# Must not be committed as paths would be wrong in other clones +redirect + +# Sync state (local-only, per-machine) +# These files are machine-specific and should not be shared across clones +.sync.lock +export-state/ +export-state.json + +# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned) +ephemeral.sqlite3 +ephemeral.sqlite3-journal +ephemeral.sqlite3-wal +ephemeral.sqlite3-shm + +# Dolt server management (auto-started by bd) +dolt-server.pid +dolt-server.log +dolt-server.lock +dolt-server.port +dolt-server.activity + +# Corrupt backup directories (created by bd doctor --fix recovery) +*.corrupt.backup/ + +# Backup data (auto-exported JSONL, local-only) +backup/ + +# Per-project environment file (Dolt connection config, GH#2520) +.env + +# Legacy files (from pre-Dolt versions) +*.db +*.db?* +*.db-journal +*.db-wal +*.db-shm +db.sqlite +bd.db +# NOTE: Do NOT add negation patterns here. +# They would override fork protection in .git/info/exclude. +# Config files (metadata.json, config.yaml) are tracked by git by default +# since no pattern above ignores them. diff --git a/.beads/README.md b/.beads/README.md new file mode 100644 index 0000000000..dbfe3631cf --- /dev/null +++ b/.beads/README.md @@ -0,0 +1,81 @@ +# Beads - AI-Native Issue Tracking + +Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code. + +## What is Beads? + +Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git. + +**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads) + +## Quick Start + +### Essential Commands + +```bash +# Create new issues +bd create "Add user authentication" + +# View all issues +bd list + +# View issue details +bd show + +# Update issue status +bd update --claim +bd update --status done + +# Sync with Dolt remote +bd dolt push +``` + +### Working with Issues + +Issues in Beads are: +- **Git-native**: Stored in Dolt database with version control and branching +- **AI-friendly**: CLI-first design works perfectly with AI coding agents +- **Branch-aware**: Issues can follow your branch workflow +- **Always in sync**: Auto-syncs with your commits + +## Why Beads? + +✨ **AI-Native Design** +- Built specifically for AI-assisted development workflows +- CLI-first interface works seamlessly with AI coding agents +- No context switching to web UIs + +🚀 **Developer Focused** +- Issues live in your repo, right next to your code +- Works offline, syncs when you push +- Fast, lightweight, and stays out of your way + +🔧 **Git Integration** +- Automatic sync with git commits +- Branch-aware issue tracking +- Dolt-native three-way merge resolution + +## Get Started with Beads + +Try Beads in your own projects: + +```bash +# Install Beads +curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash + +# Initialize in your repo +bd init + +# Create your first issue +bd create "Try out Beads" +``` + +## Learn More + +- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) +- **Quick Start Guide**: Run `bd quickstart` +- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) + +--- + +*Beads: Issue tracking that moves at the speed of thought* ⚡ diff --git a/.beads/config.yaml b/.beads/config.yaml new file mode 100644 index 0000000000..232b151111 --- /dev/null +++ b/.beads/config.yaml @@ -0,0 +1,54 @@ +# Beads Configuration File +# This file configures default behavior for all bd commands in this repository +# All settings can also be set via environment variables (BD_* prefix) +# or overridden with command-line flags + +# Issue prefix for this repository (used by bd init) +# If not set, bd init will auto-detect from directory name +# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. +# issue-prefix: "" + +# Use no-db mode: JSONL-only, no Dolt database +# When true, bd will use .beads/issues.jsonl as the source of truth +# no-db: false + +# Enable JSON output by default +# json: false + +# Feedback title formatting for mutating commands (create/update/close/dep/edit) +# 0 = hide titles, N > 0 = truncate to N characters +# output: +# title-length: 255 + +# Default actor for audit trails (overridden by BEADS_ACTOR or --actor) +# actor: "" + +# Export events (audit trail) to .beads/events.jsonl on each flush/sync +# When enabled, new events are appended incrementally using a high-water mark. +# Use 'bd export --events' to trigger manually regardless of this setting. +# events-export: false + +# Multi-repo configuration (experimental - bd-307) +# Allows hydrating from multiple repositories and routing writes to the correct database +# repos: +# primary: "." # Primary repo (where this database lives) +# additional: # Additional repos to hydrate from (read-only) +# - ~/beads-planning # Personal planning repo +# - ~/work-planning # Work planning repo + +# JSONL backup (periodic export for off-machine recovery) +# Auto-enabled when a git remote exists. Override explicitly: +# backup: +# enabled: false # Disable auto-backup entirely +# interval: 15m # Minimum time between auto-exports +# git-push: false # Disable git push (export locally only) +# git-repo: "" # Separate git repo for backups (default: project repo) + +# Integration settings (access with 'bd config get/set') +# These are stored in the database, not in this file: +# - jira.url +# - jira.project +# - linear.url +# - linear.api-key +# - github.org +# - github.repo diff --git a/.beads/hooks/applypatch-msg b/.beads/hooks/applypatch-msg new file mode 100755 index 0000000000..16aae78f5b --- /dev/null +++ b/.beads/hooks/applypatch-msg @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/.beads/hooks/commit-msg b/.beads/hooks/commit-msg new file mode 100755 index 0000000000..16aae78f5b --- /dev/null +++ b/.beads/hooks/commit-msg @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/.beads/hooks/h b/.beads/hooks/h new file mode 100755 index 0000000000..bf7c896408 --- /dev/null +++ b/.beads/hooks/h @@ -0,0 +1,22 @@ +#!/usr/bin/env sh +[ "$HUSKY" = "2" ] && set -x +n=$(basename "$0") +s=$(dirname "$(dirname "$0")")/$n + +[ ! -f "$s" ] && exit 0 + +if [ -f "$HOME/.huskyrc" ]; then + echo "husky - '~/.huskyrc' is DEPRECATED, please move your code to ~/.config/husky/init.sh" +fi +i="${XDG_CONFIG_HOME:-$HOME/.config}/husky/init.sh" +[ -f "$i" ] && . "$i" + +[ "${HUSKY-}" = "0" ] && exit 0 + +export PATH="node_modules/.bin:$PATH" +sh -e "$s" "$@" +c=$? + +[ $c != 0 ] && echo "husky - $n script failed (code $c)" +[ $c = 127 ] && echo "husky - command not found in PATH=$PATH" +exit $c diff --git a/.beads/hooks/husky.sh b/.beads/hooks/husky.sh new file mode 100755 index 0000000000..f9d0637909 --- /dev/null +++ b/.beads/hooks/husky.sh @@ -0,0 +1,9 @@ +echo "husky - DEPRECATED + +Please remove the following two lines from $0: + +#!/usr/bin/env sh +. \"\$(dirname -- \"\$0\")/_/husky.sh\" + +They WILL FAIL in v10.0.0 +" \ No newline at end of file diff --git a/.beads/hooks/post-applypatch b/.beads/hooks/post-applypatch new file mode 100755 index 0000000000..16aae78f5b --- /dev/null +++ b/.beads/hooks/post-applypatch @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/.beads/hooks/post-checkout b/.beads/hooks/post-checkout new file mode 100755 index 0000000000..813708c176 --- /dev/null +++ b/.beads/hooks/post-checkout @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" + +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run post-checkout "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'post-checkout' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run post-checkout "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'post-checkout'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/hooks/post-commit b/.beads/hooks/post-commit new file mode 100755 index 0000000000..16aae78f5b --- /dev/null +++ b/.beads/hooks/post-commit @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/.beads/hooks/post-merge b/.beads/hooks/post-merge new file mode 100755 index 0000000000..eda720ae76 --- /dev/null +++ b/.beads/hooks/post-merge @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" + +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run post-merge "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'post-merge' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run post-merge "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'post-merge'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/hooks/post-rewrite b/.beads/hooks/post-rewrite new file mode 100755 index 0000000000..16aae78f5b --- /dev/null +++ b/.beads/hooks/post-rewrite @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/.beads/hooks/pre-applypatch b/.beads/hooks/pre-applypatch new file mode 100755 index 0000000000..16aae78f5b --- /dev/null +++ b/.beads/hooks/pre-applypatch @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/.beads/hooks/pre-auto-gc b/.beads/hooks/pre-auto-gc new file mode 100755 index 0000000000..16aae78f5b --- /dev/null +++ b/.beads/hooks/pre-auto-gc @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/.beads/hooks/pre-commit b/.beads/hooks/pre-commit new file mode 100755 index 0000000000..0620b11441 --- /dev/null +++ b/.beads/hooks/pre-commit @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" + +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run pre-commit "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'pre-commit' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run pre-commit "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'pre-commit'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/hooks/pre-merge-commit b/.beads/hooks/pre-merge-commit new file mode 100755 index 0000000000..16aae78f5b --- /dev/null +++ b/.beads/hooks/pre-merge-commit @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/.beads/hooks/pre-push b/.beads/hooks/pre-push new file mode 100755 index 0000000000..7c1a4b8f71 --- /dev/null +++ b/.beads/hooks/pre-push @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" + +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run pre-push "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'pre-push' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run pre-push "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'pre-push'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/hooks/pre-rebase b/.beads/hooks/pre-rebase new file mode 100755 index 0000000000..16aae78f5b --- /dev/null +++ b/.beads/hooks/pre-rebase @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" \ No newline at end of file diff --git a/.beads/hooks/prepare-commit-msg b/.beads/hooks/prepare-commit-msg new file mode 100755 index 0000000000..98cfb10bff --- /dev/null +++ b/.beads/hooks/prepare-commit-msg @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/h" + +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run prepare-commit-msg "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'prepare-commit-msg' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run prepare-commit-msg "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'prepare-commit-msg'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 0000000000..89d57e90d3 --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,7 @@ +{ + "database": "dolt", + "backend": "dolt", + "dolt_mode": "embedded", + "dolt_database": "Archon", + "project_id": "50332000-ce1c-4bf3-9536-8e3549753f25" +} \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json index bd3b092546..0d6e5792b1 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -3,61 +3,81 @@ "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" }, "hooks": { - "SessionStart": [ + "Notification": [ { "hooks": [ { - "type": "command", - "command": ".claude/skills/save-task-list/hooks/verify-task-list.sh", - "statusMessage": "Checking for restored task list..." + "command": "kild agent-status --self waiting 2\u003e/dev/null || true", + "timeout": 5, + "type": "command" } ] } ], - "UserPromptSubmit": [ + "PreCompact": [ { "hooks": [ { - "type": "command", - "command": "kild agent-status --self working 2>/dev/null || true", - "timeout": 5 + "command": "bd prime", + "type": "command" + } + ], + "matcher": "" + } + ], + "SessionStart": [ + { + "hooks": [ + { + "command": ".claude/skills/save-task-list/hooks/verify-task-list.sh", + "statusMessage": "Checking for restored task list...", + "type": "command" } ] + }, + { + "hooks": [ + { + "command": "bd prime", + "type": "command" + } + ], + "matcher": "" } ], "Stop": [ { "hooks": [ { - "type": "command", - "command": "kild agent-status --self idle 2>/dev/null || true", - "timeout": 5 + "command": "kild agent-status --self idle 2\u003e/dev/null || true", + "timeout": 5, + "type": "command" } ] } ], "SubagentStop": [ { - "matcher": "rulecheck-agent", "hooks": [ { - "type": "command", "command": ".claude/skills/rulecheck/hooks/slack-notify.sh", - "statusMessage": "Notifying Slack..." + "statusMessage": "Notifying Slack...", + "type": "command" } - ] + ], + "matcher": "rulecheck-agent" } ], - "Notification": [ + "UserPromptSubmit": [ { "hooks": [ { - "type": "command", - "command": "kild agent-status --self waiting 2>/dev/null || true", - "timeout": 5 + "command": "kild agent-status --self working 2\u003e/dev/null || true", + "timeout": 5, + "type": "command" } ] } ] } -} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a2f33c5d5c..c861f9a169 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,8 @@ packages/server/.env skills-lock.json test-results/ .archon/ralph/ + +# Beads / Dolt files (added by bd init) +.dolt/ +*.db +.beads-credential-key diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..9390d72dbc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,84 @@ +# Agent Instructions + +This project uses **bd** (beads) for issue tracking. Run `bd prime` for full workflow context. + +## Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --claim # Claim work atomically +bd close # Complete work +bd dolt push # Push beads data to remote +``` + +## Non-Interactive Shell Commands + +**ALWAYS use non-interactive flags** with file operations to avoid hanging on confirmation prompts. + +Shell commands like `cp`, `mv`, and `rm` may be aliased to include `-i` (interactive) mode on some systems, causing the agent to hang indefinitely waiting for y/n input. + +**Use these forms instead:** +```bash +# Force overwrite without prompting +cp -f source dest # NOT: cp source dest +mv -f source dest # NOT: mv source dest +rm -f file # NOT: rm file + +# For recursive operations +rm -rf directory # NOT: rm -r directory +cp -rf source dest # NOT: cp -r source dest +``` + +**Other commands that may prompt:** +- `scp` - use `-o BatchMode=yes` for non-interactive +- `ssh` - use `-o BatchMode=yes` to fail instead of prompting +- `apt-get` - use `-y` flag +- `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var + + +## Beads Issue Tracker + +This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. + +### Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --claim # Claim work +bd close # Complete work +``` + +### Rules + +- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists +- Run `bd prime` for detailed command reference and session close protocol +- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files + +## Session Completion + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + git pull --rebase + bd dolt push + git push + git status # MUST show "up to date with origin" + ``` +5. **Clean up** - Clear stashes, prune remote branches +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** +- Work is NOT complete until `git push` succeeds +- NEVER stop before pushing - that leaves work stranded locally +- NEVER say "ready to push when you are" - YOU must push +- If push fails, resolve and retry until it succeeds + diff --git a/CLAUDE.md b/CLAUDE.md index 012498439f..f01da48cc8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -777,3 +777,51 @@ Pattern: Use `classifyIsolationError()` (from `@archon/isolation`) to map git er - Parse `@archon` in issue/PR **comments only** (not descriptions) - Events: `issue_comment` only - Note: Descriptions often contain example commands or documentation - these are NOT command invocations (see #96) + + + +## Beads Issue Tracker + +This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. + +### Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --claim # Claim work +bd close # Complete work +``` + +### Rules + +- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists +- Run `bd prime` for detailed command reference and session close protocol +- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files + +## Session Completion + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + git pull --rebase + bd dolt push + git push + git status # MUST show "up to date with origin" + ``` +5. **Clean up** - Clear stashes, prune remote branches +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** +- Work is NOT complete until `git push` succeeds +- NEVER stop before pushing - that leaves work stranded locally +- NEVER say "ready to push when you are" - YOU must push +- If push fails, resolve and retry until it succeeds + From d50162ada4960e5002c248b8758f51d867631cea Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 15:46:20 +0300 Subject: [PATCH 03/24] fix: separate memory system from beads issue tracking beads `bd remember` was pushing to upstream fork (403), causing memory loss. MEMORY.md files were prohibited but beads couldn't replace them. Now: Claude Code memory (MEMORY.md) for persistent knowledge, beads only for issue tracking. Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 3 ++- CLAUDE.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9390d72dbc..d062db0620 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,8 @@ bd close # Complete work - Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists - Run `bd prime` for detailed command reference and session close protocol -- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files +- Use Claude Code memory (MEMORY.md) for session-persistent knowledge (user profile, feedback, preferences) +- Use `bd` only for issue tracking — NOT for memory storage (beads pushes to upstream fork, not your remote) ## Session Completion diff --git a/CLAUDE.md b/CLAUDE.md index f01da48cc8..9656835098 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -797,7 +797,8 @@ bd close # Complete work - Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists - Run `bd prime` for detailed command reference and session close protocol -- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files +- Use Claude Code memory (MEMORY.md) for session-persistent knowledge (user profile, feedback, preferences) +- Use `bd` only for issue tracking — NOT for memory storage (beads pushes to upstream fork, not your remote) ## Session Completion From a63ca2e2b016ad731c6eaacb6a3c15beb4d69bbe Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 15:53:12 +0300 Subject: [PATCH 04/24] feat(telegram): add forum topic support Each forum topic gets a unique conversation ID (chatId:threadId), enabling per-topic conversation isolation and project binding. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../adapters/src/chat/telegram/adapter.ts | 90 +++++++++++++++---- 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/packages/adapters/src/chat/telegram/adapter.ts b/packages/adapters/src/chat/telegram/adapter.ts index c800612079..e12988a1f9 100644 --- a/packages/adapters/src/chat/telegram/adapter.ts +++ b/packages/adapters/src/chat/telegram/adapter.ts @@ -55,55 +55,90 @@ export class TelegramAdapter implements IPlatformAdapter { * - Short messages (≤4096 chars): Convert to MarkdownV2 for nice formatting * - Long messages: Split by paragraphs, format each chunk independently * (paragraphs rarely have formatting that spans across them) + * + * Forum topic support: + * - If chatId contains ":" (e.g. "-100123456:789"), the second part is the + * message_thread_id and replies go to that specific forum topic. */ async sendMessage(chatId: string, message: string, _metadata?: MessageMetadata): Promise { - const id = parseInt(chatId); - getLog().debug({ chatId, messageLength: message.length }, 'telegram.send_message'); + const { numericChatId, threadId } = this.parseChatId(chatId); + getLog().debug({ chatId, threadId, messageLength: message.length }, 'telegram.send_message'); if (message.length <= MAX_LENGTH) { - // Short message: try MarkdownV2 formatting - await this.sendFormattedChunk(id, message); + await this.sendFormattedChunk(numericChatId, message, threadId); } else { - // Long message: split by paragraphs, format each chunk getLog().debug({ messageLength: message.length }, 'telegram.message_splitting'); const chunks = splitIntoParagraphChunks(message, MAX_LENGTH - 200); for (const chunk of chunks) { - await this.sendFormattedChunk(id, chunk); + await this.sendFormattedChunk(numericChatId, chunk, threadId); } } } /** - * Send a single chunk with MarkdownV2 formatting, with fallback to plain text + * Parse a chatId that may contain a forum topic thread ID. + * Format: "chatId" or "chatId:threadId" */ - private async sendFormattedChunk(id: number, chunk: string): Promise { + private parseChatId(chatId: string): { numericChatId: number; threadId: number | undefined } { + const parts = chatId.split(':'); + return { + numericChatId: parseInt(parts[0]), + threadId: parts[1] ? parseInt(parts[1]) : undefined, + }; + } + + /** + * Send a single chunk with MarkdownV2 formatting, with fallback to plain text. + * If threadId is provided, sends to that forum topic. + */ + private async sendFormattedChunk( + id: number, + chunk: string, + threadId?: number + ): Promise { + // Build options: include thread ID only when targeting a forum topic + const threadExtra = threadId ? { message_thread_id: threadId } : undefined; + // If chunk is still too long after paragraph splitting, fall back to plain text if (chunk.length > MAX_LENGTH) { getLog().debug({ chunkLength: chunk.length }, 'telegram.chunk_too_long_plain_text'); const plainText = stripMarkdown(chunk); - // Split by lines if still too long const lines = plainText.split('\n'); let subChunk = ''; for (const line of lines) { if (subChunk.length + line.length + 1 > MAX_LENGTH - 100) { - if (subChunk) await this.bot.telegram.sendMessage(id, subChunk); + if (subChunk) { + if (threadExtra) { + await this.bot.telegram.sendMessage(id, subChunk, threadExtra); + } else { + await this.bot.telegram.sendMessage(id, subChunk); + } + } subChunk = line; } else { subChunk += (subChunk ? '\n' : '') + line; } } - if (subChunk) await this.bot.telegram.sendMessage(id, subChunk); + if (subChunk) { + if (threadExtra) { + await this.bot.telegram.sendMessage(id, subChunk, threadExtra); + } else { + await this.bot.telegram.sendMessage(id, subChunk); + } + } return; } // Try MarkdownV2 formatting const formatted = convertToTelegramMarkdown(chunk); + const markdownOptions = threadExtra + ? { parse_mode: 'MarkdownV2' as const, ...threadExtra } + : { parse_mode: 'MarkdownV2' as const }; try { - await this.bot.telegram.sendMessage(id, formatted, { parse_mode: 'MarkdownV2' }); - getLog().debug({ chunkLength: chunk.length }, 'telegram.markdownv2_chunk_sent'); + await this.bot.telegram.sendMessage(id, formatted, markdownOptions); + getLog().debug({ chunkLength: chunk.length, threadId }, 'telegram.markdownv2_chunk_sent'); } catch (error) { - // Fallback to stripped plain text for this chunk const err = error as Error; getLog().warn( { @@ -113,7 +148,11 @@ export class TelegramAdapter implements IPlatformAdapter { }, 'telegram.markdownv2_failed' ); - await this.bot.telegram.sendMessage(id, stripMarkdown(chunk)); + if (threadExtra) { + await this.bot.telegram.sendMessage(id, stripMarkdown(chunk), threadExtra); + } else { + await this.bot.telegram.sendMessage(id, stripMarkdown(chunk)); + } } } @@ -139,18 +178,29 @@ export class TelegramAdapter implements IPlatformAdapter { } /** - * Extract conversation ID from Telegram context + * Extract conversation ID from Telegram context. + * For forum topics (supergroups with topics enabled), includes the thread ID + * so each topic gets its own conversation: "chatId:threadId". + * For regular chats/groups, returns just the chat ID. */ getConversationId(ctx: Context): string { if (!ctx.chat) { throw new Error('No chat in context'); } - return ctx.chat.id.toString(); + const chatId = ctx.chat.id.toString(); + + // Check for forum topic (message_thread_id present on topic messages) + const msg = ctx.message; + if (msg && 'message_thread_id' in msg && msg.message_thread_id) { + return `${chatId}:${msg.message_thread_id}`; + } + + return chatId; } /** * Ensure responses go to a thread. - * Telegram doesn't have threads - each chat is a persistent conversation. + * For forum topics, the thread is already encoded in the conversation ID. * Returns original conversation ID unchanged. */ async ensureThread(originalConversationId: string, _messageContext?: unknown): Promise { @@ -188,6 +238,10 @@ export class TelegramAdapter implements IPlatformAdapter { if (this.messageHandler) { const conversationId = this.getConversationId(ctx); + // Debug: log forum topic detection + const msg = ctx.message; + const threadId = 'message_thread_id' in msg ? (msg as { message_thread_id?: number }).message_thread_id : undefined; + getLog().info({ chatId: ctx.chat?.id, threadId, conversationId, chatType: ctx.chat?.type }, 'telegram.message_received'); // Fire-and-forget - errors handled by caller void this.messageHandler({ conversationId, message, userId }); } From fef52c6c4cf0deb1568b1a742bda7b6190108cdb Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 15:57:25 +0300 Subject: [PATCH 05/24] feat: add /setproject command to bind codebase to conversation Resolves coleam00/Archon#1044. Adds deterministic /setproject command that updates conversation.codebase_id and cwd, enabling per-topic project binding in Telegram Forum Topics. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/rules/orchestrator.md | 5 ++- packages/core/src/handlers/command-handler.ts | 1 + .../src/orchestrator/orchestrator-agent.ts | 43 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/.claude/rules/orchestrator.md b/.claude/rules/orchestrator.md index acc3d64fa0..f70f05abef 100644 --- a/.claude/rules/orchestrator.md +++ b/.claude/rules/orchestrator.md @@ -14,7 +14,7 @@ Platform message → ConversationLockManager.acquireLock() → handleMessage() (orchestrator-agent.ts:383) → inheritThreadContext() — copy parent's codebase/cwd if child thread - → Deterministic gate: 10 commands (help, status, reset, workflow, register-project, update-project, remove-project, commands, init, worktree) + → Deterministic gate: 11 commands (help, status, reset, workflow, register-project, update-project, remove-project, setproject, commands, init, worktree) → Everything else → AI routing call: → listCodebases() + discoverAllWorkflows() → buildFullPrompt() → buildOrchestratorPrompt() or buildProjectScopedPrompt() @@ -29,7 +29,7 @@ Lock manager returns `{ status: 'started' | 'queued-conversation' | 'queued-capa ## Deterministic Commands (command-handler.ts) -Only **10 commands** are handled deterministically: +Only **11 commands** are handled deterministically: | Command | Behavior | |---------|----------| @@ -40,6 +40,7 @@ Only **10 commands** are handled deterministically: | `/register-project` | Handled inline — creates codebase DB record | | `/update-project` | Handled inline — updates codebase path | | `/remove-project` | Handled inline — deletes codebase DB record | +| `/setproject` | Handled inline — binds codebase to conversation (updates codebase_id + cwd) | | `/commands` | List registered codebase commands | | `/init` | Scaffold `.archon/` in current repo | | `/worktree` | Worktree subcommands | diff --git a/packages/core/src/handlers/command-handler.ts b/packages/core/src/handlers/command-handler.ts index 94227e54b9..d85ba357b6 100644 --- a/packages/core/src/handlers/command-handler.ts +++ b/packages/core/src/handlers/command-handler.ts @@ -919,6 +919,7 @@ Talk naturally — the orchestrator routes your requests to the right workflow a - \`/workflow reject \` — Reject a paused run **Projects** +- \`/setproject \` — Bind a project to this conversation - \`/register-project \` — Register a local project - \`/update-project \` — Update a project's path - \`/remove-project \` — Remove a registered project diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 97d989f47c..3f83d78b75 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -655,6 +655,7 @@ export async function handleMessage( 'register-project', 'update-project', 'remove-project', + 'setproject', 'commands', 'init', 'worktree', @@ -682,6 +683,13 @@ export async function handleMessage( return; } + if (command === 'setproject') { + getLog().debug({ command, conversationId }, 'deterministic_command'); + const result = await handleSetProject(message, conversationId); + await platform.sendMessage(conversationId, result); + return; + } + getLog().debug({ command, conversationId }, 'deterministic_command'); const result = await commandHandler.handleCommand(conversation, message); await platform.sendMessage(conversationId, result.message); @@ -1266,6 +1274,41 @@ async function handleRemoveProject(message: string): Promise { return `Project "${projectName}" removed.\nPath was: ${codebase.default_cwd}`; } +/** + * Handle /setproject command. + * Binds a registered codebase to the current conversation so all subsequent + * messages route to that project automatically. + */ +async function handleSetProject(message: string, conversationId: string): Promise { + const { args } = commandHandler.parseCommand(message); + if (args.length < 1) { + return 'Usage: /setproject '; + } + + const projectName = args.join(' '); + + // Find codebase (case-insensitive, partial path match) + const codebases = await codebaseDb.listCodebases(); + const codebase = findCodebaseByName(codebases, projectName); + + if (!codebase) { + const available = codebases.map(c => c.name).join(', '); + return `Project "${projectName}" not found.\nRegistered projects: ${available || 'none'}`; + } + + // Update conversation record + await db.updateConversation(conversationId, { + codebase_id: codebase.id, + cwd: codebase.default_cwd, + }); + + getLog().info( + { conversationId, projectName: codebase.name, codebaseId: codebase.id }, + 'project.setproject_completed' + ); + return `Project set to **${codebase.name}**\nWorking directory: ${codebase.default_cwd}`; +} + /** * Handle /workflow run command when project context may be missing. * Implements Edge Case E2 from the plan. From 88c9714354ef0a8cdf47b873d800fad6241da6b1 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 16:02:19 +0300 Subject: [PATCH 06/24] fix(setproject): use internal conversation ID, not platform ID updateConversation() expects the internal UUID (conversation.id), not the platform_conversation_id string. Fixes "Conversation not found" error in Telegram forum topics. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/orchestrator/orchestrator-agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 3f83d78b75..f6903dca4e 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -685,7 +685,7 @@ export async function handleMessage( if (command === 'setproject') { getLog().debug({ command, conversationId }, 'deterministic_command'); - const result = await handleSetProject(message, conversationId); + const result = await handleSetProject(message, conversation.id); await platform.sendMessage(conversationId, result); return; } From 26f28c64f33c083ffd21ce38527eb5684e5837eb Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 16:15:06 +0300 Subject: [PATCH 07/24] feat: add /compact and /resume for session context management /compact asks the AI to summarize the conversation, saves the summary to conversation.context_summary, and resets the session. The summary is automatically injected into subsequent prompts for continuity. /resume shows the stored summary. Useful for long Telegram forum topic conversations where 200k context may fill up. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/rules/orchestrator.md | 6 +- migrations/000_combined.sql | 1 + migrations/022_add_context_summary.sql | 5 + packages/core/src/db/adapters/sqlite.ts | 4 + packages/core/src/db/conversations.ts | 17 +++ packages/core/src/handlers/command-handler.ts | 2 + .../src/orchestrator/orchestrator-agent.ts | 103 +++++++++++++++++- packages/core/src/types/index.ts | 1 + 8 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 migrations/022_add_context_summary.sql diff --git a/.claude/rules/orchestrator.md b/.claude/rules/orchestrator.md index f70f05abef..9cb2605a4f 100644 --- a/.claude/rules/orchestrator.md +++ b/.claude/rules/orchestrator.md @@ -14,7 +14,7 @@ Platform message → ConversationLockManager.acquireLock() → handleMessage() (orchestrator-agent.ts:383) → inheritThreadContext() — copy parent's codebase/cwd if child thread - → Deterministic gate: 11 commands (help, status, reset, workflow, register-project, update-project, remove-project, setproject, commands, init, worktree) + → Deterministic gate: 13 commands (help, status, reset, compact, resume, workflow, register-project, update-project, remove-project, setproject, commands, init, worktree) → Everything else → AI routing call: → listCodebases() + discoverAllWorkflows() → buildFullPrompt() → buildOrchestratorPrompt() or buildProjectScopedPrompt() @@ -29,13 +29,15 @@ Lock manager returns `{ status: 'started' | 'queued-conversation' | 'queued-capa ## Deterministic Commands (command-handler.ts) -Only **11 commands** are handled deterministically: +Only **13 commands** are handled deterministically: | Command | Behavior | |---------|----------| | `/help` | Show available commands | | `/status` | Show conversation/session state | | `/reset` | Deactivate current session | +| `/compact` | Handled inline — AI summarizes session, saves summary, resets session | +| `/resume` | Handled inline — shows stored context summary | | `/workflow` | Subcommands: `list`, `run`, `status`, `cancel`, `reload` | | `/register-project` | Handled inline — creates codebase DB record | | `/update-project` | Handled inline — updates codebase path | diff --git a/migrations/000_combined.sql b/migrations/000_combined.sql index 176963b40e..201192d884 100644 --- a/migrations/000_combined.sql +++ b/migrations/000_combined.sql @@ -74,6 +74,7 @@ CREATE TABLE IF NOT EXISTS remote_agent_conversations ( title VARCHAR(255), deleted_at TIMESTAMP WITH TIME ZONE, hidden BOOLEAN DEFAULT FALSE, + context_summary TEXT, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), last_activity_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), diff --git a/migrations/022_add_context_summary.sql b/migrations/022_add_context_summary.sql new file mode 100644 index 0000000000..c45fc98196 --- /dev/null +++ b/migrations/022_add_context_summary.sql @@ -0,0 +1,5 @@ +-- Add context_summary to conversations for /compact session compression +-- Stores AI-generated summary of conversation context for session continuity + +ALTER TABLE remote_agent_conversations +ADD COLUMN IF NOT EXISTS context_summary TEXT; diff --git a/packages/core/src/db/adapters/sqlite.ts b/packages/core/src/db/adapters/sqlite.ts index 2864e4fc43..0ac5ff65d0 100644 --- a/packages/core/src/db/adapters/sqlite.ts +++ b/packages/core/src/db/adapters/sqlite.ts @@ -178,6 +178,9 @@ export class SqliteAdapter implements IDatabase { if (!colNames.has('hidden')) { this.db.run('ALTER TABLE remote_agent_conversations ADD COLUMN hidden INTEGER DEFAULT 0'); } + if (!colNames.has('context_summary')) { + this.db.run('ALTER TABLE remote_agent_conversations ADD COLUMN context_summary TEXT'); + } } catch (e: unknown) { getLog().warn({ err: e as Error }, 'db.sqlite_migration_conversations_columns_failed'); } @@ -281,6 +284,7 @@ export class SqliteAdapter implements IDatabase { title TEXT, deleted_at TEXT, hidden INTEGER DEFAULT 0, + context_summary TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), last_activity_at TEXT DEFAULT (datetime('now')), diff --git a/packages/core/src/db/conversations.ts b/packages/core/src/db/conversations.ts index 0a7a237da3..d82f7482c1 100644 --- a/packages/core/src/db/conversations.ts +++ b/packages/core/src/db/conversations.ts @@ -244,6 +244,23 @@ export async function updateConversationTitle(id: string, title: string): Promis } } +/** + * Update conversation context summary (used by /compact) + */ +export async function updateConversationSummary( + id: string, + summary: string | null +): Promise { + const dialect = getDialect(); + const result = await pool.query( + `UPDATE remote_agent_conversations SET context_summary = $1, updated_at = ${dialect.now()} WHERE id = $2`, + [summary, id] + ); + if (result.rowCount === 0) { + throw new ConversationNotFoundError(id); + } +} + /** * Soft delete a conversation (sets deleted_at timestamp) */ diff --git a/packages/core/src/handlers/command-handler.ts b/packages/core/src/handlers/command-handler.ts index d85ba357b6..35ba402e63 100644 --- a/packages/core/src/handlers/command-handler.ts +++ b/packages/core/src/handlers/command-handler.ts @@ -926,6 +926,8 @@ Talk naturally — the orchestrator routes your requests to the right workflow a **Session** - \`/status\` — Show current session and project info +- \`/compact\` — Summarize and compress the session (frees context window) +- \`/resume\` — Show saved context summary - \`/reset\` — Clear conversation and start fresh - \`/help\` — Show this help message diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index f6903dca4e..741771efd2 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -461,6 +461,11 @@ function buildFullPrompt( ? buildProjectScopedPrompt(scopedCodebase, codebases, workflows) : buildOrchestratorPrompt(codebases, workflows); + const summarySuffix = conversation.context_summary + ? '\n\n---\n\n## Previous Conversation Summary\n\nThe following is a summary from a prior session in this conversation. Use it as context:\n\n' + + conversation.context_summary + : ''; + const contextSuffix = issueContext ? '\n\n---\n\n## Additional Context\n\n' + issueContext : ''; const fileSuffix = @@ -474,6 +479,7 @@ function buildFullPrompt( if (threadContext) { return ( systemPrompt + + summarySuffix + '\n\n---\n\n## Thread Context (previous messages)\n\n' + threadContext + '\n\n---\n\n## Current Request\n\n' + @@ -483,7 +489,7 @@ function buildFullPrompt( ); } - return systemPrompt + '\n\n---\n\n## User Message\n\n' + message + contextSuffix + fileSuffix; + return systemPrompt + summarySuffix + '\n\n---\n\n## User Message\n\n' + message + contextSuffix + fileSuffix; } // ─── Main Handler ─────────────────────────────────────────────────────────── @@ -656,6 +662,8 @@ export async function handleMessage( 'update-project', 'remove-project', 'setproject', + 'compact', + 'resume', 'commands', 'init', 'worktree', @@ -690,6 +698,18 @@ export async function handleMessage( return; } + if (command === 'compact') { + getLog().debug({ command, conversationId }, 'deterministic_command'); + await handleCompact(platform, conversationId, conversation); + return; + } + + if (command === 'resume') { + getLog().debug({ command, conversationId }, 'deterministic_command'); + await handleResume(platform, conversationId, conversation); + return; + } + getLog().debug({ command, conversationId }, 'deterministic_command'); const result = await commandHandler.handleCommand(conversation, message); await platform.sendMessage(conversationId, result.message); @@ -1274,6 +1294,87 @@ async function handleRemoveProject(message: string): Promise { return `Project "${projectName}" removed.\nPath was: ${codebase.default_cwd}`; } +/** + * Handle /compact command. + * Summarizes the current conversation via AI, saves the summary, and resets the session. + * Next message will include the summary as context for continuity. + */ +async function handleCompact( + platform: IPlatformAdapter, + conversationId: string, + conversation: Conversation +): Promise { + const session = await sessionDb.getActiveSession(conversation.id); + if (!session?.assistant_session_id) { + await platform.sendMessage(conversationId, 'No active session to compact.'); + return; + } + + await platform.sendMessage(conversationId, 'Compacting session...'); + + // Ask the current session to summarize itself + const aiClient = getAssistantClient(conversation.ai_assistant_type); + const cwd = conversation.cwd ?? getArchonWorkspacesPath(); + let summary = ''; + + for await (const chunk of aiClient.sendQuery( + 'Summarize our entire conversation so far in a structured way. Include: key decisions made, current state of work, important context, and any pending items. Be concise but complete — this summary will be used to continue the conversation in a fresh session. Output ONLY the summary, no preamble.', + cwd, + session.assistant_session_id, + { tools: [] } + )) { + if (chunk.type === 'assistant') { + summary += chunk.content; + } + } + + if (!summary.trim()) { + await platform.sendMessage(conversationId, 'Failed to generate summary. Session not reset.'); + return; + } + + // Save summary and reset session + await db.updateConversationSummary(conversation.id, summary.trim()); + await sessionDb.deactivateSession(session.id, 'reset-requested'); + + getLog().info( + { conversationId, summaryLength: summary.length }, + 'session.compact_completed' + ); + await platform.sendMessage( + conversationId, + `Session compacted. Summary saved (${String(summary.length)} chars).\nNext message will start a fresh session with full context.` + ); +} + +/** + * Handle /resume command. + * Shows the stored context summary and confirms it will be loaded on next message. + */ +async function handleResume( + platform: IPlatformAdapter, + conversationId: string, + conversation: Conversation +): Promise { + if (!conversation.context_summary) { + await platform.sendMessage( + conversationId, + 'No saved context. Use `/compact` first to save a conversation summary.' + ); + return; + } + + const preview = + conversation.context_summary.length > 500 + ? conversation.context_summary.slice(0, 500) + '...' + : conversation.context_summary; + + await platform.sendMessage( + conversationId, + `**Saved context** (${String(conversation.context_summary.length)} chars):\n\n${preview}\n\n_This context is automatically loaded into every new message._` + ); +} + /** * Handle /setproject command. * Binds a registered codebase to the current conversation so all subsequent diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 549891f35e..14d9337f8f 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -27,6 +27,7 @@ export interface Conversation { ai_assistant_type: string; title: string | null; hidden: boolean; + context_summary: string | null; deleted_at: Date | null; last_activity_at: Date | null; // For staleness detection created_at: Date; From 0a0a3046daef0d7ae477f73acce6bd65328c7839 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 16:19:17 +0300 Subject: [PATCH 08/24] refactor: use settingSources instead of hardcoded Obsidian path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable user-level MCP servers (Obsidian, Apify, etc.) for all AI agent sessions by adding settingSources: [project, user] to config. Removes hardcoded vault path — agent can now use Obsidian MCP tools directly, same as Claude Code CLI. Co-Authored-By: Claude Opus 4.6 (1M context) --- .archon/config.yaml | 6 ++++++ packages/core/src/orchestrator/orchestrator-agent.ts | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.archon/config.yaml b/.archon/config.yaml index beadf10287..7cff7bd6ad 100644 --- a/.archon/config.yaml +++ b/.archon/config.yaml @@ -1,5 +1,11 @@ worktree: baseBranch: dev +assistants: + claude: + settingSources: + - project + - user + docs: path: packages/docs-web/src/content/docs diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 741771efd2..378f2472eb 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -1334,16 +1334,17 @@ async function handleCompact( } // Save summary and reset session - await db.updateConversationSummary(conversation.id, summary.trim()); + const trimmedSummary = summary.trim(); + await db.updateConversationSummary(conversation.id, trimmedSummary); await sessionDb.deactivateSession(session.id, 'reset-requested'); getLog().info( - { conversationId, summaryLength: summary.length }, + { conversationId, summaryLength: trimmedSummary.length }, 'session.compact_completed' ); await platform.sendMessage( conversationId, - `Session compacted. Summary saved (${String(summary.length)} chars).\nNext message will start a fresh session with full context.` + `Session compacted. Summary saved (${String(trimmedSummary.length)} chars).\nNext message will start a fresh session with full context.` ); } From 46bf9c1b16d068561944364264ef3d9048046d7a Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 16:23:28 +0300 Subject: [PATCH 09/24] fix(compact): fallback to message history when session expired Claude SDK sessions expire on server restart. /compact now catches the resume failure and falls back to building a summary from saved messages in the database. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/orchestrator/orchestrator-agent.ts | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 378f2472eb..e9efa945f0 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -21,6 +21,7 @@ import * as db from '../db/conversations'; import * as codebaseDb from '../db/codebases'; import * as sessionDb from '../db/sessions'; import * as commandHandler from '../handlers/command-handler'; +import * as messageDb from '../db/messages'; import { formatToolCall } from '@archon/workflows/utils/tool-formatter'; import { classifyAndFormatError } from '../utils/error-formatter'; import { toError } from '../utils/error'; @@ -1305,26 +1306,52 @@ async function handleCompact( conversation: Conversation ): Promise { const session = await sessionDb.getActiveSession(conversation.id); - if (!session?.assistant_session_id) { + if (!session) { await platform.sendMessage(conversationId, 'No active session to compact.'); return; } await platform.sendMessage(conversationId, 'Compacting session...'); - // Ask the current session to summarize itself const aiClient = getAssistantClient(conversation.ai_assistant_type); const cwd = conversation.cwd ?? getArchonWorkspacesPath(); let summary = ''; - for await (const chunk of aiClient.sendQuery( - 'Summarize our entire conversation so far in a structured way. Include: key decisions made, current state of work, important context, and any pending items. Be concise but complete — this summary will be used to continue the conversation in a fresh session. Output ONLY the summary, no preamble.', - cwd, - session.assistant_session_id, - { tools: [] } - )) { - if (chunk.type === 'assistant') { - summary += chunk.content; + // Try resuming the existing session for summarization. + // If the session expired (server restart), fall back to message history. + const resumeId = session.assistant_session_id ?? undefined; + const summarizePrompt = + 'Summarize our entire conversation so far in a structured way. Include: key decisions made, current state of work, important context, and any pending items. Be concise but complete — this summary will be used to continue the conversation in a fresh session. Output ONLY the summary, no preamble.'; + + try { + for await (const chunk of aiClient.sendQuery(summarizePrompt, cwd, resumeId, { + tools: [], + })) { + if (chunk.type === 'assistant') { + summary += chunk.content; + } + } + } catch { + // Session expired — build summary from saved messages + getLog().info({ conversationId }, 'session.compact_resume_failed_using_messages'); + const messages = await messageDb.listMessages(conversation.id, 50); + if (messages.length === 0) { + await platform.sendMessage(conversationId, 'No messages to summarize. Use /reset instead.'); + return; + } + + const transcript = messages + .map(m => `[${m.role}]: ${m.content.slice(0, 500)}`) + .join('\n\n'); + + const fallbackPrompt = `Summarize this conversation transcript. Include: key decisions, current state of work, important context, and pending items. Be concise but complete. Output ONLY the summary.\n\n---\n\n${transcript}`; + + for await (const chunk of aiClient.sendQuery(fallbackPrompt, cwd, undefined, { + tools: [], + })) { + if (chunk.type === 'assistant') { + summary += chunk.content; + } } } From e906e6882441014ee199a79e74849a004e9f747f Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 16:27:58 +0300 Subject: [PATCH 10/24] feat: auto-compact on expired sessions with transparent retry When Claude SDK session expires (server restart, TTL), the orchestrator now automatically: saves a summary from message history, resets the session, and retries the user's message with context injected. Users see a brief notice and get their response without manual /reset. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/orchestrator/orchestrator-agent.ts | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index e9efa945f0..56e2a8e3ab 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -509,11 +509,12 @@ export async function handleMessage( ): Promise { const { issueContext, threadContext, parentConversationId, isolationHints, attachedFiles } = context ?? {}; + let conversation: Conversation | undefined; try { getLog().debug({ conversationId }, 'orchestrator_message_received'); // 1. Get/create conversation and inherit thread context - let conversation = await db.getOrCreateConversation( + conversation = await db.getOrCreateConversation( platform.getPlatformType(), conversationId, undefined, @@ -831,6 +832,56 @@ export async function handleMessage( getLog().debug({ conversationId }, 'orchestrator_message_completed'); } catch (error) { const err = toError(error); + + // Auto-compact on expired session: save summary from messages, reset, and retry + if (conversation && err.message.includes('No conversation found with session ID')) { + getLog().info({ conversationId }, 'session.expired_auto_compacting'); + try { + const messages = await messageDb.listMessages(conversation.id, 50); + if (messages.length > 0) { + const transcript = messages + .map(m => `[${m.role}]: ${m.content.slice(0, 500)}`) + .join('\n\n'); + + const aiClient = getAssistantClient(conversation.ai_assistant_type); + const summaryCwd = conversation.cwd ?? getArchonWorkspacesPath(); + let summary = ''; + for await (const chunk of aiClient.sendQuery( + `Summarize this conversation transcript concisely. Include: key decisions, current state, important context, pending items. Output ONLY the summary.\n\n---\n\n${transcript}`, + summaryCwd, + undefined, + { tools: [] } + )) { + if (chunk.type === 'assistant') summary += chunk.content; + } + + if (summary.trim()) { + await db.updateConversationSummary(conversation.id, summary.trim()); + } + } + + // Reset the expired session + const expiredSession = await sessionDb.getActiveSession(conversation.id); + if (expiredSession) { + await sessionDb.deactivateSession(expiredSession.id, 'reset-requested'); + } + + await platform.sendMessage( + conversationId, + 'Session expired — context saved automatically. Retrying your message...' + ); + + // Retry the message (recursive call via handleMessage will create a fresh session) + await handleMessage(platform, conversationId, message, context); + return; + } catch (compactError) { + getLog().error( + { err: toError(compactError), conversationId }, + 'session.auto_compact_failed' + ); + } + } + getLog().error({ err, conversationId }, 'orchestrator_message_failed'); const userMessage = classifyAndFormatError(err); try { From b1d8db38c24ee9c2d2dae12744ce59d009cb9bd0 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 16:36:19 +0300 Subject: [PATCH 11/24] fix: broaden session expiry detection for auto-compact Match additional error patterns: invalid UUID, session not found variants. Ensures auto-compact triggers regardless of how the Claude SDK reports an expired session. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/orchestrator/orchestrator-agent.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 56e2a8e3ab..d70773740f 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -834,7 +834,11 @@ export async function handleMessage( const err = toError(error); // Auto-compact on expired session: save summary from messages, reset, and retry - if (conversation && err.message.includes('No conversation found with session ID')) { + const isSessionExpired = + err.message.includes('No conversation found with session ID') || + err.message.includes('not a valid UUID') || + err.message.includes('session') && err.message.includes('not found'); + if (conversation && isSessionExpired) { getLog().info({ conversationId }, 'session.expired_auto_compacting'); try { const messages = await messageDb.listMessages(conversation.id, 50); From 6935a72eb4ba7b1b20c1c060f4501789a4e3fbb3 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 16:40:53 +0300 Subject: [PATCH 12/24] feat: persist messages for all platforms + session log hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Messages are now saved to DB from the orchestrator level (stream + batch modes), enabling auto-compact summaries for Telegram, Slack, GitHub — not just Web/CLI. Also hints agent about Obsidian session logs for deeper history access. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/orchestrator/orchestrator-agent.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index d70773740f..93ae152a3c 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -463,7 +463,7 @@ function buildFullPrompt( : buildOrchestratorPrompt(codebases, workflows); const summarySuffix = conversation.context_summary - ? '\n\n---\n\n## Previous Conversation Summary\n\nThe following is a summary from a prior session in this conversation. Use it as context:\n\n' + + ? '\n\n---\n\n## Previous Conversation Summary\n\nThe following is a summary from a prior session in this conversation. Use it as context.\nIf you need more history, check the Obsidian vault at `Claude/Session-Logs/` using Obsidian MCP tools.\n\n' + conversation.context_summary : ''; @@ -975,6 +975,10 @@ async function handleStreamMode( } const fullResponse = allMessages.join(''); + + // Persist messages for all platforms (enables auto-compact, /compact, history) + await persistConversationMessages(conversation.id, originalMessage, fullResponse); + const commands = parseOrchestratorCommands(fullResponse, codebases, workflows); if (commands.workflowInvocation) { @@ -1108,6 +1112,9 @@ async function handleBatchMode( return; } + // Persist messages for all platforms (enables auto-compact, /compact, history) + await persistConversationMessages(conversation.id, originalMessage, finalMessage); + // Parse orchestrator commands from filtered response const commands = parseOrchestratorCommands(finalMessage, codebases, workflows); @@ -1350,6 +1357,28 @@ async function handleRemoveProject(message: string): Promise { return `Project "${projectName}" removed.\nPath was: ${codebase.default_cwd}`; } +/** + * Persist user + assistant messages to the database. + * Fire-and-forget — errors are logged but never thrown. + * These messages power auto-compact summaries when sessions expire. + */ +async function persistConversationMessages( + conversationDbId: string, + userMessage: string, + assistantResponse: string +): Promise { + try { + // Skip internal orchestrator commands (they're not useful context) + if (assistantResponse.startsWith('/invoke-workflow') || assistantResponse.startsWith('/register-project')) { + return; + } + await messageDb.addMessage(conversationDbId, 'user', userMessage); + await messageDb.addMessage(conversationDbId, 'assistant', assistantResponse); + } catch (error) { + getLog().warn({ err: error as Error, conversationDbId }, 'message.persist_failed'); + } +} + /** * Handle /compact command. * Summarizes the current conversation via AI, saves the summary, and resets the session. From 420611cd1a22db72ac1082c183301ca1060d6407 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 16:50:49 +0300 Subject: [PATCH 13/24] feat: shared memory via Obsidian session logs (CLI + Archon unified) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /compact now saves summaries to both DB (cache) and Obsidian vault (Claude/Session-Logs/{project}/). New sessions without context_summary automatically load up to 3 latest session logs from Obsidian — whether written by CLI (/compress) or Archon (/compact). This creates a unified memory timeline across both interfaces. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/orchestrator/orchestrator-agent.ts | 139 ++++++++++++++++-- 1 file changed, 130 insertions(+), 9 deletions(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 93ae152a3c..fbfb5801ba 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -7,6 +7,8 @@ * - Does NOT require a project to be selected before starting a conversation */ import { existsSync } from 'fs'; +import { writeFile, readFile, readdir, mkdir } from 'fs/promises'; +import { join } from 'path'; import { createLogger } from '@archon/paths'; import type { IPlatformAdapter, @@ -445,7 +447,7 @@ async function discoverAllWorkflows(conversation: Conversation): Promise { const scopedCodebase = conversation.codebase_id ? codebases.find(c => c.id === conversation.codebase_id) : undefined; @@ -462,9 +464,18 @@ function buildFullPrompt( ? buildProjectScopedPrompt(scopedCodebase, codebases, workflows) : buildOrchestratorPrompt(codebases, workflows); - const summarySuffix = conversation.context_summary - ? '\n\n---\n\n## Previous Conversation Summary\n\nThe following is a summary from a prior session in this conversation. Use it as context.\nIf you need more history, check the Obsidian vault at `Claude/Session-Logs/` using Obsidian MCP tools.\n\n' + - conversation.context_summary + // Load context: prefer DB summary, fall back to latest Obsidian session log + let contextContent = conversation.context_summary; + if (!contextContent && conversation.codebase_id) { + const codebase = codebases.find(c => c.id === conversation.codebase_id); + if (codebase) { + contextContent = await loadLatestSessionLog(getProjectSlug(codebase)); + } + } + + const summarySuffix = contextContent + ? '\n\n---\n\n## Previous Session Context\n\nThe following is context from a prior session (shared across CLI and Telegram). Use it to maintain continuity.\nFor more history, check Obsidian vault at `Claude/Session-Logs/` using Obsidian MCP tools.\n\n' + + contextContent : ''; const contextSuffix = issueContext ? '\n\n---\n\n## Additional Context\n\n' + issueContext : ''; @@ -761,7 +772,7 @@ export async function handleMessage( }); } - const fullPrompt = buildFullPrompt( + const fullPrompt = await buildFullPrompt( conversation, codebases, workflows, @@ -1357,6 +1368,102 @@ async function handleRemoveProject(message: string): Promise { return `Project "${projectName}" removed.\nPath was: ${codebase.default_cwd}`; } +// ─── Shared Memory (Obsidian Session Logs) ───────────────────────────────── + +/** Obsidian vault path (iCloud). Both CLI (/compress) and Archon (/compact) use this. */ +const VAULT_SESSION_LOGS = join( + process.env.HOME ?? '', + 'Library/Mobile Documents/iCloud~md~obsidian/Documents/Claude/Session-Logs' +); + +/** + * Resolve the project folder name from a codebase (last segment of name). + * e.g., "CryptixSamurai/ai-ofm" → "ai-ofm" + */ +function getProjectSlug(codebase: Codebase): string { + const parts = codebase.name.split('/'); + return parts[parts.length - 1]; +} + +/** + * Save a session summary to Obsidian vault as a session log. + * Mirrors /compress CLI format so both tools produce a unified timeline. + */ +async function saveSessionLogToVault( + projectSlug: string, + summary: string, + platform: string, + title?: string | null +): Promise { + try { + const dir = join(VAULT_SESSION_LOGS, projectSlug); + await mkdir(dir, { recursive: true }); + + const date = new Date().toISOString().slice(0, 10); + const slug = (title ?? 'compact-session') + .toLowerCase() + .replace(/[^a-z0-9а-яіїєґ]+/gu, '-') + .replace(/^-|-$/g, '') + .slice(0, 50); + const fileName = `${date}-${slug}.md`; + + await writeFile( + join(dir, fileName), + `---\ntype: session-log\ndate: ${date}\nproject: ${projectSlug}\nsource: archon-compact\nplatform: ${platform}\nstatus: completed\ntags: [claude, session, ${projectSlug.toLowerCase()}, archon]\n---\n\n# ${projectSlug} — Compact Summary\n\n${summary}\n`, + 'utf-8' + ); + return `Claude/Session-Logs/${projectSlug}/${fileName}`; + } catch (error) { + getLog().warn({ err: error as Error, projectSlug }, 'session.vault_save_failed'); + return null; + } +} + +/** Max session logs to load for context (most recent first) */ +const MAX_SESSION_LOGS = 3; +/** Max chars per session log to avoid blowing up the prompt */ +const MAX_LOG_CHARS = 2000; + +/** + * Load recent session logs from Obsidian vault for a project. + * Used when a new session has no context_summary — pulls shared memory + * that may have been written by CLI (/compress) or another Archon session. + * Loads up to MAX_SESSION_LOGS most recent logs, newest first. + */ +async function loadLatestSessionLog(projectSlug: string): Promise { + try { + const dir = join(VAULT_SESSION_LOGS, projectSlug); + if (!existsSync(dir)) return null; + + const files = await readdir(dir); + const sorted = files.filter(f => f.endsWith('.md')).sort().reverse(); + if (sorted.length === 0) return null; + + const logs: string[] = []; + for (const file of sorted.slice(0, MAX_SESSION_LOGS)) { + const content = await readFile(join(dir, file), 'utf-8'); + const body = content.replace(/^---[\s\S]*?---\s*/, '').trim(); + if (body) { + const truncated = body.length > MAX_LOG_CHARS + ? body.slice(0, MAX_LOG_CHARS) + '\n...(truncated)' + : body; + logs.push(`### ${file.replace('.md', '')}\n\n${truncated}`); + } + } + + if (logs.length === 0) return null; + + getLog().debug( + { projectSlug, count: logs.length, files: sorted.slice(0, MAX_SESSION_LOGS) }, + 'session.vault_logs_loaded' + ); + return logs.join('\n\n---\n\n'); + } catch (error) { + getLog().warn({ err: error as Error, projectSlug }, 'session.vault_load_failed'); + return null; + } +} + /** * Persist user + assistant messages to the database. * Fire-and-forget — errors are logged but never thrown. @@ -1444,18 +1551,32 @@ async function handleCompact( return; } - // Save summary and reset session + // Save summary to DB + Obsidian vault (shared with CLI /compress) const trimmedSummary = summary.trim(); await db.updateConversationSummary(conversation.id, trimmedSummary); await sessionDb.deactivateSession(session.id, 'reset-requested'); + let vaultPath: string | null = null; + if (conversation.codebase_id) { + const codebase = await codebaseDb.getCodebase(conversation.codebase_id); + if (codebase) { + vaultPath = await saveSessionLogToVault( + getProjectSlug(codebase), + trimmedSummary, + conversation.platform_type, + conversation.title + ); + } + } + getLog().info( - { conversationId, summaryLength: trimmedSummary.length }, + { conversationId, summaryLength: trimmedSummary.length, vaultPath }, 'session.compact_completed' ); + const vaultNote = vaultPath ? `\nSaved to Obsidian: ${vaultPath}` : ''; await platform.sendMessage( conversationId, - `Session compacted. Summary saved (${String(trimmedSummary.length)} chars).\nNext message will start a fresh session with full context.` + `Session compacted. Summary saved (${String(trimmedSummary.length)} chars).${vaultNote}\nNext message will start a fresh session with full context.` ); } From 008634f866ed8879761ee46a1aa9d3ff7b8f4f62 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 17:02:38 +0300 Subject: [PATCH 14/24] docs: unified memory architecture design spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4-layer memory model: CLAUDE.md (instructions), MEMORY.md (knowledge), Obsidian (session timeline), Beads (issue tracking). Both CLI and Archon share the same files — no more fragmentation. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...4-10-unified-memory-architecture-design.md | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-10-unified-memory-architecture-design.md diff --git a/docs/superpowers/specs/2026-04-10-unified-memory-architecture-design.md b/docs/superpowers/specs/2026-04-10-unified-memory-architecture-design.md new file mode 100644 index 0000000000..1ce91c7010 --- /dev/null +++ b/docs/superpowers/specs/2026-04-10-unified-memory-architecture-design.md @@ -0,0 +1,131 @@ +# Unified Memory Architecture for Archon + +**Date**: 2026-04-10 +**Status**: Approved +**Goal**: Eliminate memory fragmentation between CLI (Claude Code terminal) and Archon (Telegram/Slack/Web) by sharing the same 4-layer memory system. + +## Problem + +Currently 5 separate memory systems exist with no cross-visibility: +- CLI writes to MEMORY.md — Archon can't see it +- Archon writes to `context_summary` in DB — CLI can't see it +- Obsidian session logs partially bridged but inconsistently +- Beads used for both issue tracking and memory (conflated roles) + +Result: switching between CLI and Telegram loses context. + +## Architecture: 4 Layers + +Each layer has a single purpose. Both CLI and Archon read/write the same files. + +### Layer 1: CLAUDE.md (HOW to work) +- **Location**: `{project}/CLAUDE.md` (in repo) +- **Purpose**: Instructions, conventions, rules +- **Already shared**: Both read via `settingSources: [project, user]` +- **Changes needed**: None + +### Layer 2: MEMORY.md + topic files (WHAT we know) +- **Location**: `~/.claude/projects/{encoded-cwd}/memory/` +- **Purpose**: User profile, feedback, project decisions, persistent knowledge +- **Path encoding**: CWD with `/` replaced by `-`, prefixed with `-` + - Example: `/Users/anton/Claude workspace/ai-ofm` → `-Users-anton-Claude-workspace-ai-ofm` + - Full: `~/.claude/projects/-Users-anton-Claude-workspace-ai-ofm/memory/MEMORY.md` +- **CLI**: Auto-loads MEMORY.md index into context (existing behavior) +- **Archon**: Compute path from `conversation.cwd`, auto-inject MEMORY.md index into prompt +- **Writing**: Both can create/update topic files. Agent decides when insights are worth persisting. +- **Changes needed**: + - Add `computeMemoryPath(cwd)` utility + - Read MEMORY.md in `buildFullPrompt()` and inject into prompt + - Remove `context_summary` column usage (keep column for backward compat, stop writing) + +### Layer 3: Obsidian Session Logs (WHAT we did) +- **Location**: `~/Library/Mobile Documents/iCloud~md~obsidian/Documents/Claude/Session-Logs/{project}/` +- **Purpose**: Session timeline, work history, handoff context +- **CLI**: `/compress` writes, `/resume` reads +- **Archon**: `/compact` writes (already implemented), fallback read when MEMORY.md has no relevant context +- **Changes needed**: Keep existing Obsidian save in `/compact`. Remove auto-load from prompt builder (MEMORY.md replaces this). + +### Layer 4: Beads (WHAT we're working on) +- **Location**: `{project}/.beads/` +- **Purpose**: Issue tracking, task context, agent task-specific insights +- **CLI**: `bd list`, `bd show`, `bd close`, `bd remember` +- **Archon**: Same commands via Bash (agents working on tasks use beads for issue context) +- **Changes needed**: None — already accessible via Bash when CLAUDE.md mentions it + +## Implementation Details + +### Path computation + +```typescript +function computeMemoryPath(cwd: string): string { + const encoded = cwd.replace(/\//g, '-'); + const home = process.env.HOME ?? ''; + return `${home}/.claude/projects/${encoded}/memory`; +} +``` + +### Prompt injection + +In `buildFullPrompt()`: +1. If `conversation.cwd` exists → compute memory path +2. If `MEMORY.md` exists at that path → read it (typically 10-50 lines) +3. Inject as `## Project Memory` section in the prompt +4. Agent can read individual topic files via Read tool when needed + +### What gets removed + +- `context_summary` column: stop writing to it. Keep column in schema (no migration needed). +- `loadLatestSessionLog()` from prompt builder: MEMORY.md replaces this as the primary context source. +- Obsidian auto-read in `buildFullPrompt()`: removed. Obsidian remains write-only from `/compact` and readable by the agent on demand. + +### What stays + +- `/compact` command: resets session + writes summary to Obsidian. Does NOT write to MEMORY.md (that's the agent's choice during normal work). +- `/resume` command: shows what context is available (MEMORY.md content). +- Auto-compact on expired session: still works — summarizes from message history, resets session. The new session picks up MEMORY.md context automatically. +- Message persistence: still saves user + assistant messages to DB for auto-compact fallback. + +## Data flow + +``` +User message in Telegram + → orchestrator loads conversation (has cwd) + → computeMemoryPath(cwd) → read MEMORY.md + → buildFullPrompt() includes MEMORY.md index + → agent responds with full project context + → agent can Read topic files, Obsidian logs, bd show as needed + → messages saved to DB (for auto-compact fallback) + +/compact in Telegram + → AI summarizes session + → writes to Obsidian Session-Logs/{project}/ + → resets session + → next message → loads MEMORY.md again (fresh session, same knowledge) + +Agent learns something important + → agent writes/updates topic file in memory path (Write tool) + → updates MEMORY.md index + → knowledge persists for both CLI and Archon +``` + +## Files to modify + +1. `packages/core/src/orchestrator/orchestrator-agent.ts` + - Add `computeMemoryPath()` utility + - Update `buildFullPrompt()`: read MEMORY.md, inject as context + - Remove `loadLatestSessionLog()` from prompt injection (keep function for `/compact` Obsidian write) + - Remove `context_summary` reads from prompt + - Update `/resume` to show MEMORY.md content instead of DB summary + +2. `packages/core/src/orchestrator/prompt-builder.ts` + - No changes (prompt structure built in orchestrator-agent.ts) + +3. No DB migrations needed (keep `context_summary` column, just stop using it) + +## Success criteria + +- Open CLI in ai-ofm → agent knows user profile, feedback, project state from MEMORY.md +- Open Telegram ai-ofm topic → agent knows the SAME information +- Work in CLI → `/compress` → switch to Telegram → agent sees session log +- Work in Telegram → agent updates memory topic file → switch to CLI → CLI sees the update +- Beads issues visible to both via `bd` commands From 0dc618c2b774ba387b1bfb93214c2cb0af414432 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 17:04:46 +0300 Subject: [PATCH 15/24] docs: unified memory architecture implementation plan 6 tasks: add computeMemoryPath, inject MEMORY.md into prompt, update /resume, stop writing context_summary, update docs, verify. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-04-10-unified-memory-architecture.md | 371 ++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-10-unified-memory-architecture.md diff --git a/docs/superpowers/plans/2026-04-10-unified-memory-architecture.md b/docs/superpowers/plans/2026-04-10-unified-memory-architecture.md new file mode 100644 index 0000000000..3e45ba37a9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-10-unified-memory-architecture.md @@ -0,0 +1,371 @@ +# Unified Memory Architecture Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Give Archon agents access to the same MEMORY.md + topic files that CLI uses, eliminating memory fragmentation. + +**Architecture:** Add `computeMemoryPath(cwd)` to derive the CLI memory directory from the conversation's working directory. Inject MEMORY.md index into every prompt. Remove `context_summary` DB reads and Obsidian auto-load from prompt builder — MEMORY.md replaces both. + +**Tech Stack:** TypeScript, Node.js fs/promises, existing orchestrator-agent.ts + +--- + +### Task 1: Add `computeMemoryPath()` and `loadMemoryIndex()` + +**Files:** +- Modify: `packages/core/src/orchestrator/orchestrator-agent.ts` (Shared Memory section, ~line 1371) + +- [ ] **Step 1: Add `computeMemoryPath` function after the existing `VAULT_SESSION_LOGS` constant** + +```typescript +// ─── Project Memory (MEMORY.md — shared with CLI) ────────────────────────── + +/** + * Compute the path to Claude Code's per-project memory directory. + * CLI encodes the CWD by replacing '/' with '-' as the project folder name. + * Example: /Users/anton/Claude workspace/ai-ofm + * → ~/.claude/projects/-Users-anton-Claude-workspace-ai-ofm/memory/ + */ +function computeMemoryPath(cwd: string): string { + const encoded = cwd.replace(/\//g, '-'); + const home = process.env.HOME ?? ''; + return join(home, '.claude', 'projects', encoded, 'memory'); +} + +/** + * Load MEMORY.md index from the CLI memory directory for a project. + * Returns the file content (typically 10-50 lines) or null if not found. + * This is the same file Claude Code CLI auto-loads — sharing it gives + * Archon agents identical project knowledge. + */ +async function loadMemoryIndex(cwd: string): Promise { + try { + const memoryDir = computeMemoryPath(cwd); + const indexPath = join(memoryDir, 'MEMORY.md'); + if (!existsSync(indexPath)) return null; + + const content = await readFile(indexPath, 'utf-8'); + if (!content.trim()) return null; + + getLog().debug({ cwd, memoryDir }, 'memory.index_loaded'); + return content.trim(); + } catch (error) { + getLog().warn({ err: error as Error, cwd }, 'memory.index_load_failed'); + return null; + } +} +``` + +- [ ] **Step 2: Verify imports exist at the top of the file** + +Confirm these imports are present (they were added in a previous commit): +```typescript +import { writeFile, readFile, readdir, mkdir } from 'fs/promises'; +import { join } from 'path'; +``` + +If `readFile` is missing from the import, add it. + +- [ ] **Step 3: Commit** + +```bash +git add packages/core/src/orchestrator/orchestrator-agent.ts +git commit -m "feat: add computeMemoryPath and loadMemoryIndex utilities + +Computes the Claude Code CLI memory directory path from a project's +CWD and loads the MEMORY.md index for prompt injection." +``` + +--- + +### Task 2: Replace `context_summary` / Obsidian auto-load with MEMORY.md in `buildFullPrompt()` + +**Files:** +- Modify: `packages/core/src/orchestrator/orchestrator-agent.ts` (`buildFullPrompt()` function, ~line 450-505) + +- [ ] **Step 1: Replace the context loading block in `buildFullPrompt()`** + +Find this block (approximately lines 467-483): +```typescript + // Load context: prefer DB summary, fall back to latest Obsidian session log + let contextContent = conversation.context_summary; + if (!contextContent && conversation.codebase_id) { + const codebase = codebases.find(c => c.id === conversation.codebase_id); + if (codebase) { + contextContent = await loadLatestSessionLog(getProjectSlug(codebase)); + } + } + + const summarySuffix = contextContent + ? '\n\n---\n\n## Previous Session Context\n\nThe following is context from a prior session (shared across CLI and Telegram). Use it to maintain continuity.\nFor more history, check Obsidian vault at `Claude/Session-Logs/` using Obsidian MCP tools.\n\n' + + contextContent + : ''; +``` + +Replace with: +```typescript + // Load project memory (MEMORY.md) — shared with CLI Claude Code + let memoryContent: string | null = null; + if (conversation.cwd) { + memoryContent = await loadMemoryIndex(conversation.cwd); + } + + const memorySuffix = memoryContent + ? '\n\n---\n\n## Project Memory\n\nLoaded from MEMORY.md (shared with CLI). Topic files can be read on demand via the Read tool at: `' + + computeMemoryPath(conversation.cwd ?? '') + '/`\n' + + 'For session history, check Obsidian vault at `Claude/Session-Logs/` via Obsidian MCP or filesystem.\n\n' + + memoryContent + : ''; +``` + +- [ ] **Step 2: Update both return statements to use `memorySuffix` instead of `summarySuffix`** + +Replace the two returns (thread context and non-thread): +```typescript + if (threadContext) { + return ( + systemPrompt + + memorySuffix + + '\n\n---\n\n## Thread Context (previous messages)\n\n' + + threadContext + + '\n\n---\n\n## Current Request\n\n' + + message + + contextSuffix + + fileSuffix + ); + } + + return systemPrompt + memorySuffix + '\n\n---\n\n## User Message\n\n' + message + contextSuffix + fileSuffix; +``` + +- [ ] **Step 3: Run type-check** + +```bash +bun run type-check +``` +Expected: all packages exit 0. + +- [ ] **Step 4: Commit** + +```bash +git add packages/core/src/orchestrator/orchestrator-agent.ts +git commit -m "feat: inject MEMORY.md into prompt instead of context_summary + +Replaces DB context_summary and Obsidian auto-load with MEMORY.md +index — the same file CLI uses. Both interfaces now share one +source of project knowledge." +``` + +--- + +### Task 3: Update `/resume` to show MEMORY.md content + +**Files:** +- Modify: `packages/core/src/orchestrator/orchestrator-agent.ts` (`handleResume()` function, ~line 1585) + +- [ ] **Step 1: Replace the `handleResume` function** + +Find: +```typescript +async function handleResume( + platform: IPlatformAdapter, + conversationId: string, + conversation: Conversation +): Promise { + if (!conversation.context_summary) { + await platform.sendMessage( + conversationId, + 'No saved context. Use `/compact` first to save a conversation summary.' + ); + return; + } + + const preview = + conversation.context_summary.length > 500 + ? conversation.context_summary.slice(0, 500) + '...' + : conversation.context_summary; + + await platform.sendMessage( + conversationId, + `**Saved context** (${String(conversation.context_summary.length)} chars):\n\n${preview}\n\n_This context is automatically loaded into every new message._` + ); +} +``` + +Replace with: +```typescript +async function handleResume( + platform: IPlatformAdapter, + conversationId: string, + conversation: Conversation +): Promise { + // Show MEMORY.md content (shared with CLI) + const memoryContent = conversation.cwd ? await loadMemoryIndex(conversation.cwd) : null; + + if (!memoryContent) { + await platform.sendMessage( + conversationId, + 'No project memory found. Memory is shared with CLI — work in either interface to build it up.' + ); + return; + } + + const preview = memoryContent.length > 1000 + ? memoryContent.slice(0, 1000) + '\n...(truncated)' + : memoryContent; + + const memoryPath = computeMemoryPath(conversation.cwd ?? ''); + await platform.sendMessage( + conversationId, + `**Project Memory** (${String(memoryContent.length)} chars, shared with CLI):\n\n${preview}\n\nPath: \`${memoryPath}/\`` + ); +} +``` + +- [ ] **Step 2: Run type-check** + +```bash +bun run type-check +``` +Expected: all packages exit 0. + +- [ ] **Step 3: Commit** + +```bash +git add packages/core/src/orchestrator/orchestrator-agent.ts +git commit -m "feat: /resume shows MEMORY.md content instead of DB summary + +Shows the shared project memory that both CLI and Archon use, +with path to the memory directory for transparency." +``` + +--- + +### Task 4: Stop writing `context_summary` to DB in auto-compact + +**Files:** +- Modify: `packages/core/src/orchestrator/orchestrator-agent.ts` (auto-compact catch block, ~line 845 and handleCompact, ~line 1530) + +- [ ] **Step 1: Remove `context_summary` write from auto-compact catch block** + +Find in the catch block (~line 862): +```typescript + if (summary.trim()) { + await db.updateConversationSummary(conversation.id, summary.trim()); + } +``` + +Replace with: +```typescript + // Summary is written to Obsidian by /compact, not to DB. + // Auto-compact only resets the session — MEMORY.md provides context for the next session. +``` + +- [ ] **Step 2: Remove `context_summary` write from `handleCompact`** + +Find in `handleCompact` (~line 1540): +```typescript + await db.updateConversationSummary(conversation.id, trimmedSummary); +``` + +Remove this line. The `/compact` already saves to Obsidian — no need for DB cache. + +- [ ] **Step 3: Run type-check** + +```bash +bun run type-check +``` +Expected: all packages exit 0. + +- [ ] **Step 4: Commit** + +```bash +git add packages/core/src/orchestrator/orchestrator-agent.ts +git commit -m "refactor: stop writing context_summary to DB + +MEMORY.md is now the primary context source. Obsidian session logs +remain as supplementary history. DB context_summary column kept +for backward compatibility but no longer written." +``` + +--- + +### Task 5: Update `/help` text and orchestrator rules doc + +**Files:** +- Modify: `packages/core/src/handlers/command-handler.ts` (help text) +- Modify: `.claude/rules/orchestrator.md` + +- [ ] **Step 1: Update `/resume` description in help text** + +In `command-handler.ts`, find: +```typescript +- \`/resume\` — Show saved context summary +``` + +Replace with: +```typescript +- \`/resume\` — Show project memory (shared with CLI) +``` + +- [ ] **Step 2: Update orchestrator.md `/resume` description** + +In `.claude/rules/orchestrator.md`, find the table row for `/resume`: +``` +| `/resume` | Handled inline — shows stored context summary | +``` + +Replace with: +``` +| `/resume` | Handled inline — shows MEMORY.md content (shared with CLI) | +``` + +- [ ] **Step 3: Commit** + +```bash +git add packages/core/src/handlers/command-handler.ts .claude/rules/orchestrator.md +git commit -m "docs: update /resume description to reflect shared memory" +``` + +--- + +### Task 6: Verify end-to-end + push + +- [ ] **Step 1: Run full validation** + +```bash +bun run validate +``` +Expected: type-check, lint, format, tests all pass. + +- [ ] **Step 2: Manual verification** + +Check that `computeMemoryPath` produces correct paths: +```bash +# In node/bun REPL: +const cwd = '/Users/anton/Claude workspace/ai-ofm'; +const encoded = cwd.replace(/\//g, '-'); +console.log(`${process.env.HOME}/.claude/projects/${encoded}/memory`); +# Expected: /Users/anton/.claude/projects/-Users-anton-Claude-workspace-ai-ofm/memory +``` + +Verify MEMORY.md exists at that path: +```bash +ls ~/.claude/projects/-Users-anton-Claude-workspace-ai-ofm/memory/MEMORY.md +``` + +- [ ] **Step 3: Push all changes** + +```bash +git push +``` + +- [ ] **Step 4: Restart server and test in Telegram** + +```bash +pkill -f "bun.*watch.*index.ts"; sleep 1 +cd packages/server && bun --watch src/index.ts &>/tmp/archon-dev.log & +``` + +Send a message in the ai-ofm Telegram topic. Check logs for `memory.index_loaded`. +Send `/resume` — should show MEMORY.md content. From b743e1556ef03676cd78f047ba4b2837735f6880 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 17:07:33 +0300 Subject: [PATCH 16/24] feat: add computeMemoryPath and loadMemoryIndex utilities Computes the Claude Code CLI memory directory path from a project's CWD and loads the MEMORY.md index for prompt injection. Co-Authored-By: Claude Sonnet 4.6 --- .../src/orchestrator/orchestrator-agent.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index fbfb5801ba..5ea6c06ffa 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -1464,6 +1464,43 @@ async function loadLatestSessionLog(projectSlug: string): Promise } } +// ─── Project Memory (MEMORY.md — shared with CLI) ────────────────────────── + +/** + * Compute the path to Claude Code's per-project memory directory. + * CLI encodes the CWD by replacing '/' with '-' as the project folder name. + * Example: /Users/anton/Claude workspace/ai-ofm + * → ~/.claude/projects/-Users-anton-Claude-workspace-ai-ofm/memory/ + */ +export function computeMemoryPath(cwd: string): string { + const encoded = cwd.replace(/\//g, '-'); + const home = process.env.HOME ?? ''; + return join(home, '.claude', 'projects', encoded, 'memory'); +} + +/** + * Load MEMORY.md index from the CLI memory directory for a project. + * Returns the file content (typically 10-50 lines) or null if not found. + * This is the same file Claude Code CLI auto-loads — sharing it gives + * Archon agents identical project knowledge. + */ +export async function loadMemoryIndex(cwd: string): Promise { + try { + const memoryDir = computeMemoryPath(cwd); + const indexPath = join(memoryDir, 'MEMORY.md'); + if (!existsSync(indexPath)) return null; + + const content = await readFile(indexPath, 'utf-8'); + if (!content.trim()) return null; + + getLog().debug({ cwd, memoryDir }, 'memory.index_loaded'); + return content.trim(); + } catch (error) { + getLog().warn({ err: error as Error, cwd }, 'memory.index_load_failed'); + return null; + } +} + /** * Persist user + assistant messages to the database. * Fire-and-forget — errors are logged but never thrown. From 2331966a6c9b2dfb99b98fd76863402bbb44fb2a Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 17:11:11 +0300 Subject: [PATCH 17/24] feat: replace context_summary with MEMORY.md in prompt injection - buildFullPrompt() now reads MEMORY.md (shared with CLI) instead of DB context_summary or Obsidian auto-load - /resume shows MEMORY.md content and path - Stop writing context_summary to DB (column kept for compat) --- .../src/orchestrator/orchestrator-agent.ts | 95 +++++-------------- 1 file changed, 25 insertions(+), 70 deletions(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 5ea6c06ffa..fec1a02f10 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -7,7 +7,7 @@ * - Does NOT require a project to be selected before starting a conversation */ import { existsSync } from 'fs'; -import { writeFile, readFile, readdir, mkdir } from 'fs/promises'; +import { writeFile, readFile, mkdir } from 'fs/promises'; import { join } from 'path'; import { createLogger } from '@archon/paths'; import type { @@ -464,18 +464,17 @@ async function buildFullPrompt( ? buildProjectScopedPrompt(scopedCodebase, codebases, workflows) : buildOrchestratorPrompt(codebases, workflows); - // Load context: prefer DB summary, fall back to latest Obsidian session log - let contextContent = conversation.context_summary; - if (!contextContent && conversation.codebase_id) { - const codebase = codebases.find(c => c.id === conversation.codebase_id); - if (codebase) { - contextContent = await loadLatestSessionLog(getProjectSlug(codebase)); - } + // Load project memory (MEMORY.md) — shared with CLI Claude Code + let memoryContent: string | null = null; + if (conversation.cwd) { + memoryContent = await loadMemoryIndex(conversation.cwd); } - const summarySuffix = contextContent - ? '\n\n---\n\n## Previous Session Context\n\nThe following is context from a prior session (shared across CLI and Telegram). Use it to maintain continuity.\nFor more history, check Obsidian vault at `Claude/Session-Logs/` using Obsidian MCP tools.\n\n' + - contextContent + const memorySuffix = memoryContent + ? '\n\n---\n\n## Project Memory\n\nLoaded from MEMORY.md (shared with CLI). Topic files can be read on demand via the Read tool at: `' + + computeMemoryPath(conversation.cwd ?? '') + '/`\n' + + 'For session history, check Obsidian vault at `Claude/Session-Logs/` via Obsidian MCP or filesystem.\n\n' + + memoryContent : ''; const contextSuffix = issueContext ? '\n\n---\n\n## Additional Context\n\n' + issueContext : ''; @@ -491,7 +490,7 @@ async function buildFullPrompt( if (threadContext) { return ( systemPrompt + - summarySuffix + + memorySuffix + '\n\n---\n\n## Thread Context (previous messages)\n\n' + threadContext + '\n\n---\n\n## Current Request\n\n' + @@ -501,7 +500,7 @@ async function buildFullPrompt( ); } - return systemPrompt + summarySuffix + '\n\n---\n\n## User Message\n\n' + message + contextSuffix + fileSuffix; + return systemPrompt + memorySuffix + '\n\n---\n\n## User Message\n\n' + message + contextSuffix + fileSuffix; } // ─── Main Handler ─────────────────────────────────────────────────────────── @@ -870,9 +869,7 @@ export async function handleMessage( if (chunk.type === 'assistant') summary += chunk.content; } - if (summary.trim()) { - await db.updateConversationSummary(conversation.id, summary.trim()); - } + // context_summary writes removed — MEMORY.md is the shared memory now } // Reset the expired session @@ -1419,51 +1416,6 @@ async function saveSessionLogToVault( } } -/** Max session logs to load for context (most recent first) */ -const MAX_SESSION_LOGS = 3; -/** Max chars per session log to avoid blowing up the prompt */ -const MAX_LOG_CHARS = 2000; - -/** - * Load recent session logs from Obsidian vault for a project. - * Used when a new session has no context_summary — pulls shared memory - * that may have been written by CLI (/compress) or another Archon session. - * Loads up to MAX_SESSION_LOGS most recent logs, newest first. - */ -async function loadLatestSessionLog(projectSlug: string): Promise { - try { - const dir = join(VAULT_SESSION_LOGS, projectSlug); - if (!existsSync(dir)) return null; - - const files = await readdir(dir); - const sorted = files.filter(f => f.endsWith('.md')).sort().reverse(); - if (sorted.length === 0) return null; - - const logs: string[] = []; - for (const file of sorted.slice(0, MAX_SESSION_LOGS)) { - const content = await readFile(join(dir, file), 'utf-8'); - const body = content.replace(/^---[\s\S]*?---\s*/, '').trim(); - if (body) { - const truncated = body.length > MAX_LOG_CHARS - ? body.slice(0, MAX_LOG_CHARS) + '\n...(truncated)' - : body; - logs.push(`### ${file.replace('.md', '')}\n\n${truncated}`); - } - } - - if (logs.length === 0) return null; - - getLog().debug( - { projectSlug, count: logs.length, files: sorted.slice(0, MAX_SESSION_LOGS) }, - 'session.vault_logs_loaded' - ); - return logs.join('\n\n---\n\n'); - } catch (error) { - getLog().warn({ err: error as Error, projectSlug }, 'session.vault_load_failed'); - return null; - } -} - // ─── Project Memory (MEMORY.md — shared with CLI) ────────────────────────── /** @@ -1588,9 +1540,9 @@ async function handleCompact( return; } - // Save summary to DB + Obsidian vault (shared with CLI /compress) + // Save summary to Obsidian vault (shared with CLI /compress) + // context_summary writes removed — MEMORY.md is the shared memory now const trimmedSummary = summary.trim(); - await db.updateConversationSummary(conversation.id, trimmedSummary); await sessionDb.deactivateSession(session.id, 'reset-requested'); let vaultPath: string | null = null; @@ -1626,22 +1578,25 @@ async function handleResume( conversationId: string, conversation: Conversation ): Promise { - if (!conversation.context_summary) { + // Show MEMORY.md content (shared with CLI) + const memoryContent = conversation.cwd ? await loadMemoryIndex(conversation.cwd) : null; + + if (!memoryContent) { await platform.sendMessage( conversationId, - 'No saved context. Use `/compact` first to save a conversation summary.' + 'No project memory found. Memory is shared with CLI — work in either interface to build it up.' ); return; } - const preview = - conversation.context_summary.length > 500 - ? conversation.context_summary.slice(0, 500) + '...' - : conversation.context_summary; + const preview = memoryContent.length > 1000 + ? memoryContent.slice(0, 1000) + '\n...(truncated)' + : memoryContent; + const memoryPath = computeMemoryPath(conversation.cwd ?? ''); await platform.sendMessage( conversationId, - `**Saved context** (${String(conversation.context_summary.length)} chars):\n\n${preview}\n\n_This context is automatically loaded into every new message._` + `**Project Memory** (${String(memoryContent.length)} chars, shared with CLI):\n\n${preview}\n\nPath: \`${memoryPath}/\`` ); } From db65e0ae470dde2c04ea17204a3db35112f137d8 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 17:12:41 +0300 Subject: [PATCH 18/24] docs: update /resume description to reflect shared memory Co-Authored-By: Claude Sonnet 4.6 --- .claude/rules/orchestrator.md | 2 +- packages/core/src/handlers/command-handler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/rules/orchestrator.md b/.claude/rules/orchestrator.md index 9cb2605a4f..0f503a9fed 100644 --- a/.claude/rules/orchestrator.md +++ b/.claude/rules/orchestrator.md @@ -37,7 +37,7 @@ Only **13 commands** are handled deterministically: | `/status` | Show conversation/session state | | `/reset` | Deactivate current session | | `/compact` | Handled inline — AI summarizes session, saves summary, resets session | -| `/resume` | Handled inline — shows stored context summary | +| `/resume` | Handled inline — shows MEMORY.md content (shared with CLI) | | `/workflow` | Subcommands: `list`, `run`, `status`, `cancel`, `reload` | | `/register-project` | Handled inline — creates codebase DB record | | `/update-project` | Handled inline — updates codebase path | diff --git a/packages/core/src/handlers/command-handler.ts b/packages/core/src/handlers/command-handler.ts index 35ba402e63..03196b72f3 100644 --- a/packages/core/src/handlers/command-handler.ts +++ b/packages/core/src/handlers/command-handler.ts @@ -927,7 +927,7 @@ Talk naturally — the orchestrator routes your requests to the right workflow a **Session** - \`/status\` — Show current session and project info - \`/compact\` — Summarize and compress the session (frees context window) -- \`/resume\` — Show saved context summary +- \`/resume\` — Show project memory (shared with CLI) - \`/reset\` — Clear conversation and start fresh - \`/help\` — Show this help message From 3d9187cc051a258f4ae5309199e12014fd5de2f9 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 17:14:51 +0300 Subject: [PATCH 19/24] fix: encode spaces in memory path + remove dead auto-compact code computeMemoryPath now replaces both '/' and spaces with '-' to match Claude Code CLI's actual encoding. Removed unused summary generation from auto-compact (MEMORY.md provides context, no DB write needed). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/orchestrator/orchestrator-agent.ts | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index fec1a02f10..9d1f6dd5d1 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -851,26 +851,7 @@ export async function handleMessage( if (conversation && isSessionExpired) { getLog().info({ conversationId }, 'session.expired_auto_compacting'); try { - const messages = await messageDb.listMessages(conversation.id, 50); - if (messages.length > 0) { - const transcript = messages - .map(m => `[${m.role}]: ${m.content.slice(0, 500)}`) - .join('\n\n'); - - const aiClient = getAssistantClient(conversation.ai_assistant_type); - const summaryCwd = conversation.cwd ?? getArchonWorkspacesPath(); - let summary = ''; - for await (const chunk of aiClient.sendQuery( - `Summarize this conversation transcript concisely. Include: key decisions, current state, important context, pending items. Output ONLY the summary.\n\n---\n\n${transcript}`, - summaryCwd, - undefined, - { tools: [] } - )) { - if (chunk.type === 'assistant') summary += chunk.content; - } - - // context_summary writes removed — MEMORY.md is the shared memory now - } + // No summary generation needed — MEMORY.md provides context for the new session // Reset the expired session const expiredSession = await sessionDb.getActiveSession(conversation.id); @@ -1420,12 +1401,12 @@ async function saveSessionLogToVault( /** * Compute the path to Claude Code's per-project memory directory. - * CLI encodes the CWD by replacing '/' with '-' as the project folder name. + * CLI encodes the CWD by replacing '/' and spaces with '-' as the project folder name. * Example: /Users/anton/Claude workspace/ai-ofm * → ~/.claude/projects/-Users-anton-Claude-workspace-ai-ofm/memory/ */ export function computeMemoryPath(cwd: string): string { - const encoded = cwd.replace(/\//g, '-'); + const encoded = cwd.replace(/[/ ]/g, '-'); const home = process.env.HOME ?? ''; return join(home, '.claude', 'projects', encoded, 'memory'); } From 7a202145123b606bc11c927c3e87ab96023bf0b1 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 17:26:42 +0300 Subject: [PATCH 20/24] feat: save session log to Obsidian on /reset and auto-compact Extracts saveSessionToObsidian() helper that generates a summary from message history and writes to Claude/Session-Logs/{project}/. Called by /reset (before clearing session) and auto-compact (on expired session detection). Ensures no session context is lost. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/orchestrator/orchestrator-agent.ts | 81 ++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 9d1f6dd5d1..851572e3f0 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -710,6 +710,12 @@ export async function handleMessage( return; } + if (command === 'reset') { + getLog().debug({ command, conversationId }, 'deterministic_command'); + await handleResetWithSessionLog(platform, conversationId, conversation); + return; + } + if (command === 'compact') { getLog().debug({ command, conversationId }, 'deterministic_command'); await handleCompact(platform, conversationId, conversation); @@ -851,7 +857,8 @@ export async function handleMessage( if (conversation && isSessionExpired) { getLog().info({ conversationId }, 'session.expired_auto_compacting'); try { - // No summary generation needed — MEMORY.md provides context for the new session + // Save session log to Obsidian before resetting + await saveSessionToObsidian(conversation); // Reset the expired session const expiredSession = await sessionDb.getActiveSession(conversation.id); @@ -1456,6 +1463,78 @@ async function persistConversationMessages( } } +/** + * Generate a summary from saved messages and write a session log to Obsidian. + * Used by /reset, /compact, and auto-compact to preserve session history. + * Returns the vault path on success, null if no messages or on failure. + */ +async function saveSessionToObsidian(conversation: Conversation): Promise { + const messages = await messageDb.listMessages(conversation.id, 50); + if (messages.length === 0) return null; + + const codebase = conversation.codebase_id + ? await codebaseDb.getCodebase(conversation.codebase_id) + : null; + if (!codebase) return null; + + const transcript = messages + .map(m => `[${m.role}]: ${m.content.slice(0, 500)}`) + .join('\n\n'); + + const aiClient = getAssistantClient(conversation.ai_assistant_type); + const cwd = conversation.cwd ?? getArchonWorkspacesPath(); + let summary = ''; + + try { + for await (const chunk of aiClient.sendQuery( + `Summarize this conversation transcript concisely. Include: key decisions, current state of work, important context, and pending items. Output ONLY the summary, no preamble.\n\n---\n\n${transcript}`, + cwd, + undefined, + { tools: [] } + )) { + if (chunk.type === 'assistant') summary += chunk.content; + } + } catch (error) { + getLog().warn({ err: error as Error }, 'session.summary_generation_failed'); + return null; + } + + if (!summary.trim()) return null; + + return saveSessionLogToVault( + getProjectSlug(codebase), + summary.trim(), + conversation.platform_type, + conversation.title + ); +} + +/** + * Handle /reset with session log preservation. + * Saves conversation history to Obsidian before resetting the session. + */ +async function handleResetWithSessionLog( + platform: IPlatformAdapter, + conversationId: string, + conversation: Conversation +): Promise { + const session = await sessionDb.getActiveSession(conversation.id); + if (!session) { + await platform.sendMessage(conversationId, 'No active session to reset.'); + return; + } + + // Save session log to Obsidian before resetting + const vaultPath = await saveSessionToObsidian(conversation); + await sessionDb.deactivateSession(session.id, 'reset-requested'); + + const logNote = vaultPath ? `\nSession log saved to Obsidian: ${vaultPath}` : ''; + await platform.sendMessage( + conversationId, + `Session cleared. Starting fresh on next message.${logNote}\n\nProject and memory preserved.` + ); +} + /** * Handle /compact command. * Summarizes the current conversation via AI, saves the summary, and resets the session. From d37896f16781707b2abb235dddbfa9a0a682cd20 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 10 Apr 2026 17:33:46 +0300 Subject: [PATCH 21/24] =?UTF-8?q?fix:=20critical=20review=20fixes=20?= =?UTF-8?q?=E2=80=94=20path=20encoding,=20recursion=20guard,=20persist=20f?= =?UTF-8?q?ilter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. computeMemoryPath: encode dots too (fixes .archon paths) 2. Auto-compact: recursion depth guard (max 1 retry) 3. persistConversationMessages: regex match anywhere, not startsWith 4. isSessionExpired: add parentheses for operator precedence 5. handleSetProject: rename param to conversationDbId for clarity Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/orchestrator/orchestrator-agent.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 851572e3f0..d902c1da8a 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -853,7 +853,7 @@ export async function handleMessage( const isSessionExpired = err.message.includes('No conversation found with session ID') || err.message.includes('not a valid UUID') || - err.message.includes('session') && err.message.includes('not found'); + (err.message.includes('session') && err.message.includes('not found')); if (conversation && isSessionExpired) { getLog().info({ conversationId }, 'session.expired_auto_compacting'); try { @@ -871,9 +871,14 @@ export async function handleMessage( 'Session expired — context saved automatically. Retrying your message...' ); - // Retry the message (recursive call via handleMessage will create a fresh session) - await handleMessage(platform, conversationId, message, context); - return; + // Retry once (guard against infinite recursion) + const retryDepth = ((context as Record | undefined)?._retryDepth as number | undefined) ?? 0; + if (retryDepth > 0) { + getLog().error({ conversationId, retryDepth }, 'session.auto_compact_retry_limit'); + } else { + await handleMessage(platform, conversationId, message, { ...context, _retryDepth: retryDepth + 1 } as HandleMessageContext); + return; + } } catch (compactError) { getLog().error( { err: toError(compactError), conversationId }, @@ -1413,7 +1418,7 @@ async function saveSessionLogToVault( * → ~/.claude/projects/-Users-anton-Claude-workspace-ai-ofm/memory/ */ export function computeMemoryPath(cwd: string): string { - const encoded = cwd.replace(/[/ ]/g, '-'); + const encoded = cwd.replace(/[/. ]/g, '-'); const home = process.env.HOME ?? ''; return join(home, '.claude', 'projects', encoded, 'memory'); } @@ -1452,8 +1457,8 @@ async function persistConversationMessages( assistantResponse: string ): Promise { try { - // Skip internal orchestrator commands (they're not useful context) - if (assistantResponse.startsWith('/invoke-workflow') || assistantResponse.startsWith('/register-project')) { + // Skip responses containing orchestrator commands (not user-facing content) + if (/^\/invoke-workflow\s/m.test(assistantResponse) || /^\/register-project\s/m.test(assistantResponse)) { return; } await messageDb.addMessage(conversationDbId, 'user', userMessage); @@ -1665,7 +1670,7 @@ async function handleResume( * Binds a registered codebase to the current conversation so all subsequent * messages route to that project automatically. */ -async function handleSetProject(message: string, conversationId: string): Promise { +async function handleSetProject(message: string, conversationDbId: string): Promise { const { args } = commandHandler.parseCommand(message); if (args.length < 1) { return 'Usage: /setproject '; @@ -1683,13 +1688,13 @@ async function handleSetProject(message: string, conversationId: string): Promis } // Update conversation record - await db.updateConversation(conversationId, { + await db.updateConversation(conversationDbId, { codebase_id: codebase.id, cwd: codebase.default_cwd, }); getLog().info( - { conversationId, projectName: codebase.name, codebaseId: codebase.id }, + { conversationDbId, projectName: codebase.name, codebaseId: codebase.id }, 'project.setproject_completed' ); return `Project set to **${codebase.name}**\nWorking directory: ${codebase.default_cwd}`; From 10626206f80b87942eeb3bd650f168f9af36d19d Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 13 Apr 2026 14:27:15 +0300 Subject: [PATCH 22/24] docs: slim CLAUDE.md by extracting duplicated sections to rule files CLAUDE.md exceeded 40k char performance threshold (41,747 chars). Moved 7 duplicated sections to context-dependent .claude/rules/ files that auto-load when relevant file paths are touched. Created .claude/rules/logging.md for Pino conventions. Result: 28,036 chars (~33% reduction, well under 40k limit). Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/rules/logging.md | 54 +++++++ CLAUDE.md | 298 +++------------------------------------ 2 files changed, 72 insertions(+), 280 deletions(-) create mode 100644 .claude/rules/logging.md diff --git a/.claude/rules/logging.md b/.claude/rules/logging.md new file mode 100644 index 0000000000..2df913b98c --- /dev/null +++ b/.claude/rules/logging.md @@ -0,0 +1,54 @@ +--- +paths: + - "packages/*/src/**/*.ts" +--- + +# Logging Conventions + +## Structured Logging with Pino + +```typescript +import { createLogger } from '@archon/paths'; + +const log = createLogger('orchestrator'); + +// Event naming: {domain}.{action}_{state} +// Standard states: _started, _completed, _failed, _validated, _rejected +async function createSession(conversationId: string, codebaseId: string) { + log.info({ conversationId, codebaseId }, 'session.create_started'); + + try { + const session = await doCreate(); + log.info({ conversationId, codebaseId, sessionId: session.id }, 'session.create_completed'); + return session; + } catch (e) { + const err = e as Error; + log.error( + { conversationId, error: err.message, errorType: err.constructor.name, err }, + 'session.create_failed', + ); + throw err; + } +} +``` + +## Event Naming Rules + +- Format: `{domain}.{action}_{state}` — e.g. `workflow.step_started`, `isolation.create_failed` +- Avoid generic events like `processing` or `handling` +- Always pair `_started` with `_completed` or `_failed` +- Include context: IDs, durations, error details + +## Log Levels + +`fatal` > `error` > `warn` > `info` (default) > `debug` > `trace` + +## Verbosity + +- CLI: `archon --quiet` (errors only) — suppresses Pino logs and workflow progress output +- CLI: `archon --verbose` (debug) — enables debug Pino logs and tool-level workflow progress events +- Server: `LOG_LEVEL=debug bun run start` + +## Never Log + +API keys or tokens (mask: `token.slice(0, 8) + '...'`), user message content, PII. diff --git a/CLAUDE.md b/CLAUDE.md index 9656835098..c0de1b9d6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,79 +174,9 @@ psql $DATABASE_URL < migrations/000_combined.sql ### CLI (Command Line) -Run workflows directly from the command line without needing the server. Workflow and isolation commands require running from within a git repository (subdirectories work - resolves to repo root). +> Full CLI reference: `.claude/rules/cli.md` -```bash -# List available workflows (requires git repo) -bun run cli workflow list - -# Machine-readable JSON output -bun run cli workflow list --json - -# Run a workflow -bun run cli workflow run assist "What does the orchestrator do?" - -# Run in a specific directory -bun run cli workflow run plan --cwd /path/to/repo "Add dark mode" - -# Default: auto-creates worktree with generated branch name (isolation by default) -bun run cli workflow run implement "Add auth" - -# Explicit branch name for the worktree -bun run cli workflow run implement --branch feature-auth "Add auth" - -# Opt out of isolation (run in live checkout) -bun run cli workflow run quick-fix --no-worktree "Fix typo" - -# Grant env-leak-gate consent during auto-registration (for repos whose .env -# contains sensitive keys). Audit-logged with actor: 'user-cli'. -bun run cli workflow run plan --cwd /path/to/leaky/repo --allow-env-keys "..." - -# Show running workflows -bun run cli workflow status - -# Resume a failed workflow (re-runs, skipping completed nodes) -bun run cli workflow resume - -# Discard a non-terminal run -bun run cli workflow abandon - -# Delete old workflow run records (default: 7 days) -bun run cli workflow cleanup -bun run cli workflow cleanup 30 # Custom days - -# Emit a workflow event (used inside workflow loop prompts) -bun run cli workflow event emit --run-id --type [--data ] - -# List active worktrees/environments -bun run cli isolation list - -# Clean up stale environments (default: 7 days) -bun run cli isolation cleanup -bun run cli isolation cleanup 14 # Custom days - -# Clean up environments with branches merged into main (also deletes remote branches) -bun run cli isolation cleanup --merged - -# Also remove environments with closed (abandoned) PRs -bun run cli isolation cleanup --merged --include-closed - -# Validate workflow definitions and their referenced resources -bun run cli validate workflows # All workflows -bun run cli validate workflows my-workflow # Single workflow -bun run cli validate workflows my-workflow --json # Machine-readable output - -# Validate command files -bun run cli validate commands # All commands -bun run cli validate commands my-command # Single command - -# Complete branch lifecycle (remove worktree + local/remote branches) -bun run cli complete -bun run cli complete --force # Skip uncommitted-changes check - -# Show version -bun run cli version -``` +Key commands: `bun run cli workflow list`, `bun run cli workflow run [message]`, `bun run cli isolation list`, `bun run cli complete `. ## Architecture @@ -482,41 +412,9 @@ assistants: ### Running the App in Worktrees -Agents working in worktrees can run the app for self-testing (make changes → run app → test via curl → fix). Ports are automatically allocated to avoid conflicts: - -```bash -# Run in worktree (port auto-allocated based on path) -bun dev & -# [Hono] Worktree detected (/path/to/worktree) -# [Hono] Auto-allocated port: 3637 (base: 3090, offset: +547) - -# Test via web API (production path) -# 1) Create a conversation -curl -X POST http://localhost:3637/api/conversations \ - -H "Content-Type: application/json" \ - -d '{}' - -# 2) Send a message -curl -X POST http://localhost:3637/api/conversations//message \ - -H "Content-Type: application/json" \ - -d '{"message":"/status"}' - -# 3) Fetch messages (polling) -curl http://localhost:3637/api/conversations//messages - -# Note: SSE streaming is available at /api/stream/ -``` - -**Port Allocation:** -- Worktrees: Automatic unique port (3190-4089 range, hash-based on path) -- Main repo: Default 3090 -- Override: `PORT=4000 bun dev` (works in both contexts) -- Same worktree always gets same port (deterministic) +> Port allocation and worktree dev details: `.claude/rules/server-api.md` and `.claude/rules/dx-quirks.md` -**Important:** -- Use the web API routes for manual validation (avoid running multiple platform adapters) -- Database is shared (same conversations/codebases available) -- Kill the server when done: `pkill -f "bun.*dev"` or use the specific port +Worktrees auto-allocate ports (3190-4089 range, deterministic per path). Main repo: 3090. Override: `PORT=4000 bun dev`. ### Archon Directory Structure @@ -588,195 +486,35 @@ This ensures type compatibility with SDK updates and eliminates `as any` casts. ### Testing -**Unit Tests:** -- Test pure functions (variable substitution, command parsing) -- Mock external dependencies (database, AI SDKs, platform APIs) - -**Integration Tests:** -- Test database operations with test database -- Test end-to-end flows (mock platforms/AI but use real orchestrator) -- Clean up test data after each test - -**Mock isolation rules (IMPORTANT):** -- Bun's `mock.module()` is process-global and irreversible — `mock.restore()` does NOT undo it -- Do NOT add `afterAll(() => mock.restore())` for `mock.module()` cleanup — it has no effect -- Use `spyOn()` for internal modules that other test files import directly (e.g., `spyOn(git, 'checkout')`) — `spy.mockRestore()` DOES work for spies -- Never `mock.module()` a module path that another test file also `mock.module()`s with a different implementation -- When adding a new test file with `mock.module()`, ensure its package.json test script runs it in a separate `bun test` invocation from any conflicting files - -**Manual Validation:** Use the web API (`curl`) or CLI commands directly for end-to-end testing of new features. +> Full testing conventions, mock patterns, and batch tables: `.claude/rules/testing.md` ### Logging -**Structured logging with Pino** (`packages/paths/src/logger.ts`): +> Pino patterns, event naming, log levels: `.claude/rules/logging.md` -```typescript -import { createLogger } from '@archon/paths'; - -const log = createLogger('orchestrator'); - -// Event naming: {domain}.{action}_{state} -// Standard states: _started, _completed, _failed, _validated, _rejected -async function createSession(conversationId: string, codebaseId: string) { - log.info({ conversationId, codebaseId }, 'session.create_started'); - - try { - const session = await doCreate(); - log.info({ conversationId, codebaseId, sessionId: session.id }, 'session.create_completed'); - return session; - } catch (e) { - const err = e as Error; - log.error( - { conversationId, error: err.message, errorType: err.constructor.name, err }, - 'session.create_failed', - ); - throw err; - } -} -``` - -**Event naming rules:** -- Format: `{domain}.{action}_{state}` — e.g. `workflow.step_started`, `isolation.create_failed` -- Avoid generic events like `processing` or `handling` -- Always pair `_started` with `_completed` or `_failed` -- Include context: IDs, durations, error details +### Command System -**Log Levels:** `fatal` > `error` > `warn` > `info` (default) > `debug` > `trace` +> Variable substitution table, DAG node types, workflow format: `.claude/rules/workflows.md` -**Verbosity:** -- CLI: `archon --quiet` (errors only) — suppresses Pino logs and workflow progress output -- CLI: `archon --verbose` (debug) — enables debug Pino logs and tool-level workflow progress events -- Server: `LOG_LEVEL=debug bun run start` +**Command Types:** Codebase commands (`.archon/commands/`), Workflows (`.archon/workflows/`, YAML DAG format). -**Never log:** API keys or tokens (mask: `token.slice(0, 8) + '...'`), user message content, PII. +**Defaults:** Bundled in `.archon/commands/defaults/` and `.archon/workflows/defaults/`. Repo overrides defaults by name. Opt-out via `defaults.loadDefaultCommands: false` / `defaults.loadDefaultWorkflows: false` in `.archon/config.yaml`. -### Command System - -**Variable Substitution:** -- `$1`, `$2`, `$3` - Positional arguments -- `$ARGUMENTS` - All arguments as single string -- `$ARTIFACTS_DIR` - External artifacts directory for the current workflow run (pre-created by executor) -- `$WORKFLOW_ID` - The workflow run ID -- `$BASE_BRANCH` - Base branch; auto-detected from git when `worktree.baseBranch` is not set; fails only if referenced in a prompt and auto-detection also fails -- `$DOCS_DIR` - Documentation directory path; configured via `docs.path` in `.archon/config.yaml`. Defaults to `docs/`. Never throws. -- `$LOOP_USER_INPUT` - User feedback provided via `/workflow approve ` at an interactive loop gate. Only populated on the first iteration of a resumed interactive loop; empty string on all other iterations. -- `$REJECTION_REASON` - Reviewer feedback provided via `/workflow reject ` at an approval gate. Only populated in `on_reject` prompts; empty string elsewhere. - -**Command Types:** - -1. **Codebase Commands** (per-repo): - - Stored in `.archon/commands/` (plain text/markdown) - - Auto-detected via `/clone` or `/load-commands ` - - Loaded by `/clone` or `/load-commands`, invoked by AI via orchestrator routing - -2. **Workflows** (YAML-based): - - Stored in `.archon/workflows/` (searched recursively) - - Multi-step AI execution chains, discovered at runtime - - **`nodes:` (DAG format)**: Nodes with explicit `depends_on` edges; independent nodes in the same topological layer run concurrently. Node types: `command:` (named command file), `prompt:` (inline prompt), `bash:` (shell script, stdout captured as `$nodeId.output`, no AI), `loop:` (iterative AI prompt until completion signal), `approval:` (human gate; pauses until user approves or rejects; `capture_response: true` stores the user's comment as `$.output` for downstream nodes, default false), `script:` (inline TypeScript/Python or named script from `.archon/scripts/`, runs via `bun` or `uv`, stdout captured as `$nodeId.output`, no AI, supports `deps:` for dependency installation and `timeout:` in ms, requires `runtime: bun` or `runtime: uv`) . Supports `when:` conditions, `trigger_rule` join semantics, `$nodeId.output` substitution, `output_format` for structured JSON output (Claude and Codex), `allowed_tools`/`denied_tools` for per-node tool restrictions (Claude only), `hooks` for per-node SDK hook callbacks (Claude only), `mcp` for per-node MCP server config files (Claude only, env vars expanded at execution time), and `skills` for per-node skill preloading via AgentDefinition wrapping (Claude only), and `effort`/`thinking`/`maxBudgetUsd`/`systemPrompt`/`fallbackModel`/`betas`/`sandbox` for Claude SDK advanced options (Claude only, also settable at workflow level) - - Provider inherited from `.archon/config.yaml` unless explicitly set; per-node `provider` and `model` overrides supported - - Model and options can be set per workflow or inherited from config defaults - - `interactive: true` at the workflow level forces foreground execution on web (required for approval-gate workflows in the web UI) - - Model validation ensures provider/model compatibility at load time - - Commands: `/workflow list`, `/workflow reload`, `/workflow status`, `/workflow cancel`, `/workflow resume ` (re-runs failed workflow, skipping completed nodes), `/workflow abandon `, `/workflow cleanup [days]` (CLI only — deletes old run records) - - Resilient loading: One broken YAML doesn't abort discovery; errors shown in `/workflow list` - - `resolveWorkflowName()` (in `router.ts`) resolves workflow names via a 4-tier fallback — exact, case-insensitive, suffix (`-name`), substring — with ambiguity detection; used by both the CLI and all chat platforms - - Router fallback: if no `/invoke-workflow` is produced, falls back to `archon-assist` (with "Routing unclear" notice); raw AI response returned only when `archon-assist` is unavailable - - Claude routing calls use `tools: []` to prevent tool use at the API level; Codex tool bypass is detected and triggers the same fallback - -**Defaults:** -- Bundled in `.archon/commands/defaults/` and `.archon/workflows/defaults/` -- Binary builds: Embedded at compile time (no filesystem access needed) -- Source builds: Loaded from filesystem at runtime -- Merged with repo-specific commands/workflows (repo overrides defaults by name) -- Opt-out: Set `defaults.loadDefaultCommands: false` or `defaults.loadDefaultWorkflows: false` in `.archon/config.yaml` - -**Global workflows** (user-level, applies to every project): -- Path: `~/.archon/.archon/workflows/` (or `$ARCHON_HOME/.archon/workflows/`) -- Load priority: bundled < global < repo-specific (repo overrides global by filename) -- See the docs site at `packages/docs-web/` for details +**Global workflows:** `~/.archon/.archon/workflows/`. Priority: bundled < global < repo-specific. ### Error Handling -**Database Errors:** -```typescript -// INSERT operations -try { - await db.query('INSERT INTO conversations ...', params); -} catch (error) { - log.error({ err: error, params }, 'db_insert_failed'); - throw new Error('Failed to create conversation'); -} +> DB error patterns: `.claude/rules/database.md`. Git/isolation errors: `.claude/rules/isolation.md` -// UPDATE operations - verify rowCount to catch missing records -try { - await db.updateConversation(conversationId, { codebase_id: codebaseId }); -} catch (error) { - // updateConversation throws if no rows matched (conversation not found) - log.error({ err: error, conversationId }, 'db_update_failed'); - throw error; // Re-throw to surface the issue -} -``` - -**Git Operation Errors (don't fail silently):** -```typescript -// When isolation environment creation fails: -try { - // ... isolation creation logic ... -} catch (error) { - const err = error as Error; - const userMessage = classifyIsolationError(err); - log.error({ err, codebaseId, codebaseName }, 'isolation_creation_failed'); - await platform.sendMessage(conversationId, userMessage); -} -``` - -Pattern: Use `classifyIsolationError()` (from `@archon/isolation`) to map git errors (permission denied, timeout, no space, not a git repo) to user-friendly messages. Always log the raw error for debugging and send a classified message to the user. +Key pattern: Use `classifyIsolationError()` from `@archon/isolation` for user-friendly git error messages. Always log raw errors and throw early — never swallow silently. ### API Endpoints -**Web UI REST API** (`packages/server/src/routes/api.ts`): - -**Workflow Management:** -- `GET /api/workflows` - List available workflows; optional `?cwd=`; returns `{ workflows: [...], errors?: [...] }` -- `POST /api/workflows/validate` - Validate a workflow definition in-memory (no save); body: `{ definition: object }`; returns `{ valid: boolean, errors?: string[] }` -- `GET /api/workflows/:name` - Fetch a single workflow by name; optional `?cwd=` query param; returns `{ workflow, filename, source: 'project' | 'bundled' }` -- `PUT /api/workflows/:name` - Save (create or update) a workflow YAML; body: `{ definition: object }`; validates before writing; requires `?cwd=` or registered codebase -- `DELETE /api/workflows/:name` - Delete a user-defined workflow; bundled defaults cannot be deleted - -**Workflow Run Lifecycle:** -- `POST /api/workflows/runs/{runId}/resume` - Mark a failed run as ready for auto-resume on next invocation -- `POST /api/workflows/runs/{runId}/abandon` - Abandon a non-terminal run (marks as cancelled) -- `DELETE /api/workflows/runs/{runId}` - Delete a terminal workflow run and its events - -**Codebases:** -- `GET /api/codebases` / `GET /api/codebases/:id` - List / fetch codebases -- `POST /api/codebases` - Register a codebase (clone or local path); body accepts `allowEnvKeys` for the env-leak gate -- `PATCH /api/codebases/:id` - Flip the `allow_env_keys` consent bit; body: `{ allowEnvKeys: boolean }`. Audit-logged at `warn` level on every grant/revoke (`env_leak_consent_granted` / `env_leak_consent_revoked`) with `codebaseId`, `path`, `files`, `keys`, `scanStatus`, `actor` -- `DELETE /api/codebases/:id` - Delete a codebase and clean up resources - -**Artifact Files:** -- `GET /api/artifacts/:runId/*` - Serve a workflow artifact file by run ID and relative path; returns `text/markdown` for `.md` files, `text/plain` otherwise; 400 on path traversal (`..`), 404 if run or file not found - -**Command Listing:** -- `GET /api/commands` - List available command names (bundled + project-defined); optional `?cwd=`; returns `{ commands: [{ name, source: 'bundled' | 'project' }] }` - -**OpenAPI Spec:** -- `GET /api/openapi.json` - Generated OpenAPI 3.0 spec for all Zod-validated routes - -**Webhooks:** -- `POST /webhooks/github` - GitHub webhook events -- Signature verification required (HMAC SHA-256) -- Return 200 immediately, process async - -**Security:** -- Verify webhook signatures (GitHub: `X-Hub-Signature-256`) -- Use `c.req.text()` for raw webhook body (signature verification) -- Never log or expose tokens in responses - -**@Mention Detection:** -- Parse `@archon` in issue/PR **comments only** (not descriptions) -- Events: `issue_comment` only -- Note: Descriptions often contain example commands or documentation - these are NOT command invocations (see #96) +> Full route table, SSE patterns, webhook verification: `.claude/rules/server-api.md` + +- OpenAPI spec: `GET /api/openapi.json` +- GitHub webhooks: `POST /webhooks/github` (HMAC SHA-256 signature verification required) +- `@archon` mention detection: `issue_comment` events only (not descriptions — see #96) From 067cab64f4ec657ca82507bcd61d40b9dd778da4 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 13 Apr 2026 14:36:47 +0300 Subject: [PATCH 23/24] fix: adapt our code to upstream provider API changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getAssistantClient → getAgentProvider (moved to @archon/providers) { tools: [] } → { nodeConfig: { allowed_tools: [] } } (new SendQueryOptions shape) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/orchestrator/orchestrator-agent.ts | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/packages/core/src/orchestrator/orchestrator-agent.ts b/packages/core/src/orchestrator/orchestrator-agent.ts index 85f67370b6..3bd5e8c4d9 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.ts @@ -472,7 +472,8 @@ async function buildFullPrompt( const memorySuffix = memoryContent ? '\n\n---\n\n## Project Memory\n\nLoaded from MEMORY.md (shared with CLI). Topic files can be read on demand via the Read tool at: `' + - computeMemoryPath(conversation.cwd ?? '') + '/`\n' + + computeMemoryPath(conversation.cwd ?? '') + + '/`\n' + 'For session history, check Obsidian vault at `Claude/Session-Logs/` via Obsidian MCP or filesystem.\n\n' + memoryContent : ''; @@ -500,7 +501,14 @@ async function buildFullPrompt( ); } - return systemPrompt + memorySuffix + '\n\n---\n\n## User Message\n\n' + message + contextSuffix + fileSuffix; + return ( + systemPrompt + + memorySuffix + + '\n\n---\n\n## User Message\n\n' + + message + + contextSuffix + + fileSuffix + ); } // ─── Main Handler ─────────────────────────────────────────────────────────── @@ -871,11 +879,16 @@ export async function handleMessage( ); // Retry once (guard against infinite recursion) - const retryDepth = ((context as Record | undefined)?._retryDepth as number | undefined) ?? 0; + const retryDepth = + ((context as Record | undefined)?._retryDepth as number | undefined) ?? + 0; if (retryDepth > 0) { getLog().error({ conversationId, retryDepth }, 'session.auto_compact_retry_limit'); } else { - await handleMessage(platform, conversationId, message, { ...context, _retryDepth: retryDepth + 1 } as HandleMessageContext); + await handleMessage(platform, conversationId, message, { + ...context, + _retryDepth: retryDepth + 1, + } as HandleMessageContext); return; } } catch (compactError) { @@ -1457,7 +1470,10 @@ async function persistConversationMessages( ): Promise { try { // Skip responses containing orchestrator commands (not user-facing content) - if (/^\/invoke-workflow\s/m.test(assistantResponse) || /^\/register-project\s/m.test(assistantResponse)) { + if ( + /^\/invoke-workflow\s/m.test(assistantResponse) || + /^\/register-project\s/m.test(assistantResponse) + ) { return; } await messageDb.addMessage(conversationDbId, 'user', userMessage); @@ -1481,11 +1497,9 @@ async function saveSessionToObsidian(conversation: Conversation): Promise `[${m.role}]: ${m.content.slice(0, 500)}`) - .join('\n\n'); + const transcript = messages.map(m => `[${m.role}]: ${m.content.slice(0, 500)}`).join('\n\n'); - const aiClient = getAssistantClient(conversation.ai_assistant_type); + const aiClient = getAgentProvider(conversation.ai_assistant_type); const cwd = conversation.cwd ?? getArchonWorkspacesPath(); let summary = ''; @@ -1494,7 +1508,7 @@ async function saveSessionToObsidian(conversation: Conversation): Promise `[${m.role}]: ${m.content.slice(0, 500)}`) - .join('\n\n'); + const transcript = messages.map(m => `[${m.role}]: ${m.content.slice(0, 500)}`).join('\n\n'); const fallbackPrompt = `Summarize this conversation transcript. Include: key decisions, current state of work, important context, and pending items. Be concise but complete. Output ONLY the summary.\n\n---\n\n${transcript}`; for await (const chunk of aiClient.sendQuery(fallbackPrompt, cwd, undefined, { - tools: [], + nodeConfig: { allowed_tools: [] }, })) { if (chunk.type === 'assistant') { summary += chunk.content; @@ -1653,9 +1665,8 @@ async function handleResume( return; } - const preview = memoryContent.length > 1000 - ? memoryContent.slice(0, 1000) + '\n...(truncated)' - : memoryContent; + const preview = + memoryContent.length > 1000 ? memoryContent.slice(0, 1000) + '\n...(truncated)' : memoryContent; const memoryPath = computeMemoryPath(conversation.cwd ?? ''); await platform.sendMessage( From 4f1d75a5655fd0e5cf90258aa7f5651e523392ef Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 13 Apr 2026 14:53:24 +0300 Subject: [PATCH 24/24] fix: update /reset test for session-log behavior + prettierignore .beads/ /reset is now intercepted by handleResetWithSessionLog before reaching handleCommand. Updated test to match new behavior. Added .beads/ to .prettierignore. Fixed prettier formatting in 3 files. Co-Authored-By: Claude Opus 4.6 (1M context) --- .prettierignore | 1 + AGENTS.md | 4 ++++ packages/adapters/src/chat/telegram/adapter.ts | 16 +++++++++------- packages/core/src/db/conversations.ts | 5 +---- .../core/src/orchestrator/orchestrator.test.ts | 14 ++++++-------- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/.prettierignore b/.prettierignore index 5f7484c1a6..2f65458d7a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -26,6 +26,7 @@ package-lock.json .agents/ .claude/ .archon/ +.beads/ # Website (Astro/Starlight - uses own formatting) packages/docs-web/ diff --git a/AGENTS.md b/AGENTS.md index d062db0620..d1b57c0e59 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ bd dolt push # Push beads data to remote Shell commands like `cp`, `mv`, and `rm` may be aliased to include `-i` (interactive) mode on some systems, causing the agent to hang indefinitely waiting for y/n input. **Use these forms instead:** + ```bash # Force overwrite without prompting cp -f source dest # NOT: cp source dest @@ -31,12 +32,14 @@ cp -rf source dest # NOT: cp -r source dest ``` **Other commands that may prompt:** + - `scp` - use `-o BatchMode=yes` for non-interactive - `ssh` - use `-o BatchMode=yes` to fail instead of prompting - `apt-get` - use `-y` flag - `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var + ## Beads Issue Tracker This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. @@ -78,6 +81,7 @@ bd close # Complete work 7. **Hand off** - Provide context for next session **CRITICAL RULES:** + - Work is NOT complete until `git push` succeeds - NEVER stop before pushing - that leaves work stranded locally - NEVER say "ready to push when you are" - YOU must push diff --git a/packages/adapters/src/chat/telegram/adapter.ts b/packages/adapters/src/chat/telegram/adapter.ts index e12988a1f9..7255c60415 100644 --- a/packages/adapters/src/chat/telegram/adapter.ts +++ b/packages/adapters/src/chat/telegram/adapter.ts @@ -92,11 +92,7 @@ export class TelegramAdapter implements IPlatformAdapter { * Send a single chunk with MarkdownV2 formatting, with fallback to plain text. * If threadId is provided, sends to that forum topic. */ - private async sendFormattedChunk( - id: number, - chunk: string, - threadId?: number - ): Promise { + private async sendFormattedChunk(id: number, chunk: string, threadId?: number): Promise { // Build options: include thread ID only when targeting a forum topic const threadExtra = threadId ? { message_thread_id: threadId } : undefined; @@ -240,8 +236,14 @@ export class TelegramAdapter implements IPlatformAdapter { const conversationId = this.getConversationId(ctx); // Debug: log forum topic detection const msg = ctx.message; - const threadId = 'message_thread_id' in msg ? (msg as { message_thread_id?: number }).message_thread_id : undefined; - getLog().info({ chatId: ctx.chat?.id, threadId, conversationId, chatType: ctx.chat?.type }, 'telegram.message_received'); + const threadId = + 'message_thread_id' in msg + ? (msg as { message_thread_id?: number }).message_thread_id + : undefined; + getLog().info( + { chatId: ctx.chat?.id, threadId, conversationId, chatType: ctx.chat?.type }, + 'telegram.message_received' + ); // Fire-and-forget - errors handled by caller void this.messageHandler({ conversationId, message, userId }); } diff --git a/packages/core/src/db/conversations.ts b/packages/core/src/db/conversations.ts index d82f7482c1..d63083acae 100644 --- a/packages/core/src/db/conversations.ts +++ b/packages/core/src/db/conversations.ts @@ -247,10 +247,7 @@ export async function updateConversationTitle(id: string, title: string): Promis /** * Update conversation context summary (used by /compact) */ -export async function updateConversationSummary( - id: string, - summary: string | null -): Promise { +export async function updateConversationSummary(id: string, summary: string | null): Promise { const dialect = getDialect(); const result = await pool.query( `UPDATE remote_agent_conversations SET context_summary = $1, updated_at = ${dialect.now()} WHERE id = $2`, diff --git a/packages/core/src/orchestrator/orchestrator.test.ts b/packages/core/src/orchestrator/orchestrator.test.ts index de4618ed15..95583cc86e 100644 --- a/packages/core/src/orchestrator/orchestrator.test.ts +++ b/packages/core/src/orchestrator/orchestrator.test.ts @@ -494,17 +494,15 @@ describe('orchestrator-agent handleMessage', () => { expect(platform.sendMessage).toHaveBeenCalledWith('chat-456', 'Help text'); }); - test('delegates /reset to command handler', async () => { - mockHandleCommand.mockResolvedValue({ - message: 'Session cleared', - modified: false, - success: true, - }); + test('handles /reset with session log preservation', async () => { + // /reset is now intercepted by handleResetWithSessionLog before reaching handleCommand. + // With no active session, it sends a "no active session" message. + mockGetActiveSession.mockResolvedValueOnce(null); await handleMessage(platform, 'chat-456', '/reset'); - expect(mockHandleCommand).toHaveBeenCalled(); - expect(platform.sendMessage).toHaveBeenCalledWith('chat-456', 'Session cleared'); + expect(mockHandleCommand).not.toHaveBeenCalled(); + expect(platform.sendMessage).toHaveBeenCalledWith('chat-456', 'No active session to reset.'); }); test('uses CommandResult workflow definition without rediscovery for /workflow run', async () => {