fix: quickstart systemd restart + topic assign platform detection#85
Open
changhansung wants to merge 304 commits into
Open
fix: quickstart systemd restart + topic assign platform detection#85changhansung wants to merge 304 commits into
changhansung wants to merge 304 commits into
Conversation
changhansung
commented
Jun 16, 2026
Contributor
- Quickstart prefers systemctl restart (keeps watchdog) over detached spawn
- assignTopicIds uses instance channel_id → channels[].type instead of name.includes()
Telegram bot API requests embed the bot token in the URL path (`/bot<token>/...`). A misconfigured or malicious `telegram_api_root` would silently leak the token to whatever host is configured. Validate at startup: only api.telegram.org (https) and loopback (localhost / 127.0.0.1 / ::1, for E2E mock servers) are accepted. Anything else throws — fail fast at config load rather than after the first API call sends credentials to the attacker. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The 409 conflict retry loop was unbounded — a permanently stuck conflict would keep retrying forever every 15s. Cap at 30 attempts (~7 min total) and emit a final error so the operator can intervene. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The previous 10 MB ceiling let a runaway producer (or hostile peer sharing the socket) buffer 10 MB per client before being dropped. Real IPC payloads — tool calls/responses, decision/task lists — are well under 100 KB; 1 MB stays comfortably above any legitimate message while shrinking the DoS window 10x. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The flood-control branch in MessageQueue.runWorker had a dead comment
("reset backoff now that we've cleaned up") but never actually reset the
backoff. As a result, once sustained 429s drove backoffMs above
FLOOD_CONTROL_THRESHOLD_MS, the queue would shed all status_update items
yet still wait the full ~30s exponential delay between every retry — even
though the queue was now small and the rate-limit may already have eased.
Fix:
- After dropping items, reset backoffMs to INITIAL_BACKOFF_MS and clear
backoffUntil, so the next retry happens within ~1s instead of ~30s.
- Emit a warn log noting how many items were dropped, so operators can
see flood control activity instead of silent drops.
Test: tests/channel/message-queue.test.ts asserts that after sustained
429s push backoff past the threshold, status_updates are mostly dropped,
the warn fires, and the surviving content message still gets delivered.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
When control mode disconnected and reconnected, paneToWindow and lastOutputAt kept stale paneId → windowId mappings from the previous tmux server. After a tmux restart pane ids are recycled, so waitForIdle / isIdle could consult a pane that no longer belongs to the expected window (or has been recycled to a different one) — returning false "idle" and letting us paste into the wrong target. Track every registered windowId in a Set; on (re)connect clear the transient paneId caches and re-resolve panes for all registered windows so upper layers don't need to know about the reconnect. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
setInterval keeps firing the poll callback even when the previous invocation is still awaiting stat/open/read. With slow disk or a large transcript two concurrent runs would race on byteOffset — both could read the same byte range and emit duplicate tool_use / assistant_text events to downstream consumers. A simple boolean guard makes the second tick a no-op until the first finishes; setInterval itself stays unchanged. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
If the daemon was down across a scheduled fire time, the schedule would silently skip until the next regular trigger. Now `init()` walks every enabled schedule and, if its most recent expected run falls in the past 24h, fires it once. The 24h cap avoids dumping a backlog of pings on the user after a long outage, while still recovering from short crashes/restarts. At most one catch-up per schedule — `* * * * *` doesn't replay 59 missed minutes. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
When a daily limit fires, the limit handler pauses the instance. If the user manually restarts that instance, the new session causes a cost-rotation event but `limitEmitted` stayed sticky for the rest of the day — so the new session could blow past the daily cap again without ever re-pausing. Same shape for the warn flag. Reset both flags on `snapshotAndReset` so the next cost report re-evaluates against the (still-elevated) accumulated total and re-emits if the threshold is still crossed. Within a single session each event still fires at most once. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Old impl used `setHours(24, 0, 0, 0)` (a local-tz operation) on a Date constructed by reinterpreting the target-tz wall-clock as local-tz time. The two reinterpretations cancel for normal days, but on DST days the target tz's day length differs from local arithmetic by an hour: - Spring-forward day in target tz (23h day): scheduler fires ~1h LATE. - Fall-back day in target tz (25h day): scheduler fires ~1h EARLY. Either way the daily reset misaligns with the user's calendar day twice a year — exactly when daily-budget alerts most need to be predictable. Replaced with an Intl.DateTimeFormat-based binary search that finds the first instant where the date *as observed in the target tz* changes. This naturally handles 23h, 24h, and 25h day lengths regardless of local-vs-target tz mismatches. Tests cover the two specific buggy moments (LA pre-jump 01:30 PST and LA pre-fallback 01:30 PDT) plus a 26h upper bound and positivity invariant.
The archived Set lived in memory only — after a daemon restart the poller would re-archive every previously-closed idle topic and re-issue closeForumTopic, fighting users who had reopened topics manually. Persist to <dataDir>/archived-topics.json on add/remove and load on construct. Adds dataDir to ArchiverContext (FleetManager already exposes it). Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
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
feat: notify General when non-admin tries /start or /stop
docs: add kiro-cli session storage path to skill
fix: agend update auto-removes stale npm link
fix: systemd NotifyAccess=all + TimeoutStartSec=0
feat: /status + /sysinfo markdown tables (Rich Message compatible)
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
… for platform detection
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.