Skip to content

docs: complete configuration and commands reference#86

Open
changhansung wants to merge 314 commits into
suzuke:mainfrom
songsid:docs/config-and-commands-reference
Open

docs: complete configuration and commands reference#86
changhansung wants to merge 314 commits into
suzuke:mainfrom
songsid:docs/config-and-commands-reference

Conversation

@changhansung

Copy link
Copy Markdown
Contributor
  • docs/configuration.md: full fleet.yaml + classicBot.yaml parameter reference (all fields, types, defaults, descriptions)
  • docs/commands.md: all TG + DC slash commands with permissions, platforms, and handler info

suzuke and others added 30 commits April 23, 2026 21:58
The 6-hex suffix on getTmuxSessionName/getTmuxSocketName is not
cryptographic — it just disambiguates two custom AGEND_HOME values on
the tmux namespace. md5 was fine functionally but trips FIPS-mode Node
and routine security scanners. Switching to sha256 (still slice(0,6))
removes those false positives.

Behaviour change: users with a custom AGEND_HOME will see their
tmux session/socket name change once on first run after the upgrade.
A daemon restart resyncs cleanly; any orphan tmux session under the
old name can be killed manually. Default AGEND_HOME (which uses the
literal string "agend" with no suffix) is unaffected.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- src/scheduler/scheduler.ts no longer defines its own validateTimezone;
  it imports the canonical implementation from src/config.ts. This was
  the only duplicate left after PR #34 — error messages now consistently
  read "timezone: invalid timezone \"X\". Use IANA format ...".
- src/quickstart.ts and src/setup-wizard.ts now write the .env file with
  mode 0o600 and chmod after the write (writeFileSync's mode flag is
  ignored for existing files). The .env contains the bot token and
  optionally a Groq API key — restricting to owner-only read/write
  prevents other local users / sibling processes from reading them.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…ernally

`rm -rf ~/.agend/instances/<name>` while the daemon was running caused the
health check loop to spam ENOENT / tmux-died / respawn-failed errors every
~30s indefinitely (5000+ errors in ~20h observed in daemon.log).

Root cause: every tick the health check saw pane dead → respawn path →
writeRotationSnapshot(snapshotPath) → writeFileSync ENOENT → caught as
"Failed to respawn" → scheduleNext() → loop.

Fix: check existsSync(instanceDir) at the start of each tick; if missing,
pause the loop and clear the timer. TranscriptMonitor and ContextGuardian
already handle missing files silently, so no other cleanup is needed.
Previously writeFileSync used default perms (0o644), exposing the
fleet-control token to other users on the same host. Tightened to 0o600
on create and defensive chmod for any pre-existing looser file.

Addresses ultrareview Phase 1 item P1.2.
`startInstancesWithConcurrency` already awaits `startInstance` →
`lifecycle.start` → `daemon.start` (IPC listening) → `connectIpcToInstance`
for every instance. By the time it resolves, the IPC channel is already
wired, so the trailing `await sleep(3000)` + `connectToInstances(fleet)`
loop in `start()` and the `await sleep(5000)` + second
`connectIpcToInstance` in `bindAndStart()` were dead weight that just
slowed down startup and topic creation.

Removed both redundant waits and the now-unused private
`connectToInstances` helper.
Two leaks in the Web UI SSE pipeline:

1. /ui/events only listened for `req.on("close")`. A network reset
   that never delivers a clean FIN would leave the client in
   `sseClients` forever and the 10s heartbeat interval running until
   the daemon shut down.

2. `emitSseEvent` blindly called `client.write()`. The first dead
   client to throw aborted delivery to every later client in the set,
   and the dangling Set entry itself was never cleaned up.

Extract `broadcastSseEvent()` so the eviction logic is testable,
add `req.on("error")` and `res.on("error")` to the handler with an
idempotent cleanup, and route `FleetManager.emitSseEvent` through
the helper.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
When adding allowed guilds to classicBot.yaml, read the Discord bot
token from .env and list servers the bot is in. User can select from
the list instead of manually entering guild IDs. Falls back to manual
entry for unlisted servers.
…ld listing

