Skip to content

Releases: CES-Ltd/TitanX

v2.6.0 — Agent Workflow Builder

Choose a tag to compare

@ankurCES ankurCES released this 21 Apr 00:28

Agent Workflow Builder — node-based procedural sequences bound to agents at hire time

The LLM drives intelligence inside each step; TitanX enforces step ordering, branching, completion criteria, and retry policy between steps.

Opt-in at every level. Agents without a workflow binding behave identically to pre-v2.6.0. Enable in Governance → Security → Agent Workflow Builder (default off).

Highlights

  • 15 node handlersprompt.*, tool.git.*, sprint.*, condition, parallel.*, human.approve, memory.recall, acp.slash.invoke.
  • 7 builtin workflows seeded at boot — safe_commit, pr_triage, sprint_standup, lead_qualify, content_brief, research_digest, parallel_review.
  • Interactive visual editor at /agent-workflows — React Flow canvas with drag-to-reposition, drag-to-connect, click-to-edit, undo/redo (Cmd/Ctrl+Z), schema-aware parameter forms with Form/JSON toggle, keyboard delete, node palette, JSON import/export.
  • Per-turn dispatcher — walks the active-step frontier each turn; graph snapshot at run-start protects in-flight runs from mid-run edits.
  • Parallel semanticsparallel.fan_out spawns branches; parallel.join waits for all predecessors before activating.
  • Human-in-the-loophuman.approve pauses the run; resume via IPC.
  • Hire-time binding — slot-level override in the Add Agent modal, supersedes any template default_workflow_id.
  • Fleet publishing — master flips "Publish to fleet"; slaves auto-apply on the next config bundle poll.
  • Dream Mode integration — every terminal run writes a [workflow:<canonical>]-prefixed trajectory to reasoningBank; summarizeWorkflowFamily aggregates into a non-LLM suggestion (LLM distillation prompt coming in v2.6.x).
  • Pluggable GitExecutor — subprocess today, swappable with a dedicated git MCP adapter via one setGitExecutor() call.

Data model

Migration v74 — additive, zero breaking changes:

  • Extends workflow_definitions with 5 optional metadata columns.
  • New workflow_bindings + agent_workflow_runs tables.
  • agent_gallery.default_workflow_id.
  • New agent_workflows security feature toggle (opt-in).

Tests

47 unit + 19 integration tests. prek matrix clean (TypeScript, Oxlint, Oxfmt, i18n, merge/case/EOF/trailing-ws).

Docs

Full feature guide at docs/feature/agent-workflows/README.md.

Supported platforms in this release

  • macOS arm64 (Apple Silicon) — TitanX-2.6.0-mac-arm64.dmg / .zip
  • macOS x64 (Intel) — TitanX-2.6.0-mac-x64.dmg / .zip

Windows + Linux builds to follow once the CI runner ABI issues are resolved.

Code signing

Ad-hoc signed (no notarization). On first launch, right-click the app → Open to bypass the Gatekeeper warning.

v2.5.1 — Fleet titlebar Modal UI fix

Choose a tag to compare

@ankurCES ankurCES released this 19 Apr 14:55
bcd1cdd

v2.5.1 — UI patch over v2.5.0

Fixed

  • Fleet Mode / Role titlebar popover clipping — the v2.5.0 popover implementation clipped off-screen near the titlebar's right edge. Arco's Popover popup wrapper auto-widens to max-content of its tallest descendant, and neither content-div width clamps nor `position='br'` anchoring kept the popup in the viewport. Replaced both with a centered Arco Modal (520px, focus-trapped, Esc-dismissable, backdrop-click-dismissable). Cleaner home for the master-port / slave-URL config forms too.

All v2.5.0 Dream Mode functionality (Phases A/B/C/D + final-gap closures) carries forward unchanged. Schema stays at v73.

Downloads

  • macOS Apple Silicon: `TitanX-2.5.1-mac-arm64.dmg`
  • macOS Intel: `TitanX-2.5.1-mac-x64.dmg`

Unsigned ad-hoc macOS builds (no Apple Developer ID). On first launch, right-click → Open to bypass Gatekeeper, or run `xattr -cr /Applications/TitanX.app` after moving it to Applications.

v2.5.0 — Dream Mode self-evolution (Phases A/B/C/D)

Choose a tag to compare

@ankurCES ankurCES released this 19 Apr 07:26

