diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bef6d5..8601ec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ new version heading in the same commit. ## [Unreleased] +## [0.265.2] — 2026-07-24 +### Fixed +- **The `consolidator` (memory gardener) crashed at launch on every tenant — and so could any run with a + large task prompt.** `LocalSessionBackend` puts every launch env var on the `tmux new-session` command + line, and tmux hard-caps that command at ~16KB (`command too long`). The consolidator embeds up to 40 + recent episodes/lessons in its task (18–46KB), which — base64'd into `TASK_B64` — overflowed the cap, so + `new-session` failed **silently**: no pane, no transcript, no `.log`, and the liveness sweep then flipped + the never-launched row to `crashed` (~11s in, `turns=null`). Only the single small-task run ever + succeeded. The task now rides as a **file** (`TASK_FILE`, like `mcp.json`/`company.md` already do) instead + of a giant env var, keeping the command line tiny regardless of task size; `claude-launch.sh` reads + `TASK_FILE` (falling back to `TASK_B64` for pre-existing resume envs). Fixes the consolidation half of the + self-learning loop and any chat/automation run whose prompt exceeded ~10KB. + ## [0.265.1] — 2026-07-24 ### Fixed - **Task room sidebar no longer overlaps/cramps its Status/Assignee/Goal/Criteria controls.** Select diff --git a/package-lock.json b/package-lock.json index 2b5a61d..7917135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.265.1", + "version": "0.265.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.265.1", + "version": "0.265.2", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index f2e2cc0..5b43ef9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.265.1", + "version": "0.265.2", "description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.", "license": "MIT", "type": "commonjs", diff --git a/src/edge/launcher.ts b/src/edge/launcher.ts index 654a75a..8b6bc55 100644 --- a/src/edge/launcher.ts +++ b/src/edge/launcher.ts @@ -319,7 +319,7 @@ export class LauncherDaemon { * uid can read them, and return the env additions (MCP_CONFIG/COMPANY_FILE) pointing at them. * Written as root then chowned to the member uid, 0600. (The app can't write the member's 0700 home.) */ - private writeSessionFiles(member: string, uid: number, sessionId: string, files?: { mcp?: string; company?: string }): Record { + private writeSessionFiles(member: string, uid: number, sessionId: string, files?: { mcp?: string; company?: string; task?: string }): Record { const dir = this.sessionsDir(member); this.prepareDir(dir, uid); // mkdir + chown uid + 0700 const extra: Record = {}; @@ -332,6 +332,7 @@ export class LauncherDaemon { }; if (files?.mcp) put(`${sessionId}.mcp.json`, files.mcp, 'MCP_CONFIG'); if (files?.company) put(`${sessionId}.company.md`, files.company, 'COMPANY_FILE'); + if (files?.task) put(`${sessionId}.task`, files.task, 'TASK_FILE'); return extra; } diff --git a/src/edge/session-backend.ts b/src/edge/session-backend.ts index 87dfaab..9b71f3f 100644 --- a/src/edge/session-backend.ts +++ b/src/edge/session-backend.ts @@ -21,9 +21,11 @@ export interface SpawnSpec { env: Record; /** The in-session command, e.g. `['bash', '/abs/terminal/claude-launch.sh']`. */ argv: string[]; - /** Per-session file CONTENTS the runner reads: the `.mcp.json` (connectors) + company markdown. - * Each backend materialises them somewhere the session can read (local dir vs the member home). */ - files?: { mcp?: string; company?: string }; + /** Per-session file CONTENTS the runner reads: the `.mcp.json` (connectors), company markdown, and the + * opening task prompt. Each backend materialises them somewhere the session can read (local dir vs the + * member home). The task rides as a file (not inline env) so a large prompt can't overflow tmux's + * new-session command-length cap — see launchClaudeCode. */ + files?: { mcp?: string; company?: string; task?: string }; /** The shared agent source dir. Under the launcher it's copied to a per-member working dir (so the * member uid can write claude's `.claude/`/scratch); the local backend ignores it (claude runs as * the app uid directly in the source dir, as today). */ diff --git a/src/terminal.ts b/src/terminal.ts index aca2c71..c000900 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -1591,8 +1591,11 @@ export class TerminalManager { // agent's plain `ssh` authenticates to a host without ever handling the key. (Local-lane only.) this.injectHostCredentials(env, o.agent, o.actingMember, o.id); if (this.uidIsolation) { - // Flag on: the launcher writes the files INTO the member's home and sets MCP_CONFIG/COMPANY_FILE itself. - this.backend.spawn(this.spaceFor(o.actingMember ?? o.spawnedBy), { sessionId: o.id, agent: o.agent, tmuxName: tmux, env, argv: ['bash', this.launcher], files: { mcp: mcpJson || undefined, company: companyMd || undefined }, agentSrc: manifest.dir }); + // Flag on: the launcher writes the files INTO the member's home and sets MCP_CONFIG/COMPANY_FILE/ + // TASK_FILE itself. The task rides as a FILE (not the inline TASK_B64 env) for the same reason as the + // local lane below — see the delete note there; the launcher points TASK_FILE at the member-home copy. + delete env.TASK_B64; + this.backend.spawn(this.spaceFor(o.actingMember ?? o.spawnedBy), { sessionId: o.id, agent: o.agent, tmuxName: tmux, env, argv: ['bash', this.launcher], files: { mcp: mcpJson || undefined, company: companyMd || undefined, task: o.task || undefined }, agentSrc: manifest.dir }); } else { // Flag off: materialise into the app's connectors dir and persist the launch context so the ttyd // attach wrapper can resurrect a dead session. Headless automation runs write no resurrect env. @@ -1600,6 +1603,15 @@ export class TerminalManager { if (mcpFile) env.MCP_CONFIG = mcpFile; const companyFile = this.writeSessionFile(o.id, 'company.md', companyMd); if (companyFile) env.COMPANY_FILE = companyFile; + // The task rides as a FILE, not the inline TASK_B64 env. LocalSessionBackend.spawn puts EVERY env var + // on the `tmux new-session` command line, and tmux hard-caps that command at ~16KB ("command too + // long") — so a base64'd task over ~10KB (e.g. the consolidator's 40-episode batch, or any long + // chat/automation prompt) made new-session fail SILENTLY: no pane, no transcript, then the liveness + // sweep flipped the never-launched row to `crashed`. Passing the task by path keeps the command line + // tiny regardless of task size; the launcher reads TASK_FILE (falling back to TASK_B64 for old + // persisted resume envs). Drop TASK_B64 once the file is written so it can't re-inflate the cmdline. + const taskFile = this.writeSessionFile(o.id, 'task', o.task); + if (taskFile) { env.TASK_FILE = taskFile; delete env.TASK_B64; } if (!o.headless) this.writeEnvFile(o.id, env); this.backend.spawn(this.spaceFor(o.actingMember ?? o.spawnedBy), { sessionId: o.id, agent: o.agent, tmuxName: tmux, env, argv: ['bash', this.launcher] }); } diff --git a/terminal/claude-launch.sh b/terminal/claude-launch.sh index 243c9e1..b9ced79 100755 --- a/terminal/claude-launch.sh +++ b/terminal/claude-launch.sh @@ -27,7 +27,15 @@ fi # The `claude` CLI is commonly installed under ~/.local/bin; make sure it's findable even # when the parent process (e.g. a hardened systemd unit) ships a minimal PATH. export PATH="$HOME/.local/bin:$PATH" -TASK=$(printf '%s' "${TASK_B64:-}" | base64 -d 2>/dev/null) +# The opening task prompt. Prefer TASK_FILE (a path the server materialised) over the inline TASK_B64 +# env: LocalSessionBackend puts every env var on the `tmux new-session` command line, which tmux caps at +# ~16KB, so a large base64 task there made new-session fail and the run never launched. TASK_B64 is kept +# as a fallback for sessions whose persisted resume env predates TASK_FILE. +if [ -n "${TASK_FILE:-}" ] && [ -f "${TASK_FILE}" ]; then + TASK=$(cat "${TASK_FILE}") +else + TASK=$(printf '%s' "${TASK_B64:-}" | base64 -d 2>/dev/null) +fi cyan() { printf '\033[36m%s\033[0m\n' "$1"; } dim() { printf '\033[2m%s\033[0m\n' "$1"; }