readDiscordToken() was hardcoded to look for AGEND_DISCORD_TOKEN in
.env, but the actual env var name is configured in fleet.yaml's
bot_token_env (could be AGEND_BOT_TOKEN or anything else).

Also coerce allowed_guilds to strings — yaml may load bare numbers.
Classic bot instances from classicBot.yaml now appear in list_instances
results with tag 'classic' and display_name 'classic: <channel-name>'.
Allows agents to discover and interact with classic bot instances.
…lock

createTopic() used guild.channels.cache which is empty on fresh
environments, causing each channel to create its own category.

- Add promise-based lock (categoryIdPromise) so concurrent callers
  share one resolution instead of racing
- Fetch all guild channels on first call to find existing category
- Invalidate cache on Discord error 10003 (deleted category) and retry
- Clear rejected promise so transient API failures don't stick
Document attachments were only passed as file_id for manual download,
unlike photos which are auto-downloaded. Now documents are downloaded
to inbox with attachment_path in extraMeta and a [📎 File] prefix,
falling back to file_id on download failure.
Extend saveClassicAttachment() to download both photo and document
attachments to workspace inbox. Returns { path, kind } so the caller
can set the correct extraMeta (image_path vs attachment_path) and use
appropriate emoji reactions (📸 for photos, 📎 for documents).
- New classicBot creation: collect admin_users after backend
  (pre-filled with quickstart userId, can add more)
- Patch mode: renamed maybeAddClassicBotGuilds → maybeUpdateClassicBot,
  now also asks 'Add admin users? [y/N]' with show/collect/append
- Output format includes defaults.admin_users alongside allowed_guilds
… /raw

Part 1: ClassicChannelManager
- defaults.admin_users?: string[] in classicBot.yaml
- isAdmin(userId): empty/unset = no admins (secure default)

Part 2: Discord adapter
- Register /compact, /save (filename + force), /load (filename) slash commands
- Extract interaction options as key-value pairs in slash_command event

Part 3: Fleet manager
- Handle /compact, /save, /load with admin check
- pasteRawToClassicInstance: send raw_paste IPC to bypass [user:] wrapping
- /save supports force option (appends -f flag)

Part 4: Daemon
- raw_paste IPC handler: paste directly to CLI
- /raw prefix in pushChannelMessage: topic mode users can paste raw text
  (protected by allowed_users access control upstream)
…paste

1. Classic channel /chat /raw bypass: users could send '/chat /raw /compact'
   to bypass admin check. Now handleClassicChannelMessage blocks messages
   starting with /raw after stripping /chat prefix.

2. raw_paste IPC handler now uses pasteLock + deliverMessage instead of
   direct tmux.pasteText, preventing race conditions with concurrent
   message deliveries and respecting idle wait.
C1: /update authorization — empty allowed_users now disables /update
    (was: empty = anyone can update). Matches upstream/main behavior.

C2: /save /load filename injection — validate filename against [\w.-]+
    to prevent newline injection via tmux paste.

C3: Restore shellQuote for AGEND_HOME and AGEND_INSTANCE_NAME in
    daemon env prefix. JSON.stringify uses double quotes which allow
    $ and backtick interpretation in bash.
- daemon.ts: emit message_queued (when pasteQueueDepth > 0) and
  message_delivered (after deliverMessage) with chatId + messageId
- instance-lifecycle.ts: listen for both events, call ctx.reactMessageStatus
- fleet-manager.ts: reactMessageStatus reacts on the user's message,
  gated to Discord adapter only
- /chat respond now returns the bot reply message ID
- Pass channelId as chatId + reply messageId to daemon meta
- Optimize react() to try chatId as channelId first (fast path)
- Fixes: ⏳/✅ reactions not showing because messageId was empty
…sy TUI

When multiple messages are queued and the TUI is busy outputting, the
first send-keys Enter after paste-buffer can be swallowed. Add a retry
Enter after 1s delay. All supported CLIs (kiro-cli, claude-code,
gemini-cli, codex, opencode) ignore Enter on empty input, making the
retry safe.
…allow

