Skip to content

chore/merge upstream to gynzy#22

Merged
genisd merged 46 commits into
gynzyfrom
chore/merge-upstream-to-gynzy
Apr 21, 2026
Merged

chore/merge upstream to gynzy#22
genisd merged 46 commits into
gynzyfrom
chore/merge-upstream-to-gynzy

Conversation

@genisd

@genisd genisd commented Apr 20, 2026

Copy link
Copy Markdown
Member

Summary

Changes

Testing

  • Tests pass (pnpm turbo test)
  • Typechecks pass (pnpm turbo typecheck)

Related

Closes #

Screenshots

genisd and others added 30 commits April 14, 2026 09:51
…ue-configurable

feat: make repo init timeout configurable via OPTIO_REPO_INIT_TIMEOUT_MS
PVC mounted at /home/agent was root-owned on GKE, causing "Permission
denied" when agent user tried to write ~/.gitconfig or create worktrees.

- Add pod-level securityContext (fsGroup/runAsUser/runAsGroup=1000) to
  StatefulSet pods so mounted volumes are writable by the agent user
- Create agent user with explicit UID/GID 1000 in base Dockerfile
  (Ubuntu 24.04 ships 'ubuntu' at UID 1000, pushing 'agent' to 1001)

Closes jonwiggins#424
…onwiggins#425)

Allow users to configure a custom base URL for local/self-hosted inference
servers (vLLM, lightllm, Ollama, etc.) when using the OpenCode agent. When
a base URL is set, provider API keys are optional — a placeholder key is
injected so keyless local endpoints work out of the box.

Closes jonwiggins#425
Build each image on native runners (ubuntu-latest for amd64,
ubuntu-24.04-arm for arm64) in parallel, push per-arch by digest, then
merge into a single multi-arch manifest via `docker buildx imagetools
create`. Applies to optio-api, optio-web, optio-optio, and all
optio-agent-* presets.

Closes jonwiggins#426
Every unit of agent work is now a Task. A Task has an optional
"Attach a repo" toggle: when set, it becomes a Repo Task that opens
a PR; when not, it's a Standalone Task that runs the agent with no
checkout. Both share the same trigger, template, and connection
primitives.

Unified trigger model
- workflow_triggers polymorphic via (target_type, target_id)
- task_configs table stores reusable Repo Task blueprints
- Trigger worker dispatches to createWorkflowRun or instantiateTask
- Ticket sync fires task_config triggers on actionable tickets
- Webhook ingress (/api/hooks/:path) dispatches by target_type

Templates
- prompt_templates gains kind (prompt/review/job/task), params_schema,
  default_agent_type, description
