Recent feature changes and implementation details for SAM. Use when understanding recent changes, checking what was modified recently, or understanding feature history.
Installs into .claude/skills of the current project.
Are you the author of Changelog?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/raphaeltm-changelog-simple-agent-manager)
---
name: changelog
description: Recent feature changes and implementation details for SAM. Use when understanding recent changes, checking what was modified recently, or understanding feature history.
user-invocable: false
---
# SAM Recent Changes
## Detailed Changes Moved From CLAUDE.md
These entries were removed from root `CLAUDE.md` so startup instructions stay compact. Use this skill when recent implementation context is needed.
- Compact archive shards: opt-in `PROJECT_DATA_ARCHIVE_COMPACT_ENABLED` pins lossless `r2-gzip-v1` raw history while keeping complete grouped text/search and metadata in SQL. `PROJECT_DATA_ARCHIVE_DAILY_WRITE_BUDGET` (250000), `PROJECT_DATA_ARCHIVE_WRITE_ESTIMATE_FACTOR` (32) and `PROJECT_DATA_ARCHIVE_R2_TIMEOUT_MS` (10000) bound admission/object I/O; see compact archive configuration for estimated versus measured costs and whole-session deletion limits. Legacy archives remain readable and unchanged.
Use the `/changelog` skill for structured queries.
- mcp-connection-custom-headers: Bring-your-own MCP servers can carry custom HTTP headers (Composio requires `x-api-key` / `x-consumer-api-key`). Headers are independent of `authType`; `Authorization` is accepted only with `none`. Names `^[A-Za-z0-9_-]{1,64}$` — exactly what the Amp bridge `mcp-remote@0.1.38` parses and a TOML bare key — pinned TS↔Go by the `headerNames` block of `mcp-server-name-contract.json`; transport headers are reserved (`headerNames.reserved` pins that list on both sides). D1 `0178` adds `header_names` (plaintext display projection) + `encrypted_headers`/`headers_iv` (one AES-GCM ciphertext of the `[{name,value}]` list, the only column injection reads). All rules and the storage format live in `services/mcp-connection-headers.ts`. PATCH `headers` is the full desired set and an entry without `value` keeps the stored value, so the UI edits without ever holding secrets. PATCH writes only if `updated_at` still matches the row it read (`nextUpdatedAt` keeps the column strictly increasing; a lost race is a 409): every derived field comes from that read, so without the guard a header-only edit racing a switch to bearer persisted an `Authorization` header beside a bearer token. Resolution re-validates decrypted headers, refuses that pair as a backstop, and skips a bad row. vm-agent: `McpServerEntry.ValidateHeaders` (charset, reserved names, case-insensitive duplicates including `Authorization` beside the bearer token — one repeated key makes the whole Codex/Vibe TOML unparseable, `sam-mcp` included) runs in `normalizeMcpServers` and again before any config file is written; headers are persisted (`migrateV18`; a row whose headers cannot be decoded is skipped on restore instead of dropping every server), converted by `toPersistedMcpServers`/`fromPersistedMcpServers`; ACP header list, Codex `env_http_headers` with `SAM_MCP_<NAME>_HEADER_<i>_SECRET` (the `_SECRET` suffix both classifies it for `isSecretEnvVar` and cannot collide with a bearer `_TOKEN` var), Vibe `headers` table, Amp `--header name:${SAM_MCP_HEADER_<i>}`. Wire shape pinned by `packages/shared/src/fixtures/mcp-server-entry-wire.json` (node-agent serializer + real Go handler). UI: `McpServerForm` (add + inline Edit), `McpServerHeadersField`. Limits `MAX_MCP_CONNECTION_HEADERS` (10), `MCP_CONNECTION_HEADER_VALUE_MAX_BYTES` (8192). Refactor in the same PR: MCP/Codex/Vibe code moved out of `gateway.go`/`session_host.go`/`workspaces.go`/`store.go` into `acp/mcp_servers.go`, `acp/codex_config.go`, `acp/vibe_config.go`, `server/mcp_servers.go`, `persistence/session_mcp_servers.go`.
- project-chat-instant-switching: Switching between project chats renders the target chat at once. Transcripts (`sessions/messages`) now keep a 24 h `gcTime` (`CHAT_TRANSCRIPT_CACHE_TTL_MS`, from shared `DEFAULT_CHAT_TRANSCRIPT_CACHE_TTL_MS`, override `VITE_CHAT_TRANSCRIPT_CACHE_TTL_MS`); previously the TanStack 5-minute default dropped an unobserved transcript from memory and, on the next write, from IndexedDB. Restored queries get `RESTORED_QUERY_GC_TIME_MS` via `hydrateOptions`, the dehydrate filter stops writing a transcript older than the TTL, and opening a chat evicts the least recently updated unobserved transcripts beyond `CHAT_TRANSCRIPT_CACHE_MAX_SESSIONS` (20; `evictStaleTranscripts` in `lib/query-options/chats.ts`); on disk each transcript keeps only its newest `CHAT_TRANSCRIPT_PERSIST_MAX_ROWS` rows (default: the 500-row page; `persistedQueryForDisk`). `ProjectMessageView` is keyed per session (`SessionMessageView`), and the transcript is read straight from the query cache (`useSessionTranscript`), so a cached chat paints in the switching commit and an uncached one shows a spinner, never the previous chat. The cold load requests the newest page (`fetchNewestPage`, `CHAT_SESSION_MESSAGE_LIMIT` = 500) instead of the 50,000-row `CHAT_SESSION_MESSAGE_MAX` ceiling (now only the server clamp); older history pages in through Virtuoso `startReached` once the reader scrolls up — wheel, swipe, or ArrowUp/PageUp/Home, not the list position (a page of tool calls can fold into a few rows that fit on screen, and a chat can open away from the bottom when its newest message is taller than the screen; an ungated `startReached` then paged the whole history in on open) and through the existing "Load earlier" button. The #2159 forward-delta refresh moved into the query function unchanged. Jumps to a specific message page back until that message id is loaded (`HistoryTarget` in `lib/message-paging.ts`) and then confirm the row is on screen, re-scrolling past Virtuoso's prepend compensation (`useConversationJump`), and an unloaded comment anchor reads "on a message". Composer drafts are per chat (`session-drafts.tsx`), report-issue config is a cached query, and server `session`/`state` snapshots hydrate only when the server reports new ones, which fixes a streamed row resetting a working agent to idle. Auth gating is unchanged: no persisted transcript renders before the session check resolves.
- archive-sweep-affordability-ceiling-and-fallthrough: The production ProjectData archive sweep stopped reclaiming anything on 2026-09-08 and reported `succeeded` for 226 hourly runs while the root object climbed from 94% to 96.7% of its 10 GB ceiling. Two independently configured ceilings had to agree and drifted: a GitHub `production` Environment override lowered `PROJECT_DATA_ARCHIVE_DAILY_WRITE_BUDGET` to 100000 (affordability ceiling `floor((100000-1000)/32)` = 3093 write units) while `PROJECT_DATA_ARCHIVE_SWEEP_MESSAGE_BUDGET` stayed at the checked-in 5000, and `selectCandidates` orders `message_count DESC ... LIMIT sweepProjects * sweepSessions` (deployed 1x1). Every tick therefore picked the same 4994-message session, estimated ~160,808 writes, was refused by `reserveArchiveWrites` BEFORE it touched D1 (so the UTC budget window also froze at `2026-09-08T00:00:00Z`), and `continue`d out of a one-element list — no journal row, no location change, no error. The selection ceiling is now DERIVED from the allowance (`archiveAffordableWriteUnits` / `archiveAffordableMessageCeiling` in `project-data-archive/write-budget.ts`, used as the `message_count <= ?` bind), so the two can no longer disagree at any configuration; an explicit session-scoped operator canary still bypasses it. `selectCandidates` over-reads `PROJECT_DATA_ARCHIVE_SWEEP_FALLTHROUGH_DEPTH` (4) spare candidates and the journaling loop descends past a refusal instead of ending the tick, with both per-tick bounds (session slots and the cumulative message budget) moved INTO that loop so a refused candidate — which opens no `migrating` fence and moves no rows — consumes neither. `reserveArchiveWrites` returns a discriminated outcome separating `exceeds_allowance` (waiting cannot help) from `window_exhausted` (normal end-of-day backpressure), and `PROJECT_DATA_ARCHIVE_BUDGET_STALL_ALERT_SWEEPS` (3) consecutive ticks that migrate nothing and see only the former flip the cadence row to `partial` with an actionable `last_error` — counted and escalated inside one atomic `UPDATE ... RETURNING` (migration `0156` adds `consecutive_budget_stalls`) so a failed read cannot silently restart a streak. `wrangler.toml` now ships `DAILY_WRITE_BUDGET=100000` (matching the production override, so staging and self-hosts derive the same ceiling), `SWEEP_MESSAGE_BUDGET=2000`, `SWEEP_SESSIONS=2`. Owner stubs are memoised per tick so the fall-through does not multiply `ensureProjectId` DO round trips. NOTE: within a 100000/day allowance the restored sweep reclaims on the order of 1-1.5 MB/day against ~66 MB/day of growth — it ends the deadlock but cannot reverse the storage curve; raising the budget is a spend decision.
- codex-astra-runtime-selection: Codex ACP is upgraded 1.8.0→1.10.0 and its Codex companion 0.153.2→0.153.4 across the canonical install manifest, VM-agent installer, and cf-container runtime image; the sandbox image's CLI-only pin is aligned to 0.153.4. VM-agent now validates both exact executable versions and supplies `CODEX_PATH=codex`, ensuring the adapter launches the explicitly pinned companion rather than a nested dependency resolved relative to itself. An explicit Codex profile model is applied through ACP `session/set_config_option`; rejection now fails session establishment with the requested model in the diagnostic instead of silently retaining the adapter default. A wire-level ACP regression test pins both the successful `gpt-6-astra` request and the fail-closed case. Process fix: `.claude/rules/23-cross-boundary-contract-tests.md` now treats adapter/companion resolution as one runtime contract.
- archive-drain-enable-unfence-and-throughput: (Cadence/budgets below superseded 2026-09-27 by the tripled 18-minute cadence and 2,400,000 daily write budget in PR #2161; the 2026-10-03 hourly billing firebreak, PR #2216, was reverted the same day. Current shipped values: `PROJECT_DATA_ARCHIVE_GLOBAL_SWEEP_INTERVAL_MS=1080000`, `_SWEEP_SESSIONS=8`, `_SWEEP_MESSAGE_BUDGET=10000`, `_WALL_TIME_MS=10000`; see the env-reference skill.) The production archive drain now runs on its own. Root cause of the never-running sweep: the GitHub `production` Environment variable `PROJECT_DATA_ARCHIVE_GLOBAL_SWEEP_ENABLED=false` (set 2026-09-04) replaced the `"true"` PR #2023 checked into `wrangler.toml`, so the deployed Worker kept `false` and every `cron.completed` reported `projectDataArchiveShardingSkipReason: disabled`; that Environment override is deleted as part of shipping this change (a deploy-time action, not a repo file: verify the deployed value per rule 70 before trusting it), and `sync-wrangler-config.ts` now prints every GitHub Environment override that differs from `wrangler.toml` (`listEnvironmentVarOverrides`). A pre-copy refusal no longer fences the session: the root object's `archiveSourcePrepareIntent` RPC RETURNS a typed `ProjectDataArchiveSourcePrepareRefusal` (`prepareArchiveSourceIntentOrRefuse`) for eligibility invariants that reject before any write, and the coordinator (`refusePreCopyMigration`) returns the D1 location to `root` in the same tick and freezes the journal as `precopy_refused` (no retry, no poison, no breaker) instead of leaving `failed` + `migrating`. `selectCandidates`/`selectScopedCandidates` skip a session with such a marker for `PROJECT_DATA_ARCHIVE_PRECOPY_REFUSAL_RETRY_MS` (default 7 days); an explicit session-scoped canary bypasses it. Stats gain `refused`; the problem-migrations list sorts self-healing `precopy_refused` rows after rows that need a human and the frozen-intent inspection skips them; `failed` journals wait `PROJECT_DATA_ARCHIVE_FAILED_RETRY_DELAY_MS` (default 1 h) before a sweep reclaims them so the 3-attempt poison budget spans hours at the shorter cadence; abandon, copy-back and the refusal unwind share one `restoreSessionLocationToRootStatement`; `project_data_archive_candidate_migrated` logs per-phase durations (prepare/copy/seal/manifest/finalize). Throughput was initially shipped at `PROJECT_DATA_ARCHIVE_GLOBAL_SWEEP_INTERVAL_MS=900000` and `PROJECT_DATA_ARCHIVE_WALL_TIME_MS=30000`; after the 2026-09-08 billing firebreak, `wrangler.toml` ships a one-hour cadence, one session per pass, a 5,000-message budget, and a 10s wall budget while the code fallback stays daily / 5s.
- archive-sharding-streaming-hash-abandon-size-budget: Archive sharding can now drain a large root ProjectData object without resetting it. The terminal-version hash (`computeTerminalVersion` → `tableAggregateSha256` in `durable-objects/project-data/archive-sharding.ts`) used to `toArray()` every row of a session per table and digest one string; a 100,000-message session and a tool-heavy 9,906-message session both reset the SAM root object with `Durable Object's isolate exceeded its memory limit`. It now streams pages of `PROJECT_DATA_ARCHIVE_HASH_PAGE_ROWS` (default 500) through `createCanonicalRowsHasher` (`node:crypto` incremental SHA-256), which is byte-identical to the one-shot definition so every recorded proof stays valid; `finalizeSourceDelete` and `rebuildTargetFts` page grouped rows the same way. New superadmin `POST …/archive-sharding/migrations/:migrationId/abandon` (`abandonProjectDataArchiveMigration`) recovers pre-copy migrations that copy-back cannot: it verifies on the root object that the source was never deleted, drops the partial shard copy (`abandonArchiveTargetSession`, which refuses `sealed` targets without that proof) and the source intent (`abandonArchiveSourceIntent`), freezes the journal as `operator_abandoned` without opening the project breaker, and returns the D1 location to `root`. Abandon reserves the journal lease first (`reserveAbandonLease`, epoch bump) and removes the source intent under the root transcript lock BEFORE dropping the shard copy, so a sweep finalizing concurrently cannot leave zero copies; the fence and the ordering are each pinned by a mutation-verified test. Candidate selection is largest-first (`session_summaries.message_count DESC`) under a new per-tick `PROJECT_DATA_ARCHIVE_SWEEP_MESSAGE_BUDGET` (default 20,000; a single oversize session is still selected alone); `PROJECT_DATA_ARCHIVE_SWEEP_SESSIONS` becomes the hard ceiling (default raised 1 → 10); the sweep now journals (fences) each candidate only immediately before processing it (`processArchiveMigrationBatch` `pending`), so a tick that runs out of wall time never leaves unreached sessions fenced `migrating` until the next cadence-gated sweep. `forEachGroupedRowPaged` seeks on the indexed `(created_at, id)` order (a `rowid` seek planned as a per-page sort of the whole session). Process fix: rule 69 gains the memory-ceiling case of harness-ceiling divergence.
- agent-client-version-refresh: Codex, Claude Code, and OpenCode client stacks brought current across every synchronized pin location (`agent-install-manifest.json`, `gateway.go` install constants, `Dockerfile.vm-agent-container`, gateway tests, e2e mock): `@agentclientprotocol/claude-agent-acp` 0.58.1→0.73.0 (bundles claude-agent-sdk 0.3.257; `emitRawSDKMessages`/`_claude/sdkMessage` harness-lifecycle contract verified unchanged), `@anthropic-ai/claude-code` 2.1.258→2.1.260, `@agentclientprotocol/codex-acp` 1.1.2→1.8.0 (CODEX_CONFIG env + `INITIAL_AGENT_MODE=agent-full-access` channels verified still supported; still no CLI `-c` parsing; depends on `@openai/codex@^0.152.0`), `@openai/codex` 0.144.6→0.153.2, `opencode-ai` 1.17.18→1.18.27 (`opencode acp` + `OPENCODE_CONFIG_CONTENT` verified). The implied adapter/companion compatibility was later invalidated: zero-major caret semantics mean `^0.152.0` excludes 0.153.x, so the adapter retained a nested 0.152.x runtime; see `codex-astra-runtime-selection` above. `claudeCodeMinVersion` floor stays 2.1.251. Also caught and fixed silent drift in `Dockerfile.sandbox` (the SandboxDO guided credential-setup image): it pinned claude-code 2.1.258 + codex 0.142.5 and installed the deprecated unpinned `@zed-industries/claude-agent-acp` that no sandbox surface execs — CLIs aligned to the manifest companions, dead adapter removed, and the contract test now pins sandbox codex alignment and bans the Zed adapter's return. `check-agent-install-manifest.ts` matching is boundary-aware (a stale `pkg@1.18.270` can no longer satisfy a `pkg@1.18.27` pin by substring). Existing live containers keep old binaries until restart/new session; cf-container images pick the new versions up at next deploy.
- knowledge-injection-relevance-ranking: Session-start knowledge injection (`get_instructions`) is ranked instead of alphabetical. `getAllHighConfidenceKnowledge` previously used `ORDER BY e.name ... LIMIT 50`, so the cap filtered on entity _spelling_: in production all 50 slots went to `AccountMap`..`AgentReliability`, 46 of them to the one `AgentBehavior` grab-bag, while `ContentStyle`/`CodeQuality`/`User`/`Architecture`/`BusinessStrategy` — entities the same payload tells agents to consult — had never been injected once, with no hint they existed. Now scored by the EXISTING formula (`computeRelevanceScore`: confidence × 1/(1 + age/30d) on `last_confirmed_at`, so `confirm_knowledge` restores rank), mirrored into SQL and pinned by a parity test; per-entity cap via `ROW_NUMBER() OVER (PARTITION BY entity_id)`; `now` bound as a parameter and ties broken to a total order (`score DESC, last_confirmed_at DESC, id ASC`) so output is reproducible. New `getKnowledgeEntityIndex` appends a compact `Name (type, N)` index to `knowledgeDirectives` disclosing what was NOT injected and naming `search_knowledge`/`get_relevant_knowledge` as the retrieval path; it returns `{entries, totalEntities}` (total via `COUNT(*) OVER ()`) so a truncated index can never be labelled "full". The three DO reads run as an isolated `Promise.allSettled` fan-out — a failed ranked read still leaves the index. Limits are clamped at the DO boundary (`clampRowLimit`): a negative `perEntityLimit` would otherwise make `entity_rank <= ?` unsatisfiable and inject nothing project-wide, and `LIMIT -1` means _unbounded_ in SQLite. New env vars `KNOWLEDGE_AUTO_RETRIEVE_PER_ENTITY_LIMIT` (8), `KNOWLEDGE_ENTITY_INDEX_LIMIT` (200). Process fix: `.claude/rules/65-capped-selection-must-rank-and-disclose.md`.
- byo-mcp-servers: Bring-your-own MCP endpoints. Users store `{name, url, authType, token, enabled}` at personal (`/api/mcp-connections`) or project (`/api/projects/:projectId/mcp-connections`) scope; SAM injects them into every agent session alongside `sam-mcp`. Both URL and token are AES-256-GCM encrypted and never returned by a read path (several providers issue pre-signed URLs with the credential in the URL, so the URL is a secret; `url_host` is the display value). Migration `0120_mcp_connections`. `buildSessionMcpServers` is the single composition point, called by `agent-session-bootstrap.ts` (covers VM + cf-container, rule 61) and the manual workspace agent-session route; the anonymous trial path is deliberately pinned to `sam-mcp` only. Resolution skips-and-warns per row so one bad connection cannot brick session start (rules 41/50). vm-agent: `McpServerEntry.Name` (additive, rule 54) plus `ResolveMcpServerNames` as the single naming source of truth — it replaced three drifted copies, one of which named a lone server `sam-mcp-0` for Vibe and `sam-mcp` everywhere else; persistence `migrateV12`. Codex's startup precondition, which required a bearer token for EVERY injected server, is now scoped to the reserved `sam-mcp` entry so a no-auth connection cannot break all Codex sessions. UI: Settings → MCP Servers (personal) and Project Settings → Runtime (project), one shared `McpServersManager`. v1 was bearer/none auth and personal/project scope only; custom headers shipped later (see `mcp-connection-custom-headers`), profile/skill attachment is tracked in idea `01M0QDASJCK3YWVX1GETZTSFWZ`. Limits: `MAX_MCP_CONNECTIONS_PER_SCOPE`, `MCP_CONNECTION_URL_MAX_BYTES`, `MCP_CONNECTION_TOKEN_MAX_BYTES`.
- policy-lifecycle-controls: Project policies gain a shelf life so one-shot workflow policies stop being injected into every session forever. Additive DO migration `034-policy-lifecycle-controls` adds `expires_at INTEGER` (nullable — `NULL` means never expires, preserving every existing policy's behaviour) and `scope TEXT NOT NULL DEFAULT 'always'` (`'always' | 'task'`). `getActivePolicies` filters at READ time (`active = 1 AND (expires_at IS NULL OR expires_at > ?)`) — no sweep, no cron; the row is retained and stays `active` so `get_policy` / `list_policies` / the Policies tab still show a human why a policy stopped applying, and the per-project cap COUNT excludes expired rows. A `scope: 'task'` policy MUST carry an `expiresAt`, enforced by one shared `validatePolicyLifecycle` at all three write boundaries (MCP `policy-tools.ts`, REST `routes/policies.ts`, sam-session `tools/add-policy.ts`) plus a DO-level choke point. `add_policy` / `update_policy` accept `scope` + `expiresAt`; `expiresAt: null` on update clears an expiry. Expiring policies render an inline `(task-scoped, expires YYYY-MM-DD)` annotation in `policyDirectives`, and the capture instruction now tells agents to scope dated work. New limit `POLICY_MAX_EXPIRY_MS` (default 365 days). Also hardened `scripts/quality/check-do-migration-safety.ts`, which extracted only backtick and single-quoted SQL — a `sql.exec("DROP TABLE ...")` in double quotes was invisible to the gate and reported PASS.
- claude-fable-51-model-catalog: Claude Fable 5.1 (`claude-fable-5-1`, released 2026-09-01, $10/$50 per MTok, native 1M context) added to both canonical model lists — `CLAUDE_MODELS` dropdown catalog and `PLATFORM_AI_MODELS` proxy allowlist/pricing. Claude Code CLI pinned to `@anthropic-ai/claude-code@2.1.258` because Fable 5.1 requires Claude Code 2.1.251 or newer; the VM-agent installer now validates the underlying `claude` binary instead of treating `claude-agent-acp` alone as sufficient. Focused tests pin dropdown presence, platform metadata, and the no-`[1m]` selector rule for native 1M Claude 5-family models.
- report-issue-idea-flow: Hosted "Report an Issue" flow that groups reports into private feedback incidents and creates/updates a linked draft Idea in the effective private feedback project (Admin → Integrations runtime setting, falling back to `PLATFORM_FEEDBACK_PROJECT_ID`). Two entry points: the session tool rail's Report action and ErrorBoundary crash screen. Users explicitly consent before technical refs (sessionId, taskId, nodeId) are attached. Server-side cross-tenant ref authorization validates project membership before storing references. User text sanitized with secret/PII redaction and fenced with provenance markers. Feature auto-hidden when no effective feedback project exists or it does not reference an existing project in the current deployment database. Configurable limits: `REPORT_ISSUE_TITLE_MAX_LENGTH`, `REPORT_ISSUE_DESCRIPTION_MAX_LENGTH`, `REPORT_ISSUE_CONTENT_MAX_LENGTH`.
- claude-opus-5-model-catalog: Claude Opus 5 (`claude-opus-5`, released 2026-07-24, $5/$25 per MTok, native 1M context) added to both canonical model lists — `CLAUDE_MODELS` dropdown catalog and `PLATFORM_AI_MODELS` proxy allowlist/pricing. Retired `claude-sonnet-4-20250514` pruned; stale defaults bumped to `claude-sonnet-5` (`DEFAULT_SAM_MODEL` — was pointing at the retired model, `DEFAULT_AI_PROXY_ANTHROPIC_MODEL`, `DEFAULT_TRIAL_MODEL_PRODUCTION`). New regression tests pin retired-Anthropic-model exclusion and default↔catalog registration; process rule `.claude/rules/52-model-catalog-lifecycle.md`.
- dispatch-task-runtime-routing: MCP `dispatch_task` now honors explicit and skill/profile `runtime` selection. Explicit `cf-container` decisions launch task-mode Instant sessions asynchronously without VM credential/quota gates or duplicate chat persistence; the Instant branch re-verifies repository owner access (`requireRepositoryOwnerAccess`) before launching, matching the VM branch; async Instant launch failures mark the task failed via the shared queued-guarded transition (`markQueuedTaskFailed`) instead of stalling `queued` until the stuck-task cron; VM-only arguments conflict with container runtime instead of silently downgrading; responses expose the effective runtime and decision reason, and `get_task_details` returns the task's chat `sessionId` once the async session exists.
- origin-tag-injected-messages: SAM-injected prompt text (e.g. the `get_instructions` reminder) is no longer concatenated into the visible user prompt. It is passed separately as `injectedInstructions` on the vm-agent create-session HTTP body (`startAgentSessionOnNode` → `buildInitialPromptParams`), emitted as a second ACP prompt block tagged `_meta["sam.origin"]="system"`. The vm-agent honors that marker ONLY from a trusted control-plane prompt (`HandlePrompt(..., trustedSource)`) — browser viewer prompts have it stripped so a user cannot hide their own content. Origin propagates through the messagereport outbox (new `origin` column), the ProjectData DO `chat_messages` table (additive migration `024-chat-message-origin`, NULL/absent = `user`), `ChatMessageResponse`, and the live `session/update` broadcast. The web collapses `origin=system` user messages behind a `<details>` "Show system context" disclosure (`AcpConversationItemView`). `origin=system` content is excluded from user-content dedup, LIKE/FTS search (materialization filters it out of `chat_messages_grouped`), topic auto-capture, and attention resolution.
- chat-full-load-timeline-jump: (Initial full load superseded 2026-09-27 by project-chat-instant-switching, which loads the newest page first.) Project chat loads the FULL conversation on open (`getSessionMessageLimit` split into a small default page size `CHAT_SESSION_MESSAGE_LIMIT` for the 3s poll/load-more and a large `CHAT_SESSION_MESSAGE_MAX` ceiling the initial load requests; new `DEFAULT_CHAT_SESSION_MESSAGE_MAX`). The 30 MiB RPC size guard + `hasMore` "Load earlier" remain the fallback for oversized sessions. Timeline jump-to-message now anchors by messageId+timestamp with a `loadUntil` fallback (no more dead clicks), every entry kind (user message, status update, activity) is clickable, and the jumped-to message flashes (`.sam-message-highlight`). Removed the now-dead `messageIndex`/`messageIndexMap` from the timeline.
- skills-profile-override-layer: Skills as a first-class profile-override layer (skill → profile → project → platform); additive migration `0063_skills.sql` adds `skills`, `skill_runtime_env_vars`, `skill_runtime_files` and `tasks.skill_id`/`tasks.skill_hint`/`triggers.skill_id`; shared mappers in `profile-fields.ts`/`profile-runtime-assets.ts`; CRUD + runtime routes under `/api/projects/:projectId/skills`; resolution wired through task/trigger/MCP/dispatch + workspace runtime asset injection; Skills UI + chat/task skill selectors in `apps/web`
- explicit-sam-provider-selection: Require explicit opt-in to SAM as AI provider via `providerMode: 'sam'`; three-mode agent auth (user-api-key, oauth, sam); AI proxy auth gate on all endpoints including /models
- compact-mode-lazy-load-tool-content: Chat compact mode strips tool content from RPC payload (80-90% reduction); lazy-loads on expand
- harness-track-d-integration-design: SAM-native harness architecture doc; Gemma 4 26B as default Workers AI model
- ai-proxy-universal-tracking: URL-path-based passthrough proxy for usage tracking without consuming auth headers
- user-ai-budget-controls: User-facing daily token budgets + monthly cost cap with 3-tier resolution
- anthropic-proxy-endpoint: Native Anthropic Messages API proxy through AI Gateway with Unified Billing
- user-ai-usage-dashboard: Per-user LLM usage dashboard from AI Gateway logs (by model, by day)
- cost-monitoring-dashboard: Admin cost dashboard aggregating LLM + compute costs with projections
- sam-observability-context-tools: SAM tools for searching task messages and browsing project codebases
- sam-agent-phase-a-tools: SAM orchestration tools (dispatch_task, create_mission, get_task_details)
- policy-propagation-phase4: Project policies with MCP tools + propagation to child tasks
For the latest changes, also check `git log --oneline -20`.
- **durable-project-eventing**: Operational same-chat event delivery, persisted one-off schedules and bounded standing watches, agent channels with atomic history/follow, and member project/session Events controls. Canonical source outbox covers configured lifecycle/GitHub CI/review/authenticated-webhook facts; credential-limit events use actual server-selected attribution. Shared mailbox/recovery guards preserve cancellation and membership authority. Retention uses incremental accounting and bounded durable orphan-scan progress. Delivery receipts and schedule admission remain separate from agent action.
- **session-visibility**: VM Agent enriches session responses with live `hostStatus` and `viewerCount`; shared `AgentHostStatus` type; new `listAgentSessionsLive()` browser API; enhanced status colors in workspace UI
- **devcontainer-runtime-user-consistency**: VM Agent resolves effective devcontainer execution user when `CONTAINER_USER` is unset; detection order: `devcontainer read-configuration` > `devcontainer.metadata` label > `docker exec id -un` fallback; per-workspace `ContainerUser` stored in runtime
- **workspace-runtime-identity**: VM Agent derives host workspace identity from canonical `workspaceId`; strict workspace label matching prevents cross-workspace routing
- **sam-workspace-env**: SAM platform metadata injected as env vars into devcontainers during bootstrap (`SAM_WORKSPACE_ID`, `SAM_NODE_ID`, `SAM_WORKSPACE_URL`, `SAM_API_URL`, `SAM_REPOSITORY`, `SAM_BRANCH`)
- **worktree-context**: Deep git worktree integration across VM agent, API, shared types, terminal, and workspace UI; worktree-aware terminal/chat session creation; `WorktreeSelector` UI
- **node-system-info**: VM Agent sysinfo package collects metrics from Linux procfs and Docker CLI; two-tier collection (quick for heartbeat, full for `/system-info` endpoint); Node detail page redesigned with composed section components
- **devcontainer-named-volumes**: Replaced bind-mount storage with named Docker volumes (`sam-ws-<workspaceId>`); eliminated permission normalization
- **command-palette-search**: Enhanced command palette with fuzzy file search and tab switching; camelCase-aware fuzzy matching
- **tab-reorder-rename**: Unified tab ordering, rename, and drag-and-drop reordering; useTabOrder hook with localStorage persistence
- **persistent-agent-sessions**: Agent processes survive browser disconnects; SessionHost owns agent lifecycle independently; fan-out message broadcasting; bounded message ring buffer for late-join replay
- **workspace-keyboard-shortcuts**: VS Code-style keyboard shortcuts; centralized shortcut registry with platform-aware matching
- **agent-settings-acp-protocol**: Wire agent settings through ACP protocol; all 5 permission modes exposed
- **comprehensive-lifecycle-logging**: End-to-end ACP/WebSocket lifecycle observability via CF Workers
- **vm-agent-error-reporting**: Go errorreport package with batching and periodic flushing
- **client-error-reporting**: Client-side error reporting pipeline with batched sends
- **agent-file-ops**: ACP ReadTextFile and WriteTextFile handlers via docker exec
- **file-browser**: File browser with directory listing and syntax-highlighted file viewer
- **git-changes-viewer**: GitHub PR-style git changes viewer with unified diff
- **voice-to-text**: Voice input with Workers AI Whisper transcription
- **014-multi-workspace-nodes**: First-class Nodes with multi-workspace hosting, async provisioning, workspace recovery
- **013-agent-oauth-support**: Dual credential support for Claude Code (API key + OAuth token)
- **012-pty-session-persistence**: PTY sessions survive page refresh with ring buffer replay