Root cause: with 10s/30s timeout, waitForIdle expires while the agent is
still outputting. The forced paste succeeds but Enter is swallowed by the
busy TUI. With 120s, paste only happens when the CLI has been silent for
2s (truly idle), so Enter is reliably accepted.

The retry Enter in pasteText remains as a safety net for the rare case
where the 120s timeout expires.
Part 1: restart_instance MCP tool
- outbound-schemas.ts: RestartInstanceArgs (name only)
- outbound-handlers.ts: restartInstance handler calling ctx.restartSingleInstance
- OutboundContext: add restartSingleInstance method
- mcp-tools.ts: register restart_instance tool
- daemon.ts: add to CROSS_INSTANCE_TOOLS set

Part 2: lightweight waitForIdle
- deliverMessage: lightweight 30s, normal 120s (was 120s for both)
After /start registers a new classic channel, setOpenChannels on the
adapter wasn't updated. Messages from non-primary guilds to the new
channel were silently dropped because the channel wasn't in openChannels.
This also affected attachment URL storage — images couldn't be downloaded.
1. handleClassicStop: add reregisterClassicChannels() after unregister
   to remove stopped channel from adapter openChannels.

2. reregisterClassicChannels: always call setOpenChannels even when
   channels list is empty, so the last /stop properly clears all
   stale entries from the adapter.
No admin required. Reads context from statusline.json first, falls
back to tmux capture-pane with kiro-cli regex parser.
Shows context percentage, backend, and instance name.
OpenCode shows 'Permission required' when accessing external directories,
followed by a 'confirm' prompt. Add getRuntimeDialogs to auto-press Enter
on both layers.
Runtime dialogs (Permission required, confirm) need fast response.
30s meant each prompt waited up to 30s before auto-dismiss.
5s keeps CPU impact minimal (single capture-pane per cycle).
changhansung and others added 30 commits June 15, 2026 15:58
fix: install.sh removes npm-linked old version via readlink
fix: /sysinfo markdown table + kiro error pattern
fix: workspace path validation + classic group unbound topic suppression
ci: add contents:write permission for auto Release
docs: v2.0.0 + v2.0.1 CHANGELOG
feat: memory layering best practice in fleet steering
fix: memory best practice - add guards against hallucination
Previously only triggered when NO general existed at all.
Now detects each channel adapter without a bound general and
auto-creates one. Uses new channel_id field for explicit binding.
findGeneralInstance prefers channel_id match over name heuristic.
fix: auto-create general per adapter in multi-channel setup
docs: multi-channel skill (dual platform setup)
- Removed fleet.pid blocker for 'add users' and 'add platform' actions
- Add platform: detached spawn 'agend fleet restart --reload' (avoids SIGUSR1 exit(0) + Restart=on-failure bug)
- Add users: SIGHUP hot-reload
- Only 'Overwrite' still requires fleet to be stopped
feat: quickstart supports adding platform while fleet is running
…assign

fix: quickstart systemd restart + assignTopicIds channel_id platform detection
…ates

When adding a second platform, existing general without channel_id was
not recognized as belonging to any adapter. Auto-create spawned duplicates.

Fix: 3-phase approach:
1. Match unbound generals by topic_id (TG general has topic_id=1, DC has general_channel_id)
2. Adopt remaining unbound generals first-come
3. Only create new instances if no unbound generals left

This correctly handles: existing TG general + add DC → general gets channel_id=telegram,
new general-discord created for DC.
fix: adopt existing general when adding second platform
Without it, General has no valid topic_id and routing breaks.
Loop until user provides a value with explanation why it's needed.
fix: Discord general_channel_id required in quickstart
On machines without D-Bus user session (root, SSH without linger,
containers), systemctl --user fails. Now falls back to fleet.pid
kill like agend fleet stop does.
fix: agend stop fallback to PID kill when systemd unavailable
…lable

Same D-Bus issue as agend stop. When startService() fails or no service
is installed, spawns 'agend fleet start' in background instead of
giving up.
fix: agend start fallback to direct fleet start when systemd unavailable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants