Skip to content

Helm release and minor changes to auth deploy optio#530

Closed
jplorier wants to merge 562 commits intojonwiggins:mainfrom
jplorier:main
Closed

Helm release and minor changes to auth deploy optio#530
jplorier wants to merge 562 commits intojonwiggins:mainfrom
jplorier:main

Conversation

@jplorier
Copy link
Copy Markdown

@jplorier jplorier commented May 5, 2026

Summary

I'm adding pipelines to publish the helm chart so it can be consumed directly.
Some fixes to route and other to get optio running

jonwiggins and others added 30 commits April 2, 2026 17:10
Private repos failed to clone because GITHUB_TOKEN was not being
resolved from the secrets store — it wasn't in the agent adapter's
requiredSecrets list. The git clone would hang waiting for credentials,
causing a 120s timeout.
Repos without CI always had checksStatus "none", which blocked
auto-merge since it only triggered on "passing". Now treats "none"
as passing so PRs on repos without CI merge automatically.
Two fixes:

- Show the OAuth token paste input when the existing token is expired.
  Previously the expired warning and paste input were in mutually
  exclusive if/else branches, so users saw "token expired" but had no
  way to paste a replacement.

- Skip 409 errors when saving repos that already exist from a previous
  setup run instead of showing a generic "Failed to save repos" error.
The BFF proxy's fetch() followed OAuth redirects server-side instead of
passing them to the browser. This caused three issues:

- Login redirect went to the API pod hostname instead of GitHub
- Auth callback redirect used the pod hostname instead of PUBLIC_URL
- Session cookie had the Secure flag set on HTTP (from NODE_ENV=production
  in the Next.js build), causing browsers to reject it

Fix by using redirect:"manual" for auth routes in the BFF proxy, using
PUBLIC_URL for redirect base URLs in the auth callback, deriving the
Secure cookie flag from the PUBLIC_URL protocol, and injecting PUBLIC_URL
into the web pod via Helm.
When a GitHub App is configured at deployment level, the setup wizard
now detects it via GET /api/github-app/status and:

- Shows a success message instead of the PAT input on the GitHub step
- Allows proceeding without entering a personal access token
- Lists repos using the GitHub App installation token
- Falls back to installation token for repo validation

The backend setup endpoints now resolve tokens in order:
user-supplied PAT → stored GITHUB_TOKEN secret → GitHub App installation
token.
Adds a per-repo "Cautious Mode" toggle that opens draft PRs and
disables auto-merge, keeping a human in the loop for all merges.
Auto-resume and review agents continue to work normally.
The single-pass regex broke when GitLab support introduced nested
conditionals. Process innermost blocks first, iterating until all
nesting is resolved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a guided workflow for rotating/replacing an expired GITHUB_TOKEN
secret. Previously, when the token expired, PR watching, issue sync,
and repo detection would silently fail with no way to diagnose or fix
the issue from the UI.

Changes:
- New API routes: GET /api/github-token/status (validates stored token
  against GitHub API) and POST /api/github-token/rotate (validates new
  token then atomically replaces the stored secret)
- New GitHubTokenManager component in Settings page showing token
  health status with clear visual indicators and a guided replacement
  flow
- API client methods for the new endpoints
- 9 tests covering status checks and rotation scenarios

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…oded message

The review summary UI showed "Agent completed successfully" instead of the
agent's actual findings. Root causes:

- ClaudeCodeAdapter.parseResult() hardcoded summary text, ignoring the result event
- agent-event-parser truncated tool_result to 300 chars, cutting off review JSON
- parseReviewOutput() fallback was ineffective for generic summaries

Also includes:
- Review draft cleanup on force-redo to prevent stale summaries
- Increase DEFAULT_MAX_TURNS_REVIEW from 10 to 30
- Add turn budget warning to review prompt template
- Rename githubReviewUrl to reviewUrl for platform-neutral naming

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Switch to searchForIssuesUsingJqlEnhancedSearch (cursor-based pagination)
- Smart repo URL matching: match ticket repo field against configured repos
- Support full URLs, owner/repo paths, and repo: labels in Jira tickets
- Add/delete ticket provider UI in settings page
- Add deleteTicketProvider API client method

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Skip idle pod cleanup when active interactive sessions exist
- Show clear error message when session pod was cleaned up due to inactivity
  instead of generic "Pod not found or not ready"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When web and API run on different origins (e.g. NodePort with separate
ports), WebSocket connections failed because ws-client derived the URL
from the page origin instead of the API origin.

- Inject PUBLIC_API_URL at runtime via window.__OPTIO_CONFIG in layout.tsx
- ws-client reads it to derive the correct ws:// or wss:// URL
- Helm web-deployment auto-sets PUBLIC_API_URL for NodePort configurations
- Fix PGDATA path for postgres to prevent initdb issues on restart

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In local dev (Docker Desktop K8s), all traffic shares a single gateway
IP. The previous limit of 10 was too low for normal multi-tab usage
(6+ WS connections per tab), causing constant "connection limit exceeded"
errors. Rejected connections triggered a reconnect storm since the client
retried every 3s regardless of close code.

- Raise MAX_WS_CONNECTIONS_PER_IP from 10 to 50
- Skip auto-reconnect on close code 4429 (limit exceeded)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: raise WebSocket per-IP connection limit and stop retry on 4429
…-flow

fix: pass OAuth redirects through BFF proxy to browser
…github-app

feat: skip GitHub PAT step in setup wizard when GitHub App is configured
fix: setup wizard UX improvements for token expiry and existing repos
fix: prevent pod cleanup for active interactive sessions
- Escape </script> sequences in runtime config JSON to prevent XSS
- Remove leaked globalThis.__OPTIO_CONFIG in ws-client test cleanup
- Remove unscoped PGDATA change (belongs in its own PR)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jplorier and others added 28 commits April 24, 2026 22:29
…ins#496)

Co-authored-by: Ramesh Nethi <r.nethi@gogatewayai.com>
…ts (jonwiggins#499)

Follow-ups from review of jonwiggins#496:

- Add `users.test.ts` covering 200/400/401/403/404 paths for the new
  `GET /api/users/lookup` route.
- Move the duplicate-member check into `workspaceService.addMember`:
  swap `onConflictDoUpdate` for `onConflictDoNothing + returning` so a
  stale local member list can no longer silently overwrite a member's
  role. Route now returns 409 when the user is already a member.
- Tighten `UserLookupResponseSchema.email` to `z.string().email()` to
  match the querystring validator.
- Workspace settings: drop the client-side duplicate-member precheck
  (server enforces it now) and stop hardcoding "User not found" as the
  toast description fallback.

Refs: jonwiggins#496

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
…oggle (jonwiggins#503)

The setup wizard was hardcoding ANTHROPIC_API_KEY, OPENAI_API_KEY,
GEMINI_API_KEY, and CLAUDE_CODE_OAUTH_TOKEN at user scope. Background
runs (GitHub ticket sync, scheduled triggers, webhooks) have no user
context, so the user-scoped lookup is skipped in resolveSecretsForTask
and these credentials are never found, putting the agent into an error
loop on first ticket sync.

- Setup wizard: default agent credentials to global scope when the
  caller has admin role (or auth is disabled), user scope otherwise.
- Setup wizard: add a user-vs-global radio with copy explaining that
  user scope is invisible to background runs. Global is disabled with
  an explanatory hint for non-admins.
- /secrets: render a User icon and "User-only" label for user-scoped
  secrets, and show an info banner explaining the same caveat when
  any user-scoped secret exists.

Closes jonwiggins#501
Phase 1 of issue jonwiggins#497 (community/marketplace skills). Skills can now
target specific agent types (claude-code, codex, copilot, gemini,
opencode) and ship as a directory of files rather than a single
.claude/commands/<name>.md, matching Claude Code's SKILL.md convention
and unblocking marketplace-style multi-file payloads in a follow-up.

- custom_skills gains layout (commands | skill-dir), files (jsonb),
  agent_types (jsonb). Existing rows default to commands + unbounded
  scope so behavior is preserved.
- getSkillsForTask now takes agentType and filters skills whose
  agent_types is set; the per-name dedupe runs after filtering so a
  more specific repo skill that doesn't match the agent doesn't hide
  a matching global one.
- buildSkillSetupFiles emits .claude/skills/<name>/SKILL.md plus
  sanitized extra files for skill-dir; commands layout is unchanged.
- task-worker and pr-review-worker pass the executing agentType.
- Settings UI gains agent-type chips, layout selector, and an extra-
  files editor; rows show layout and agent-scope badges.
Reviews are no longer hard-wired to Claude Code. The review agent type
and model now resolve through an inheritance chain (per-repo override →
repo's defaultAgentType → global default → "claude-code") so a Gemini-only
user just sets defaultAgentType=gemini once and reviews follow.

A single resolver in services/review-config.ts is the source of truth for
both review entrypoints (review-service for subtask reviews on Optio-opened
PRs, pr-review-worker for external PR review runs).

Schema: adds repos.review_agent_type and optio_settings.default_review_*
columns, all NULL by default; existing repos keep current behavior.

UI: replaces the Claude-only model dropdown on /repos/new, /repos/[id],
and /settings with a two-step picker (agent → model). Per-repo pages get
an "Inherit from repo default" option; the API returns the resolved
effective values so the UI can show "Reviews will run with: gemini ·
gemini-2.5-pro" hints.

Closes jonwiggins#500

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of issue jonwiggins#497. Lets users install Claude Code skills from any
public git URL (Anthropic's marketplace, internal repos, gists, ...) and
have the API fetch + materialize them into the agent worktree at
.claude/skills/<name>/ on every Claude Code task. Sync is
content-addressable and reproducible: the user pins a ref, the worker
resolves it to a SHA, and the row records that SHA so task setups stay
deterministic until the user advances the ref.

- New installed_skills table records source_url, ref, resolved_sha,
  subpath, and per-row scoping (workspace, repo, agent_types).
- skill-sync-worker (BullMQ) shallow-clones source@ref, parks the
  contents in /opt/optio/skills-cache/<sha>/, parses SKILL.md
  frontmatter, sums sizes, and flags executable scripts. Multiple rows
  pinned to the same SHA share one cache entry. Eager sync on install
  + force-sync via POST /:id/sync; a 5-min sweep catches anything
  missed.
- /api/installed-skills CRUD + /:id/sync endpoint, both Zod-validated.
  Subpath traversal (..) is rejected; skill names must be slug-shaped.
- task-worker and pr-review-worker copy cached files into the
  worktree's .claude/skills/<name>/ when agentType is claude-code,
  using the new contentBase64 setupFiles variant so binary scripts
  round-trip cleanly.
- agent-entrypoint.sh decodes contentBase64 to bytes when present,
  preserving the existing text-content path for everything else.
- Helm: optional persistentVolumeClaim mounted on the API deployment at
  /opt/optio/skills-cache (installedSkillsCache.enabled, default off
  for fresh installs and local dev). Falls back to emptyDir, which
  re-clones on every restart but otherwise works.
- Dockerfile.api: install git + ca-certificates so the worker can
  actually clone.
- Settings UI gets a "Marketplace Skills" section with install dialog
  (URL/ref/subpath/agentTypes), per-row sync button, last-synced and
  resolved-SHA badges, and an executable-scripts warning chip.

Smoke-tested end-to-end against a public test repo on the local cluster:
sync resolves SHA, populates the cache PVC, and a second skill at the
same source@ref hits the cache without re-cloning. Task-worker
materialization verified by calling getInstalledSkillsForTask +
readInstalledSkillFiles inside the deployed pod.
…jonwiggins#510)

* feat(persistent-agents): backend foundation — schema, state machine, worker

Introduces a third Task tier alongside Repo Tasks and Standalone Tasks:
long-lived, named, message-driven agent processes that halt after each turn
and wake on user/agent/webhook/cron/ticket events. See
docs/persistent-agents.md (added in a follow-up commit).

Schema (1777200001_persistent_agents.sql):
- persistent_agents (slug, system_prompt, agents_md, initial_prompt,
  pod_lifecycle, idle_pod_timeout_ms, sticky_pod_id, max_turns, etc.)
- persistent_agent_turns (per-turn record with cost, halt_reason, session_id)
- persistent_agent_turn_logs
- persistent_agent_messages (inbox: pending/processed)
- persistent_agent_pods (per-agent pods with keep_warm_until)
- workflow_triggers gains target_type='persistent_agent' (no migration —
  column was already free-form text)

State machine: cyclic (idle → queued → provisioning → running → idle), with
paused/failed/archived as branches. After consecutive_failure_limit failed
turns, the agent transitions to FAILED and requires manual resume.

Pod lifecycle modes (configurable per agent):
- always-on : pod runs until paused/archived
- sticky    : pod kept warm idle_pod_timeout_ms after each turn (default)
- on-demand : cold-start every turn

Reconciler integration: new RunKind 'persistent-agent' with snapshot
builder, pure decision function (reconcilePersistentAgent), and CAS-gated
executor applicators (transition/enqueueTurn/patchStatus). Producers wake
the reconciler on message arrival, trigger fire, control intent, and
turn completion.

Worker (persistent-agent-worker.ts) drives one turn per job: drains pending
messages into a structured prompt with OPTIO MESSAGE envelopes, acquires a
pod via the lifecycle-aware pool service, invokes the agent runtime CLI,
streams logs, parses cost/halt, and transitions the agent back to idle —
or escalates to FAILED if consecutive failures exceed the limit.

* feat(persistent-agents): HTTP routes, WS events, inter-agent API, triggers

API routes:
- /api/persistent-agents — full CRUD + control intent (pause/resume/archive/restart)
- /api/persistent-agents/:id/messages — POST wakes the agent; GET lists recent
- /api/persistent-agents/:id/turns[/:turnId] — turn history with logs
- /api/internal/persistent-agents/{send,broadcast,inbox,/} — agent-callable
  HTTP API used by PAs from inside their pods. Auth via OPTIO_AGENT_TOKEN
  bearer (the agent's own UUID for v0.4 — to be hardened with signed tokens
  in a follow-up). Agents learn the API from their agents.md operator manual,
  Scion-style.
- WebSocket /ws/persistent-agents/:agentId/events — state changes, turn
  started/halted, messages, and turn logs with catch-up history.

Trigger system:
- workflow-trigger-worker dispatches target_type='persistent_agent' by
  formatting the cron payload as a system message and calling wakeAgent().
- /hooks/:webhookPath now resolves persistent_agent triggers and pipes the
  POST body into the agent's inbox.

Worker env injection:
- OPTIO_AGENT_TOKEN + OPTIO_API_URL added to every PA turn so the agent
  can call /api/internal/persistent-agents/* from inside the pod.

* feat(persistent-agents): web UI, demo, docs

Web UI at /agents:
- List page with state badges, runtime, lifecycle mode, lifetime cost
- Create page with system prompt + agents.md (with sensible default operator
  manual), pod lifecycle picker, failure threshold
- Detail page with three tabs:
  - Chat: inbox column + live activity column, message composer (⌘+Enter)
  - Turns: collapsible per-turn entries with logs and cost
  - Config: rendered system prompt / agents.md / initial prompt
  Live updates via the new /ws/persistent-agents/:id/events stream.
  Pause / Resume / Restart / Archive / Delete controls.
- Sidebar: "Agents" entry under Run, between Tasks and Sessions

Demo: demos/the-forge/
- Four PAs (Vesper, Forge, Sentinel, Chronicler) — an engineering team
  that decomposes feature requests, drafts code, reviews, and journals.
- Each agent ships with system_prompt, agents.md operator manual, and an
  initial_prompt. Mix of pod lifecycle modes — Chronicler is always-on
  (high-frequency listener), the rest are sticky.
- ./demos/the-forge/setup.sh provisions all four into the running API
  (idempotent — safe to re-run).

Docs:
- docs/persistent-agents.md — full design doc with mental model, lifecycle,
  pod modes, message envelope format, inter-agent API, wake sources, and
  open follow-ups.
- docs/tasks.md — promoted to "Repo Tasks, Standalone Tasks, and Persistent
  Agents" with cross-link.
- CLAUDE.md — Persistent Agent description added under Tasks tier.
- CHANGELOG.md — v0.4.0 unreleased entry.

* feat(examples): reorg demos→examples + Mars Mission Control demo

Layout:
- examples/                       (was demos/)
  ├── README.md                   navigation across all three Task tiers
  ├── repo-tasks/                 stub README with planned examples
  ├── standalone-tasks/           stub README with planned examples
  └── persistent-agents/
      ├── README.md               authoring guide
      ├── forge/                  (moved from demos/the-forge/)
      └── mars-mission-control/   NEW

Mars Mission Control — 7-agent persistent-agent community:
- Clock fires on a schedule trigger every N minutes (default 10)
- On each tick: advances /workspace/sol-counter, broadcasts the next sol's
  scenario verbatim from its system-prompt playbook (Athenaeum-style)
- Director, Trajectory, Comms, Life Support, Geology, EVA coordinate the
  response — only specialists whose domain is implicated chime in
- Director synthesizes each sol into /workspace/mission-log.md

Five sols cover: touchdown, dust storm, water-recycler fault decision,
surface anomaly go/no-go, departure. Each illustrates a different
collaboration pattern (status-roundup, parallel mitigation, decision
under uncertainty, cross-discipline negotiation, orderly close).

Pattern showcased: scheduled-trigger-as-forcing-function. Real production
agent communities are usually driven by external events (monitors,
schedules, webhooks) rather than human chat — Mars makes that legible.

Backend: added GET/POST/DELETE /api/persistent-agents/:id/triggers so the
demo's setup.sh can attach the Clock's schedule trigger via API.

* fix(persistent-agents): four bugs surfaced by running Mars demo end-to-end

1. **Reconcile worker dispatch missed persistent-agent kind.** The kind→decider
   ternary fell through to reconcileStandalone, which noop'd with 'wrong kind'.
   Added the third branch + extended the resync sweep to enqueue PA reconciles.

2. **CAS guard couldn't match microsecond-precision timestamps.** Postgres
   timestamptz columns store microseconds; JS Date is millisecond-precision.
   For new rows whose updated_at was set by PG's now() (via Drizzle's
   defaultNow()), the round-tripped JS Date never matched the DB value.
   Tasks/workflows weren't affected because their updated_at gets stomped by
   `new Date()` writes from JS code on creation. PA rows weren't touched by
   JS until first reconcile, so the first CAS deadlocked in a stale-retry loop.
   Fix: compare with `date_trunc('milliseconds', col) = date_trunc(...)` in
   both reconcile-executor's casUpdate (covers all kinds) and the service's
   transitionPersistentAgentState. Drizzle won't serialize Date params
   inside raw `sql` templates correctly either, so passed `version.toISOString()`
   explicitly.

3. **drainMessagesIntoTurn used a broken IN-clause.** `sql\`id = ANY(${ids}::uuid[])\``
   expanded each array element into a separate parameter, generating
   `ANY(($4, $5, $6)::uuid[])` — a tuple cast, not an array. Switched to
   drizzle's `inArray()` which produces the expected `id IN (...)`. This was
   blowing up first-turn drains, escalating PAs to FAILED.

4. **Transitions didn't enqueue follow-up reconciles.** A successful PA
   transition (e.g. PAUSED→IDLE on resume) left pending inbox messages
   undrained because nothing nudged the reconciler to re-evaluate. Added a
   post-transition enqueueReconcile so the loop progresses without waiting
   for the 5-min resync sweep.

Plus: setup.sh's `${ARRAY[@]}` failed under `set -u` when no auth token was
set. Switched to `${ARRAY[@]+"${ARRAY[@]}"}` to no-op cleanly when empty.

* fix(mars-demo): Clock pod must be sticky, not on-demand

Clock writes /workspace/sol-counter to track the current sol number and
reads it on each cron-driven turn. With on-demand lifecycle the pod
cold-starts every turn, /workspace is empty, the counter resets to 0,
and Clock keeps re-broadcasting Sol 1 forever — burning credits and
never advancing the mission.

Switching to sticky with a 30-min idle window (long enough to survive
the cron interval) fixes the regression. Same pod, same /workspace,
counter persists across the gap.

* feat(ux): split tabbed /tasks hub into dedicated pages

The legacy /tasks page hosted four tabs (Repo Tasks / Standalone / Issues /
PRs) competing with the sidebar. Now that the sidebar exposes Jobs, Reviews,
and Issues as first-class destinations, the tab layer is redundant.

- /tasks now renders just the Repo Task list (no tabs, focused header)
- /jobs becomes a real list page using StandaloneList instead of redirecting
- /reviews becomes a real list page using PrBrowser instead of redirecting
- /issues is new, hosting the IssuesBrowser extracted from /tasks
- Sidebar gains Issues under Run; final shape is
    Run    Tasks · Jobs · Reviews · Issues · Scheduled
    Live   Agents · Sessions
- Back-compat: /tasks?tab=standalone|issues|prs client-side redirects to the
  new dedicated pages so existing bookmarks keep working.
- Each new page has a one-line subtitle telling the user what the section is
  for, since the sidebar label alone isn't always self-explanatory.

The IssuesBrowser was inlined as a 220-line function inside /tasks/page.tsx;
extracted to apps/web/src/components/issues-browser.tsx as its own component.

* feat(ux): unify list pages with shared PageHeader / StatusDot / EmptyState

Six list pages (Tasks, Jobs, Reviews, Issues, Agents, Sessions) all set their
own header padding, title classes, button shapes, empty-state markup, and
status-color maps. Result: each page felt slightly off from its neighbors.

This pass extracts three shared primitives and applies them everywhere:

- PageHeader (apps/web/src/components/page-header.tsx)
  - icon glyph + title + description + meta row + action cluster
  - icon mirrors the sidebar icon for the same destination, creating an
    unmistakable "you are here" mark across nav and page
  - gradient-divider hairline anchors the bottom of every header for
    consistent rhythm

- StatusDot / StatusPill (apps/web/src/components/status-dot.tsx)
  - one canonical state→color map covering Task, Workflow, PA, Session,
    PR Review, and pod states
  - same hue means the same thing everywhere in the app

- EmptyState (apps/web/src/components/empty-state.tsx)
  - identical icon framing, dashed border, vertical rhythm

Other tightening:
- All cards on Agents / Issues / Sessions list now use the existing
  .card-hover utility for the same lift-on-hover treatment as Standalone
  cards. They also gain hover:border-primary/30 to match Sessions.
- Sessions filter chips moved into the PageHeader meta row instead of a
  competing tab strip below the header — eliminates the only remaining
  inline-tab UI on a list page.
- Action button styles harmonized across the six pages (rounded-lg,
  px-3 py-2, text-sm primary / text-xs secondary, same border + bg).

No behavior changes — purely visual.

* feat(ux): use shared LogViewer for agent live + per-turn log panels

The agent detail page rendered its own bespoke log views — a "Live activity"
column in the chat tab and per-turn collapsibles in the turns tab — both as
inline divs that duplicated about 60 lines of styling and missed everything
LogViewer already gives the rest of the app: tool-call grouping, search,
type filter, thinking toggle, results toggle, time-gap dividers, export,
copy, virtualization-friendly scrolling.

Now the agent detail uses the same LogViewer component as Tasks, Jobs, and
Reviews. Two new hooks adapt the data sources:

- useAgentLiveLogs(agentId, currentTurnId?)
  WebSocket-only stream of persistent_agent:log events. Resets on
  turn_started so the live view stays anchored to the current turn rather
  than accumulating forever.

- useAgentTurnLogs(agentId, turnId)
  REST fetch of one turn's historical logs. Each open turn renders an
  ExpandedTurnLogs child that owns its own hook instance — collapse cleans
  up automatically.

Both hooks return the same { logs, connected, capped, clear } shape as
useLogs / useWorkflowRunLogs so they slot straight into LogViewer's
externalLogs prop with no special-casing.

Trims ~80 lines of bespoke rendering from the agent page and fixes a real
bug: the old turn-expand fetched once and cached — it never reflected logs
that arrived after the turn was first expanded. The new hook re-fetches on
each open.

* feat(ux): collapse SessionChat into LogViewer + composer/status slots

LogViewer gains three optional render slots:
  - status   — sticky strip ABOVE the toolbar (caller's connection state,
                model picker, "Thinking…", cost meter)
  - composer — sticky footer BELOW the log content (chat input)
  - emptyMessage — override the default "Waiting for output…" copy

This lets the log render be the same widget across every agent surface
in the app — Tasks, Jobs, Reviews, Agents, AND now Sessions — without
forcing the data sources to look the same.

SessionChat dropped from ~600 lines of bespoke chat-bubble rendering to
~150 lines of composer + status + LogViewer. The 400 lines of removed
code were duplicate event-grouping, tool-call rendering, search wiring,
auto-scroll, time formatting — all things LogViewer already does.

The session WebSocket adapter is the new useSessionLogs hook: opens
/ws/sessions/:id/chat, normalizes ChatEvent → LogEntry, and exposes
sendMessage / interrupt / setModel / status / costUsd to the chat shell.
ChatEvent.type maps 1:1 to LogEntry.logType so LogViewer's existing
tool-call grouping, thinking toggle, and time-gap rendering work
unchanged.

User-typed messages still get the inline "You: …" annotation that
LogViewer already supported via userMessages.

Visual changes that come for free vs the old SessionChat:
  - tool calls grouped with their results (used to be flat)
  - search (Ctrl+F) over the conversation
  - filter by event type
  - thinking / results toggles
  - time-gap dividers between bursts
  - export to JSON / text / Markdown (when ExportLogs has a taskId — N/A
    for sessions today; future work)

* fix(log-viewer): interleave user messages with log groups by timestamp

User-typed messages were rendered in their own block AFTER the log-groups
loop, so every message you sent appeared at the bottom of the viewer
instead of being interleaved chronologically with the agent's response
events. Reproduces on Sessions and any task with chat where messages and
agent output overlap in time.

Build a single timeline that merges groups and userMessages, sort by
timestamp, then render in one pass. Time-gap dividers compute their gap
relative to whichever item came before — group-to-group, group-to-user,
user-to-group all work the same way.
The sidebar's Issues link 404'd because the /tasks split (9e2ac87) left
the page without a route, and a stale top-level `issues/` .gitignore rule
(now scoped to /issues/) was hiding the new app dir.

Restore the page and extend it: in addition to GitHub/GitLab issues from
configured repos, /api/issues now fans out to enabled ticket_providers
(Linear, Jira, Notion), normalizing each into the same envelope with a
`source` discriminator. Tasks are matched back to external tickets via a
source+externalId lookup (their IDs are globally unique within a source).
The IssuesBrowser shows a source badge and replaces "Assign to Optio"
with an "auto-sync" pill for external tickets, since the ticket-sync
worker — not the manual assign endpoint — is what brings them in.
…-vendor (jonwiggins#519)

Restructure the top of the README to lead with Optio's wedge versus hosted
competitors (Devin, Charlie Labs, Cursor background agents, Sweep) instead of
a generic orchestration tagline.

- Reframe tagline to signal self-hosted + vendor-neutral up front
- Add a "Why Optio?" section with a side-by-side comparison table
- Add a "Who is this for?" section so readers can self-qualify in 30s
- Existing How It Works / Key Features / Architecture content preserved;
  Architecture diagram now sits below the differentiation pitch

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Agents tier (jonwiggins#518)

Refresh the user-facing docs to match the v0.4 nav restructuring (PR jonwiggins#510):
- "Repo Tasks" is now just "Tasks"; "Standalone Tasks" is now "Jobs"
- Reviews and Issues promoted out of /tasks tabs to their own top-level routes
- "Templates" in the Library renamed to "Prompts"
- Persistent Agents documented as the third Task tier across all primary docs

Files updated:
- README.md — three-tier intro, refreshed architecture diagram, Agents flow
- CLAUDE.md — backend-naming note rewritten, diagram + UI section refreshed,
  Persistent Agents added as a key subsystem, reconciler now four RunKinds
- docs/tasks.md — replaced tabbed UI section with dedicated routes; legacy
  /tasks?tab=… → /jobs|/issues|/reviews redirects documented
- docs/reconciliation.md — pr-review and persistent-agent RunKinds added
  with their state machines; producers list expanded
- docs/persistent-agents.md — cross-link to examples/persistent-agents/
- examples/README.md — UI table updated to dedicated routes
- CHANGELOG.md — 0.4.0 entry expanded with Issues/Reviews promotion,
  rename summary, and a migration note for legacy URL bookmarks

Closes jonwiggins#514

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e conversation (jonwiggins#517)

Sessions previously emitted chat events live over `/ws/sessions/:id/chat` but
never persisted them, so navigating away and back gave you an empty chat with
only newly-arriving events. Tasks, workflow runs, and persistent-agent turns
all use a "REST history + live WS tail" pattern; sessions were the odd one out.

Changes:
- New `session_chat_events` table (migration 1777400000_session_chat_events.sql)
  keyed by session_id with timestamp index. Cascade-deletes with the session.
- `appendSessionChatEvent` / `listSessionChatEvents` in interactive-session-service.
  Insert path prunes per-session events past `MAX_SESSION_CHAT_EVENTS` (5000)
  to bound storage on long-running sessions.
- `GET /api/sessions/:id/chat` returns persisted events oldest-first, mirroring
  `/api/tasks/:id/logs` / `/api/workflow-runs/:id/logs`.
- `apps/api/src/ws/session-chat.ts` now persists every parsed agent event,
  every stderr line, and the user's prompt as it streams them. On WebSocket
  connect it also replays history with `catchUp: true` for clients that don't
  call REST first (mirrors persistent-agent-stream catch-up).
- `useSessionLogs` now loads history via `api.getSessionChat()` first, then
  merges live events with the same buffer/dedup pattern as `useLogs` and
  `useWorkflowRunLogs`. User messages stored in history are restored into
  `userMessages`. Live `catchUp` frames are ignored — REST is the source of
  truth for history.
- New tests: hook test (9 cases), service test (5 cases), route test (4 cases).

Closes jonwiggins#513

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t output (jonwiggins#520)

Closes jonwiggins#511

Wraps text-type log entries in a new LogMarkdown component
(react-markdown + remark-gfm), so tables, code fences, lists, links,
and other GFM blocks render with proper layout instead of as a wall
of pipes. Disallows img/iframe/script/style/video/audio/embed tags
and relies on react-markdown's default of ignoring raw HTML, so log
content cannot inject DOM. Falls back to the existing plain-text
HighlightedText path while a search query is active so substring
highlights keep working. The same wiring is applied to the
assistant-event branch that already special-cases streamed JSON.

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s#516)

The Claude Code stream-json `result` event includes the final assistant
text in `event.result`, but that same text was already emitted as an
`assistant` text block during streaming. Including it again produced a
duplicate line in the chat view — once as plain streamed text, once as a
green-tinted info entry from the "final answer" highlight.

Drop `event.result` from the info entry content; keep the metadata-only
summary `(N turns · Xs · $Y)` and the full metadata payload (cost,
turns, durationMs, isError) so cost tracking and the metadata footer
still work. Closing stdin on terminal events is unchanged.

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
…gins#521)

The Overview page now shows the same PipelineStatsBar shell for all four
execution surfaces — Repo Tasks, Standalone Tasks, Persistent Agents, and
Sessions — so the at-a-glance "what's live right now" picture is symmetric.

Backend adds two stats endpoints mirroring `/api/tasks/stats`:
- `GET /api/persistent-agents/stats` — counts grouped by agent state
  (idle / queued / running / paused / failed / archived). Archived agents
  are excluded from `total` because they are terminal.
- `GET /api/sessions/stats` — counts grouped by session state, with the
  `ended` bucket windowed to the last 24h so the bar reflects recent
  activity rather than lifetime totals.

Frontend extends `PipelineStatsBar` with `agents` and `sessions` variants,
adds matching api-client methods, threads the new stats through
`useDashboardData`, and conditionally renders each bar on `total > 0` so
empty workspaces stay clean (matching the Standalone Tasks pattern).

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…wiggins#529)

Adds CodeCommit support symmetrically with GitHub and GitLab so agent tasks
can clone, push, and open PRs against repos hosted in AWS CodeCommit.

- Extend GitPlatformType to include "codecommit" and parseRepoUrl/parsePrUrl
  to recognise git-codecommit.<region>.amazonaws.com plus console PR URLs.
- New CodeCommitPlatform implementing the full GitPlatform interface via
  @aws-sdk/client-codecommit (PR get/list, comments, approval states, three
  merge modes, repo metadata, folder listing).
- New codecommit-credential-service that resolves AWS creds from secrets
  (workspace -> global -> env vars) with a "workload-identity" sentinel for
  IRSA / instance-profile fallback.
- Pod runtime: install AWS CLI v2 in the agent base image and wire
  `aws codecommit credential-helper` for HTTPS clone auth in repo-init.sh
  and agent-entrypoint.sh.
- Prompt templates: new GIT_PLATFORM_CODECOMMIT / CODECOMMIT_REPO /
  BASE_BRANCH vars; agent uses `aws codecommit create-pull-request` and
  `update-pull-request-approval-state` instead of gh/glab.
- Setup wizard: new CodeCommit panel with region + access key + secret +
  session token inputs, validate button (sts:GetCallerIdentity +
  codecommit:ListRepositories), and repo picker integration.
- Helm: document EKS IRSA via serviceAccount.annotations
  (eks.amazonaws.com/role-arn).

Closes jonwiggins#527
…gins#525)

- add fallback to find any available GITHUB_TOKEN when workspace context is missing
- propagate workspace context to GitHub token lookup in git-token-service
- resolve AAD decryption failures during system-level repo initialization

Co-authored-by: Ramesh Nethi <r.nethi@gogatewayai.com>
…nwiggins#509)

POST /api/secrets was threading the caller's workspaceId into storeSecret
regardless of scope, so picking "Global" in the setup UI wrote contradictory
(scope='global', workspace_id='ws-X') rows. AAD-bound retrieval from
workspace-less callers (repo-init, /api/internal/git-credentials, the
workflow trigger worker) then auth-tag-failed on decrypt — observed as
hangs and 500s when adding a repo or starting a session.

- storeSecret now rejects scope='global' with a non-null workspaceId
- routes/secrets.ts strips workspaceId on the global write path (and treats
  omitted scope as global up front so the rule applies uniformly)
- new healContradictoryGlobalSecrets() runs at boot inside a pg_advisory_lock
  to re-encrypt existing bad rows under the canonical global AAD; rows
  shadowed by a true global row are dropped to avoid PG's NULLS-distinct
  UNIQUE behavior creating duplicates
- revert PR jonwiggins#525's "find any GITHUB_TOKEN, borrow its workspace_id" fallback
  in getServerToken — now unnecessary and itself nondeterministic
- audit log + response use effectiveScope (not input.scope) so user→global
  downgrade in auth-disabled mode is reported truthfully
- per-row INFO log on heal so an operator can audit which secrets crossed
  workspace boundaries

The DB-side CHECK constraint is intentionally deferred to a follow-up so it
doesn't 500 in-flight POST /api/secrets calls during a rolling deploy of
this change.
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.

6 participants