- renderTemplateString() supports {{param}} and {{#if}} blocks
- /templates library page with editor, preview, kind filter

UI
- Sidebar grouped into Run / Library / Insights / Admin
- /tasks/new is a single unified form (Who/What/When/Where/Why)
- /tasks/scheduled list + detail pages with per-trigger pause/resume
- Shared TriggerSelector component for cron/webhook configuration
- Jobs page retitled "Standalone Tasks"; /jobs/new redirects to /tasks/new

Docs
- CLAUDE.md rewritten for the unified model
- /docs/guides/workflows retitled "Standalone Tasks"
- New /docs/guides/scheduled-tasks guide
- API reference paths updated to /api/jobs and /api/task-configs

Also includes follow-up OpenCode work from parallel branch:
- Custom OpenCode base URL via OPENCODE_DEFAULT_BASE_URL secret
- OpenCode adapter ungated (OPTIO_OPENCODE_ENABLED removed)
- Setup wizard step for OpenCode custom endpoints

1917 API tests pass; 242 web tests pass; production build clean.
Mirrors the Repo Tasks filter bar (search, time range, agent type, cost
min/max) with client-side filtering and URL state sync. Stage and repo
filters are omitted — definitions have no pipeline state, and standalones
are repo-agnostic.
All three task kinds — repo-task (ad-hoc), repo-blueprint (scheduled
Repo Task config), and standalone (Standalone Task) — are now reachable
through one polymorphic /api/tasks resource. The existing tables
(tasks, task_configs, workflows, workflow_runs) stay distinct because
their lifecycle and column shapes genuinely differ; only the HTTP
surface is unified.

Backend
- unified-task-service: resolveAnyTaskById() checks tasks → task_configs
  → workflows; UUIDs are globally unique so no collision. Plus
  listUnifiedTasks, listUnifiedRuns, getUnifiedRun, list/getTriggersForParent.
- GET /api/tasks now accepts ?type=repo-task|repo-blueprint|standalone|all.
  Default preserves existing enriched repo-task shape.
- POST /api/tasks accepts { type } discriminator and dispatches to
  taskService, taskConfigService, or workflowService. Default preserves
  existing repo-task behavior.
- GET /api/tasks/:id falls through to the resolver when the id isn't a
  tasks row — returns the native row tagged with a `type` field.
- NEW routes at /api/tasks/:id/runs[/:runId] and /api/tasks/:id/triggers
  polymorphic over blueprints + standalone; 405 for ad-hoc repo-tasks.

Frontend
- api-client: added listTasksUnified, createTaskUnified, getTaskUnified,
  listTaskRuns, createTaskRun, getTaskRun, listTaskTriggers,
  createTaskTrigger, updateTaskTrigger, deleteTaskTrigger.
- /tasks/new rewritten to call createTaskUnified with the right type based
  on the mode + run-mode selection — one branch instead of four.

Docs
- CLAUDE.md: added Unified /api/tasks HTTP layer section.
- api-reference: rewrote the Tasks section to show the polymorphic
  endpoints and document the type discriminator.

Legacy /api/jobs/* and /api/task-configs/* still work as thin aliases
for back-compat.

1930 API tests pass (13 new for the unified routes). 242 web tests pass.
Production build clean.
Found during end-to-end testing of the unified /api/tasks surface. Each
bug pre-existed my unification work but was exposed by actually driving
traffic through the full pipeline.

1. createWorkflowRun never enqueued
   workflow-service.createWorkflowRun inserted a workflow_runs row but
   did NOT enqueue a BullMQ job on workflowRunQueue — only the
   /api/jobs/:id/runs HTTP route did. This meant schedule triggers,
   webhook ingress, and the new /api/tasks/:id/runs endpoint all
   created runs that sat in queued forever. Moved the enqueue into the
   service so every caller gets it automatically; removed the duplicate
   enqueue from workflows.ts.

2. runAsUser: 1000 doesn't match image's agent UID 1001
   0a32e34 set fsGroup/runAsUser/runAsGroup to 1000 on StatefulSets,
   but the base image creates `agent` at UID 1001 and chowns /workspace
   to 1001. Pods ran as 1000 and got Permission denied cloning the
   repo. Changed all three fields to 1001.

3. No initContainer to chown the PVC mount
   Some storage classes (docker-desktop hostpath, GKE standard) don't
   honor fsGroup. Added a home-perm-fix initContainer that runs as root
   and chown -R 1001:1001 /home/agent before the main container starts.

4. 120s pod-ready timeout too tight for local dev
   Extracted POD_READY_TIMEOUT_MS to OPTIO_POD_READY_TIMEOUT_MS env
   var, default 300s. First-ever agent pod on docker-desktop often
   takes longer than 120s (image pull + init container + scheduling).

5. waitForJobPod used the wrong label selector
   Looked up workflow pods with labelSelector: job-name=X, but the
   pods are labeled app.kubernetes.io/instance=X (our template, not
   the standard K8s Job controller label). Function ALWAYS timed out.
   Switched to the instance label we actually set.

Bonus: the OAuth token bug surfaced here too, but that's user data in
the secret store (fix: POST /api/secrets with a fresh token). Not
included in this commit.

Verified end-to-end: R1 repo-task ran through focus_center (clone,
agent edits, completion). R2 standalone run completed in ~10s with
\$0.062 cost. R3 scheduled trigger fired and dispatched correctly.
Docs drift cleanup after the Task unification and /api/tasks polymorphic
endpoint shipped.

- docs landing: pitch rewritten around Task (with optional repo) instead
  of "workflow orchestration + agent workflows"
- creating-tasks: adds the New Task mode picker (Repo Task / Standalone
  Task), the "Run now vs Schedule" toggle, adapted button labels, and
  where each kind surfaces in the UI
- scheduled-tasks: all API examples moved to /api/tasks with the type
  discriminator (repo-blueprint, standalone); callout now describes both
  Repo + Standalone as schedulable; legacy paths called out as aliases
- workflows (Standalone Tasks) guide: code examples use /api/tasks with
  type: "standalone" and /api/tasks/:id/runs + /api/tasks/:id/triggers
  instead of /api/jobs/*

No code changes. Typecheck clean.
…onwiggins#446)

When a workflow pod dies (OOM, crashloop, node drain) the corresponding
workflow_runs row stays in 'running' forever — blocking concurrency and
confusing the UI. Add periodic zombie detection that checks pod liveness
for stale running runs and transitions them to failed with retry support.

Also adds orphaned repo task detection for tasks whose pod record has
been cleaned up but the task was never transitioned out of running.

Both checks run as part of the existing repo-cleanup-worker cycle.

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lone-tasks (jonwiggins#440)

The guide page was retitled to "Standalone Tasks" but the URL slug still
used the legacy "workflows" path. This renames the route directory, updates
all nav/sitemap/cross-link references, and adds a client-side redirect page
at the old path so existing external links don't 404.

- Rename workflows/ directory to standalone-tasks/
- Update docs.ts nav href
- Update sitemap.ts entry
- Update cross-links in connections and integrations guide pages
- Add redirect page at old /docs/guides/workflows path
- Add vitest + tests for docs nav and sitemap entries

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

Replace the stale Schedules section (leftover from removed feature) and
the separate Workflows section (listing /api/jobs/* as primary API) with
a single "Legacy Aliases" note that points readers to the canonical
/api/tasks polymorphic surface.

Closes jonwiggins#436

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

- Add last_error, last_error_at, consecutive_failures columns to
  ticket_providers table so sync errors are persisted per-provider
- On successful sync, clear error state; on failure, record the error
  message and increment consecutive failure count
- Auto-disable provider after 5 consecutive failures to stop log spam
- Downgrade repeated identical errors to debug level logging
- Add PATCH /api/tickets/providers/:id/re-enable endpoint to clear
  errors and re-enable a disabled provider
- Update Settings > Ticket Providers UI to show error details, disabled
  state, failure count, and a "Re-enable" button
- Add migration and journal entry for the new columns

Closes jonwiggins#438

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

Delete the FailureInsights and PerformanceSummary components from the
main dashboard. The underlying API endpoints remain since they are
still used by the /analytics page.

Closes jonwiggins#428

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

Remove the conditional failed-tasks banner that prompted users to ask
Optio to investigate. Also removes now-unused Bot icon import and
useOptioChatStore hook from the dashboard page.

Adds tests verifying the banner no longer appears.

Closes jonwiggins#429

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add proactive detection and surfacing of expired Claude OAuth tokens
so users see warnings before tasks fail with opaque 401 errors.

Changes:
- New token-validation-worker: BullMQ repeating job (every 5min) that
  validates the stored CLAUDE_CODE_OAUTH_TOKEN against the Anthropic API
  and caches the result in Redis. Publishes auth:failed WebSocket event
  when expired.
- Pre-flight check in task-worker: reads the cached validation before
  pod provisioning to fail fast with a clear error instead of wasting
  ~10s of infra work per task.
- Auth status endpoint extended with lastValidated timestamp from the
  background worker's cache, avoiding redundant API probes on each poll.
- Global auth warning banner in layout-shell: visible on all pages (not
  just the dashboard) when the token is expired, with a direct link to
  the Secrets page.
- Improved error-classifier remedy for expired tokens: leads with
  "Go to Secrets" instead of burying it after a shell command.

Closes jonwiggins#435

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

The backend already supports ticket triggers (type="ticket" with
config={source, labels?}), but no UI existed to create them. This adds
the "Ticket" option to the TriggerSelector component with a source
dropdown (GitHub, Linear, Jira, Notion) and an optional labels array.

- Add ticket type, source dropdown, and labels input to TriggerSelector
- Wire ticket trigger config through /tasks/new form submission
- Wire ticket trigger config through /tasks/scheduled/:id detail page
- Add Ticket icon to trigger displays in list and detail pages
- Add "ticket" to createTaskConfigTrigger type in api-client
- Update scheduled-tasks docs guide to mention ticket triggers
- Add comprehensive tests for the new ticket trigger UI

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…out PR (jonwiggins#443)

A Repo Task that completes successfully without opening a PR now
transitions to needs_attention instead of completed. This prevents
false-positive success badges when the agent exits cleanly but the
work never shipped.

The fix adds a post-agent check: if the task has a repoUrl, is not a
review/planning task, and no PR URL was detected in logs, the system
first queries the Git provider API as a fallback. If a PR is found
there, the task transitions to pr_opened normally. Otherwise it
transitions to needs_attention with a clear message so the user can
resume or restart the agent.

Also adds a "Completed without PR" label in the UI attention reason
formatter, and state machine lifecycle tests for the escalation path.

Closes jonwiggins#433

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

Empty-string environment variables (e.g. from Helm rendering a missing
value as '') caused parseInt("", 10) to return NaN, bypassing the ??
nullish-coalescing default. This crashed the API pod on startup when
OPTIO_REPO_INIT_TIMEOUT_MS was set to empty string.

Introduces a shared parseIntEnv() utility that treats empty/whitespace-
only strings the same as undefined, falling back to the default value.
Applied to all 19 parseInt(process.env.X ?? "default", 10) call sites.

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`helm upgrade --reuse-values` silently ignores chart defaults added
after the original install, so any new values.yaml key renders as an
empty string in downstream templates. This bit us when
`agent.repoInitTimeoutMs` was added: the secret key rendered empty,
`parseInt("")` returned `NaN`, and the API crashlooped on the new
positive-integer validation in repo-pool-service.ts.

Switch to `--reset-then-reuse-values` so fresh chart defaults merge on
top of the stored release values (encryption.key etc. are preserved).
Introduces a per-run reconciler shaped after Kubernetes controllers:
event sources enqueue {kind, id} keys; a worker pops each, builds a
frozen WorldSnapshot, runs a pure decision function, and applies the
resulting Action via a CAS-gated executor. A periodic resync sweeps
all non-terminal runs as a correctness backstop for lost events.

- Decision layer (packages/shared/src/reconcile): reconcileRepo (10
  states) and reconcileStandalone (4 states) return a typed Action
  union (transition, patchStatus, deferWithBackoff, launchReview,
  autoMergePr, resumeAgent, etc.). Fully pure; no DB, no clock.
- Executor (apps/api/src/services/reconcile-executor.ts): CAS-gates
  every mutation on updated_at; delegates repo transitions to the
  existing transitionTask so event/webhook fan-out is preserved;
  defers side-effect actions in this phase until Phase C wires event
  sources.
- Snapshot builder (reconcile-snapshot.ts): reads run row, pod state,
  PR status, deps, capacity, heartbeat, and repo settings in parallel,
  records read failures for defer-with-backoff.
- Workers (reconcile-worker.ts, reconcile-resync-worker): BullMQ queue
  keyed by \`\${kind}__\${id}\` for per-key dedup; 5-min resync interval.
- Schema: adds control_intent, reconcile_backoff_until,
  reconcile_attempts to both tasks and workflow_runs. Nullable /
  default 0 — no backfill needed.

Feature-flagged: off by default. Enable with OPTIO_RECONCILE_ENABLED=true;
shadow mode on by default (OPTIO_RECONCILE_SHADOW=true).

Tests: 128 new (73 pure decisions + 33 edge cases + 22 executor).
Claude CLIs catch 401s internally and exit 0, which previously caused
runs that hit "Invalid authentication credentials" mid-execution to be
marked state=completed with no errorMessage. The UI banner and auth
detector correctly flagged the token as expired, but individual runs
looked like successes.

- detectAuthFailureInLogs(logs) in auth-failure-detector.ts: pure
  scanner over the accumulated agent log blob using the existing
  AUTH_FAILURE_PATTERNS ("api error: 401", "authentication_error",
  '"status":401', etc.). Returns the first matching pattern and a
  whitespace-normalized excerpt capped at 240 chars.
- workflow-worker and task-worker now run this check right after
  adapter.parseResult. On a match, the result is coerced to
  success=false with a classified errorMessage, and recordAuthEvent
  fires so the global banner surfaces it.
- scripts/update-claude-auth.sh: one-shot refresh of the stored
  CLAUDE_CODE_OAUTH_TOKEN. On macOS, extracts the token from the
  "Claude Code-credentials" Keychain entry (same path the UI banner
  uses) and POSTs it to /api/secrets for encryption + validation
  against Anthropic. Falls back to manual paste on Linux or if the
  Keychain entry is missing.

Tests: 10 new cases on detectAuthFailureInLogs covering each pattern,
case insensitivity, excerpt shape, first-match precedence, and
unrelated-"401" false-positive rejection.
Reorder the overview panel so sections appear as:
1. Recent Tasks (first)
2. Pods
3. Recent Events
4. Recent Activity (last)

Previously Recent Activity appeared before Pods/Recent Events. Each
section now renders as its own full-width block instead of being
paired in a 2-column grid.

Closes jonwiggins#451

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

Add a GenericOIDCProvider that supports any OpenID Connect-compatible
identity provider (Keycloak, Authentik, Authelia, Zitadel, Okta, Auth0,
etc.) via standard discovery protocol.

- New provider class in apps/api/src/services/oauth/oidc.ts with:
  - Auto-discovery via .well-known/openid-configuration (24h cache)
  - ID token signature verification using JWKS (jose library)
  - OIDC claim mapping (sub, email, name/preferred_username, picture)
- Conditional registration when OIDC_ISSUER_URL is set
- Optional prepare() hook on OAuthProvider interface for async init
- Helm chart values (auth.oidc.*) and secrets template wiring
- KeyRound icon fallback for unknown providers on login page
- Docs, .env.example, and CLAUDE.md updated
- 19 unit tests covering discovery, caching, token exchange,
  signature verification, tampered token rejection, and registration

Closes jonwiggins#448

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restore the 2-column layout so Recent Tasks sits on the left and Pods
on the right, instead of each rendering as full-width blocks.
Include Recent Activity in the same grid as Recent Tasks and Pods so
all three reflow together: 1 column on mobile, 2 on md, 3 on xl.
Each column is at least 28rem wide before stacking. The grid now
naturally flows from 1 to 3 columns depending on available width
instead of switching at fixed breakpoints that left columns cramped.
Switch the overview section container from CSS grid (which stretched
rows to the tallest item and left a gap before Recent Activity wrapped
onto a new row) to a multi-column layout. Items now stack tightly
within each column and flow between columns as width allows.

Also promote the "Recent Events" subheading inside PodsList to match
the h2 style used by Recent Tasks, Pods, and Recent Activity so all
four section titles render identically.
jonwiggins and others added 15 commits April 18, 2026 12:45
Add docs/reconciliation.md covering the K8s-style control plane (decision
flow, what it reconciles, triggers, env vars, schema, debugging) and
docs/tasks.md covering the two task flavors, the three internal types,
and the unified /api/tasks layer. Surface both in the README and add a
reconciliation entry to CLAUDE.md's Key Subsystems.
Workflow logs live in workflow_run_logs, not task_logs, so the auth
detector never saw 401s from Standalone Task runs and the banner
wouldn't trigger. Add an auth_events check for tokenType=claude that
mirrors the existing github path — the workflow worker already records
claude auth_events on detection, the detector just wasn't reading them.
…ask pages

Repo Task and Standalone Task pages each reimplemented the same UI
primitives. Unify on the repo-task versions where they existed.

- TokenRefreshBanner gains the numbered-step layout (1. Copy / 2. Paste)
  from the repo task page; the repo task page now imports it instead of
  inlining the form. Standalone run page and overview panel pick up the
  nicer layout for free.
- Delete the two local RunStateBadge / RUN_STATE_CONFIG definitions in
  the jobs pages; use the shared StateBadge already powering task-card.
- Extract MetadataCard into its own component with a sm/lg size prop so
  both the workflow detail stats bar and the run detail runtime cards
  can share it.
…th token (jonwiggins#455)

When getClaudeUsage() receives a 401 or 403 from Anthropic's usage API,
it now records an auth event and publishes an auth:failed WebSocket event
so the UI shows the re-auth banner immediately — without waiting for the
next 5-minute /api/auth/status poll or a task launch failure.

Closes jonwiggins#454

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…input totals (jonwiggins#457)

All agent adapters were only summing input_tokens and output_tokens from
Anthropic's usage object, ignoring cache_creation_input_tokens and
cache_read_input_tokens. Since agent loops are heavily cache-driven, this
caused input token counts to be undercounted by 10-100x, making cost
analytics and per-task spend attribution misleading.

Add cache_creation_input_tokens and cache_read_input_tokens to the input
token accumulation in all six adapters: claude-code, openclaw, opencode,
gemini, copilot, and codex. Add corresponding unit tests to prevent
regression.

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes shadow mode and the env-flag gate. The reconciler now owns
PR-driven transitions, auto-merge, auto-resume, review launch, stall
and pod-death detection, control-intent handling, and standalone-run
auto-retry with exponential backoff. Workers stay as side-effect doers
but no longer drive state transitions in parallel.

Executor now implements the five previously-deferred side-effect
actions (requeueForAgent, enqueueAgent, resumeAgent, launchReview,
autoMergePr) and publishes events + webhooks on standalone transitions.
transitionTask and workflow-worker's transitionRun fire
enqueueReconcile after every successful transition, so any state change
wakes the reconciler within milliseconds.

pr-watcher slimmed from ~600 to ~290 lines: it now only polls PRs,
writes the refreshed fields to the row, and enqueues a reconcile —
all decision logic flows through reconcile-repo.ts. repo-cleanup
keeps its stale-task recovery loop but stops firing the FAILED
transition itself on pod death; the reconciler observes pod.phase=error
from the snapshot and owns the transition.

Snapshot now loads pod state for standalone runs and computes
recentAutoResumeCount so decideFromPrStatus can cap the auto-resume
loop. New decideFailed for standalone runs returns a transition-with-
backoff that the executor pairs with a delayed reconcile job.

Bugfixes uncovered during cutover:
- enqueueReconcile used a stable jobId, which caused BullMQ to silently
  drop re-enqueues for any task whose prior reconcile sat in the
  completed set. Switched to unique jobIds per enqueue.
- requeueForAgent and enqueueAgent now use jobId=runId so process-task
  jobs dedup against the original enqueue, preventing the race that
  caused InvalidTransitionError cascades.
- tryTransitionTask catches InvalidTransitionError as a lost-race
  signal in addition to StateRaceError.
- detectAuthFailureInLogs no longer scans inside `user`/`assistant`
  NDJSON events, fixing the false positive when an agent reads a test
  fixture containing literal pattern text.
- Task detail page no longer renders the error panel for pr_opened
  state; a non-fatal warning shouldn't paint a healthy task as failed.

TODO(reconciler) left in repo-cleanup-worker for the stall-recovery
loop, which has retry-cap and orphan-agent-killing semantics that
need lifting into the snapshot/decision/executor before it can move.

See docs/reconciliation.md for the updated control-plane diagram.
Stable jobIds were being silently no-op'd by BullMQ when a prior
process-task or process-workflow-run job for the same id sat in the
completed/failed set. This blocked auto-retried workflow runs from
ever progressing past the second enqueue: decideFailed transitioned
them to QUEUED, decideQueued returned enqueueAgent on every pass, and
the executor's add() was a no-op every time.

Same root cause as the enqueueReconcile dedup fix in the cutover
commit. The worker's existing "skip if state != queued" defensive
check (task-worker.ts:106, workflow-worker.ts:276) protects against
duplicate concurrent claims, so unique jobIds are safe.
Both the auto-retry path (decideFailed) and the user-driven retry path
(interpretIntent retry) transitioned a FAILED run back to QUEUED with
errorMessage cleared and retryCount incremented, but left finishedAt
holding the original failure timestamp.

When the worker subsequently transitioned QUEUED→RUNNING, the reconcile
wake-up landed in decideRunning, which treats `finishedAt && !errorMessage`
as "agent already finished successfully" and short-circuits to COMPLETED.
The retry was marked completed without the agent ever running again
(observed: started_at = today, finished_at = days ago, output null).

Including `finishedAt: null` in both retry statusPatches keeps the
"agent already finished" detection on real finished runs only.
…caling

Standalone runs previously spawned one Kubernetes pod each — a burst of
triggers would open tens of pods even though only a few ran at once.
Pool pods per-workflow instead, mirroring repo pod scaling: each workflow
has `maxPodInstances` replicas (default 1, max 20) hosting up to
`maxAgentsPerPod` concurrent runs (default 2, max 50). Runs inject env
per-exec and track their assigned pod via `workflow_runs.pod_id`, with
`last_pod_id` preserving retry affinity. Selection order matches repo
pods: preferred pod → least-loaded → scale-up → overflow wait.
…et (jonwiggins#460)

GitHub ticket sync threw "provider requires token" whenever no
provider-specific secret was stored, even though a GitHub App was
configured at the server level. Now routes through the existing
getGitHubToken({ server: true }) resolver so the App installation
token (or PAT) is used when the provider config/secret omits one.

Closes jonwiggins#458
- Remove gynzy's custom schedules feature (schedules table, routes, worker, UI)
- Adopt main's unified task model (task_configs, workflow_triggers, reconciler)
- Fix workflow-pool-service tests for renamed getOrCreateWorkflowPod function
- Update mock to include parseJsonEnv for nodeSelector/tolerations tests
- Accept main's UID 1001 for agent pods (matches base.Dockerfile)
- Keep nodeSelector/tolerations env var support as gynzy feature
The securityContext was incorrectly set to UID 1001, but the agent
user in images/base.Dockerfile is created with UID 1000. This mismatch
caused git config --global to fail with "Permission denied" because
HOME resolved to / when running as a non-existent UID.
@genisd genisd merged commit b176be8 into gynzy Apr 21, 2026
16 checks passed
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.

3 participants