TL;DR

Dream Mode was architecturally complete at v2.4.x but dormant in practice (opt-in off by default, 24–60h latency, shallow retrieval-only consumption). v2.5.0 ships four phases that flip it into active self-evolution:

  • Phase A — Activate. Default-on for fleet slaves; push cadence 24h → 2h; dream-scheduler catch-up on boot + threshold trigger; explicit fleet-consolidated preference in retrieval. Latency floor drops from ~60h → ~4h.
  • Phase B — Feedback. Slaves report consumption back to master (adoption ratio × usage feeds dream-pass ranking); failure turns captured alongside successes.
  • Phase C — Depth. Structured-JSON distillation (taskShape + preferredPath / avoidancePath + triggerCondition); memory summaries consolidated (were piling up untouched since v1.11.0); template persona patches via agent_gallery.fleet_instructions_md.
  • Phase D — Safety. Per-slave daily trajectory quota; LLM cost cap per dream pass; post-redaction secret audit (provider prefixes, PEM keys, JWT shape, Shannon entropy); workspace-scope columns foundation for multi-tenant.

Migrations

DB schema bumped to v72. Additive only: new columns on reasoning_bank + agent_gallery + fleet_learnings, new indexes, fleet_learnings.learning_type CHECK widened to accept consumption_feedback. Rollback drops the columns / indexes cleanly.

Back-compat

  • Envelope: all new fields are optional — pre-v2.5 master accepts v2.5 slaves unchanged, v2.5 master accepts pre-v2.5 slaves.
  • Config bundle: consolidated payload has a dual-shape reader (array shape for pre-v2.5, {trajectories, memorySummaries, templatePatches} for v2.5).
  • The "persistent slave Lead session" farm loop from v2.4.x is unaffected.

New env toggles

  • TITANX_LEARNING_PUSH_HOURS — push interval (default 2, min 1, max 168)
  • TITANX_FLEET_LEARNING_DEVICE_QUOTA — per-slave trajectory cap per 24h (default 500)
  • TITANX_DREAM_MAX_COST_CENTS — dream-pass distillation budget (default 500¢, floor 50¢)

Upgrade notes

  • Both master and slave should move to v2.5.0 to get the full loop. Pre-v2.5 slaves keep working but don't contribute consumption feedback or failure trajectories.
  • If you were relying on the off-by-default behavior of fleet.learning.enabled, set an explicit enabled=0 row in security_features OR toggle off master-side before upgrading slaves. Otherwise slaves will start contributing immediately.

v2.4.2 — Sanity audit cleanups: enrollmentRole gate + farm draft slot

Choose a tag to compare

@ankurCES ankurCES released this 19 Apr 06:15

Post-v2.4.1 sanity audit cleanups

Cross-mode audit (Regular / Master / Slave-workforce / Slave-farm) surfaced two real gaps. Everything else — wake-dispatch farm guard, migration v71 copy, telemetry runtimes optional field, ConversationServiceImpl 'farm' case, command-type registry, AgentType exhaustive switches — checked out clean.

1. enrollmentRole gate on farm commands (defense in depth)

Neither team.farm_provision nor agent.execute verified the slave's enrolled role before executing. A workforce slave that somehow received one (misconfigured master, stale replay, or a compromised master assuming signed-envelope bypass) would silently materialize a mirror team it can't actually run. Both handlers now fast-skip with reason: 'not_farm_role' when ProcessConfig('fleet.enrollmentRole') isn't 'farm'. Signed envelope still verifies — this is a second-layer sanity gate so boundaries don't leak even if the first layer is wrong.

2. Missing 'farm' case in SendBoxDraftStore

The store.farm bucket was added in v2.2.0 but the setDraft / getDraft switches in useSendBoxDraft.ts still fell through to default. FarmSendBox uses local state today so it doesn't rely on the hook, but any future code path that does would silently drop all draft mutations. Both switches now handle 'farm' correctly.

Upgrade notes

  • Slave-only change #1 (enrollmentRole gate); renderer-only change #2 (draft store).
  • No DB migration.
  • Pre-v2.4.2 farm slaves keep working identically — the new gate returns skipped on workforce slaves, which v2.4.2 master audit picks up cleanly.

v2.4.1 — Farm reply routes through team mailbox (Lead sees the output)

Choose a tag to compare

@ankurCES ankurCES released this 19 Apr 05:57

Fix

