Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/edge/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> {
private writeSessionFiles(member: string, uid: number, sessionId: string, files?: { mcp?: string; company?: string; task?: string }): Record<string, string> {
const dir = this.sessionsDir(member);
this.prepareDir(dir, uid); // mkdir + chown uid + 0700
const extra: Record<string, string> = {};
Expand All @@ -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;
}

Expand Down
8 changes: 5 additions & 3 deletions src/edge/session-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ export interface SpawnSpec {
env: Record<string, string>;
/** 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). */
Expand Down
16 changes: 14 additions & 2 deletions src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1591,15 +1591,27 @@ 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.
const mcpFile = this.writeSessionFile(o.id, 'mcp.json', mcpJson);
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] });
}
Expand Down
10 changes: 9 additions & 1 deletion terminal/claude-launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"; }
Expand Down
Loading