Architectural invariant the operator flagged: farm agents are just teammates that live on a different machine. Their response must flow back through the same team mailbox local teammates use, so the master Lead picks it up on its next wake — otherwise master sees no output when the Lead delegates a farm turn.

Local teammates accomplish this via the team MCP send_message tool, which their ACP session has access to. Farm teammates run headless on the slave without that tool.

The change

WakeRunner.dispatchFarmTurn now does the mailbox write + recipient wake on the farm teammate's behalf after a successful turn:

  • Collect every distinct fromAgentId in the mailbox messages that triggered the turn (excluding self + 'user').
  • For each sender:
    • mailbox.write({ fromAgentId: farmSlot, toAgentId: sender, content: assistantText })
    • void this.wake(sender) — matches how ActionExecutor's send_message action wakes its recipients.
  • 'user' senders (direct sends from the chat UI) are deliberately NOT routed through the mailbox — the user sees the reply via the FarmChat responseStream subscription added in v2.3.4.

Result

When the master Lead delegates to a farm teammate → slave Lead runs the turn → response comes back → mailbox gets an entry addressed to the master Lead → Lead wakes and sees the reply → Lead can continue the conversation or report to the user. Same loop as local teammates.

wake_completed audit row now carries a repliedTo list for observability.

Upgrade notes

  • Master-only fix. Machine A must be on v2.4.1. Slave can stay on v2.4.0.
  • No DB migration.

v2.4.0 — Slave-Lead provisioning + persistent Lead-routed farm dispatch

Choose a tag to compare

@ankurCES ankurCES released this 19 Apr 05:38

The hire now materializes on the slave immediately, and turns run through a persistent slave Lead

Two architectural moves the operator flagged as the right end-state: farm hires provision on the slave before any message flows, and subsequent turns drive a cached slave-side Lead CLI instead of spawning ephemeral CLIs per message.

Phase 1 — hire-time provisioning

New signed-non-destructive command: team.farm_provision.

  • Master enqueues it from TeamSessionService.addAgent (farm branch) with {teamId, teamName, leadRuntimeBackend, agentSlotId, agentName, remoteSlotId, runtimeBackend, toolsAllowlist}. The Lead runtime defaults to the teammate's so a single operator pick covers both sides.
  • Slave handler (farmProvisioning.ts):
    • Team row doesn't exist → create a Lead ACP conversation + append the Lead slot + the farm teammate to a fresh teams row (lead_agent_id = Lead.slotId).
    • Team row exists (subsequent hires to the same master team) → skip Lead creation, just append the teammate.
    • Always creates a type: 'farm' mirror conversation for the teammate so the Teams UI has a drill-down target.
  • Fires before any agent.execute — the slave's Teams UI reflects the hire the moment master clicks Hire, no more lazy on-message materialization.

Phase 2 — persistent Lead-routed dispatch

farmExecutor.handleAgentExecute now prefers a cached Lead session:

  • resolveTeamLead looks up the team's Lead conversation by lead_agent_id and verifies it's a type='acp' row.
  • getOrStartLeadSession spins up a single AcpAgent scoped to a per-team tmp workspace (titanx-farm-lead-<teamId>) with stream callbacks that funnel into a mutable currentTurn bucket. Cached thereafter; 30min idle teardown.
  • runTurnOnLead serializes one turn through the cached session, accumulates content via onStreamEvent, resolves on finish signal, races against the master timeout.
  • If the cached CLI dies mid-turn (runtime_error / runtime_send_failed), the session is torn down so the next turn re-spawns fresh.
  • Falls back to the v2.3.x ephemeral executeViaAcp path when no Lead is provisioned yet (older hires, replay-in-flight, or provision command failed).

Result

  • Slave's Teams UI shows the mirrored team immediately after hire.
  • Multi-turn conversations with the Lead preserve context.
  • ACP connect cost paid once per team lifetime instead of once per turn — follow-up turns feel noticeably faster.

Upgrade notes

  • Both machines need v2.4.0. Master for the envelope type + enqueue, slave for the handler + dispatch.
  • No DB migration — uses existing tables.
  • Pre-v2.4.0 farm slots fall through to the ephemeral executor (v2.3.x behavior) transparently.

v2.3.4 — FarmChat live stream subscription

Choose a tag to compare

@ankurCES ankurCES released this 19 Apr 05:22

Fix

v2.3.3 closed the farm ACP loop end-to-end (validated: a real Qwen3.5 reply from opencode acp on a slave, 41s round-trip), and WakeRunner.writeFarmAssistantMessage correctly persists the assistant bubble and emits a responseStream content event. But nothing in the FarmChat tree subscribed to that stream, so the reply sat in the DB until the user closed and reopened the chat.

The change

FarmChat now subscribes to ipcBridge.conversation.responseStream on mount, filters by conversation_id, and routes content + user_content events through the shared transformMessageaddOrUpdateMessage. Matches the pattern AcpSendBox and RemoteSendBox already use; FarmSendBox stays send-only.

Upgrade notes

  • Master-only fix. Machine A must be on v2.3.4. Slave can stay on v2.3.3.
  • No DB migration.

v2.3.3 — Per-backend ACP args for slave farm dispatch

Choose a tag to compare

@ankurCES ankurCES released this 19 Apr 05:10

Fix

v2.3.2 successfully resolved the CLI path and spawned opencode, but the CLI exited with code 1 and printed its help text because createGenericSpawnConfig's default args (['--experimental-acp']) only work for claude. opencode needs the acp subcommand; goose wants session start; etc.

The change

The AcpDetector already attaches the right args to every DetectedAgent (sourced from ACP_BACKENDS_ALL — e.g. opencode: { acpArgs: ['acp'] }). resolveCliPathForBackend now returns them, and executeViaAcp passes them into both AcpAgent.config.customArgs and extra.customArgs — where AcpConnection.doConnect reads them on the non-claude paths.

Observed against a real slave (job 66d6e4fb):

runtime_start_failed: opencode ACP process exited during startup (code: 1)
<opencode CLI help text>

With this fix, opencode launches as opencode acp and enters ACP protocol mode.

Upgrade notes

  • Slave-only. Machine B must be on v2.3.3. Master can stay on v2.3.0+.
  • No DB migration.

v2.3.2 — Dynamic import for AcpDetector in slave farm path

Choose a tag to compare

@ankurCES ankurCES released this 19 Apr 04:58

Fix

v2.3.1's CLI-path lookup used a dynamic require() with a path alias:

const { acpDetector } = require('@process/agent/acp/AcpDetector');

Path aliases (@process/…) only get rewritten by the bundler (esbuild/vite) for static imports and dynamic import(). A dynamic require() call is left literal, so the packaged app hit at runtime:

Cannot find module '@process/agent/acp/AcpDetector'

which surfaced to master as "Agent failed to start."

The change

Switched resolveCliPathForBackend to await import(...) — same pattern the same file already uses for AcpAgent. Made the function async and awaited it at the one call site in executeViaAcp. No other behavior change.

Upgrade notes

  • Slave-only. Machine B must be on v2.3.2. Master can stay on v2.3.0+ for the dispatch to work end-to-end.
  • No DB migration.

v2.3.1 — Thread slave CLI path into ACP farm dispatch

Choose a tag to compare

@ankurCES ankurCES released this 19 Apr 04:47

Fix

v2.3.0 shipped the ACP dispatch path on the slave, and it worked end-to-end except for one missing config field: AcpConnection.connect() rejects most backends with:

CLI path is required for <backend> backend

when cliPath is undefined. The slave's acpDetector already probes for these binaries at boot (which opencode, etc.) and stashes the result on each DetectedAgent, but executeViaAcp wasn't reading them.

The change

resolveCliPathForBackend() now looks up the chosen backend in acpDetector.getDetectedAgents() and threads the resulting cliPath into both AcpAgent.config.cliPath and config.extra.cliPath (the latter is what AcpConnection.connect() actually reads).

New ack reason

runtime_not_detected — fires when the envelope asks for a backend the slave's detector didn't find. Clearer signal than conflating with runtime_start_failed, so the operator immediately knows to install (or re-login) the CLI on the slave rather than debug a generic start failure.

Observed against

A real slave attempting opencode dispatch (v2.3.0 job 4b990b01 ack'd with):

{ "reason": "runtime_start_failed",
  "error": "CLI path is required for opencode backend",
  "runtimeBackend": "opencode" }

With this fix the cliPath from the slave's own detector is passed through and the connect proceeds.

Upgrade notes

  • Slave-only fix. Machine A (master) doesn't need to upgrade for this to work. Machine B (slave) must be on v2.3.1.
  • No DB migration.