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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class RenameAutomationCompletionSignal1775400000000 implements MigrationInterface {
name = 'RenameAutomationCompletionSignal1775400000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "ticket_automation_run"
RENAME COLUMN "completion_marker_seen" TO "completion_signal_seen"
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "ticket_automation_run"
RENAME COLUMN "completion_signal_seen" TO "completion_marker_seen"
`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

/**
* Persist explicit OpenCode model (`provider/model`) for ticket chat/AI and automation.
* OpenCode no longer has an auto model; callers must pass a concrete selection.
*/
export class AddPreferredModelsForChatAndAutomation1775500000000 implements MigrationInterface {
name = 'AddPreferredModelsForChatAndAutomation1775500000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "tickets"
ADD COLUMN IF NOT EXISTS "preferred_chat_model" varchar(256) NULL
`);
await queryRunner.query(`
ALTER TABLE "ticket_automation"
ADD COLUMN IF NOT EXISTS "preferred_model" varchar(256) NULL
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "ticket_automation" DROP COLUMN IF EXISTS "preferred_model"
`);
await queryRunner.query(`
ALTER TABLE "tickets" DROP COLUMN IF EXISTS "preferred_chat_model"
`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,13 @@ export class ControllerJobsProcessor extends WorkerHost {
}

private async runOpencodeConfigSyncCoordinator(): Promise<void> {
const targetIds = await this.opencodeConfigSyncTargets.findPendingTargetIds(getOpencodeConfigSyncBatchSize());
const batchSize = getOpencodeConfigSyncBatchSize();
// Prefer already-pending/failed targets so reconcile rollouts cannot starve retries.
const pending = await this.opencodeConfigSyncTargets.findPendingTargetIds(batchSize);
const remaining = Math.max(0, batchSize - pending.length);
const reconciled =
remaining > 0 ? await this.opencodeConfigSyncTargets.reconcileOutdatedSyncedTargets(remaining) : [];
const targetIds = [...new Set([...pending, ...reconciled])].slice(0, batchSize);

for (const targetId of targetIds) {
await enqueueUnitJob({
Expand Down
28 changes: 26 additions & 2 deletions apps/agenstra/frontend-agent-console/src/i18n/messages.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -3127,13 +3127,17 @@
<source>Agent provider error</source>
<target>Agent-Provider-Fehler</target>
</trans-unit>
<trans-unit id="featureTicketsBoard-runFailureAgentNoCompletionStatus" datatype="html">
<source>No completion status</source>
<target>Kein Abschlussstatus</target>
</trans-unit>
<trans-unit id="featureTicketsBoard-runFailureAgentNoCompletionMarker" datatype="html">
<source>No completion marker</source>
<target>Kein Abschluss-Marker</target>
</trans-unit>
<trans-unit id="featureTicketsBoard-runFailureMarkerWithoutVerify" datatype="html">
<source>Completion marker without verify profile</source>
<target>Abschluss-Marker ohne Verifizierungsprofil</target>
<source>Completion without verify profile</source>
<target>Abschluss ohne Verify-Profil</target>
</trans-unit>
<trans-unit id="featureTicketsBoard-runFailureVerifyCommandFailed" datatype="html">
<source>Verify command failed</source>
Expand Down Expand Up @@ -3307,10 +3311,30 @@
<source> Agent for chat / AI </source>
<target>Agent für Chat / KI</target>
</trans-unit>
<trans-unit id="featureTicketsBoard-modelLabel" datatype="html">
<source> Model for chat / AI </source>
<target>Modell für Chat / KI</target>
</trans-unit>
<trans-unit id="featureTicketsBoard-modelPlaceholder" datatype="html">
<source> {{ ticketAiModelsLoading() ? 'Loading models…' : 'Select agent to load models…' }} </source>
<target> {{ ticketAiModelsLoading() ? 'Modelle werden geladen…' : 'Agent wählen, um Modelle zu laden…' }} </target>
</trans-unit>
<trans-unit id="featureTicketsBoard-agentPlaceholder" datatype="html">
<source>Select agent…</source>
<target>Agent wählen…</target>
</trans-unit>
<trans-unit id="featureTicketsBoard-automationPreferredModel" datatype="html">
<source>Model for automation</source>
<target>Modell für Automatisierung</target>
</trans-unit>
<trans-unit id="featureTicketsBoard-automationPreferredModelHint" datatype="html">
<source> Required when automation is enabled. OpenCode has no auto model selection. </source>
<target> Erforderlich, wenn die Automatisierung aktiviert ist. OpenCode hat keine Auto-Modellauswahl. </target>
</trans-unit>
<trans-unit id="featureTicketsBoard-automationModelPlaceholder" datatype="html">
<source> {{ ticketAiModelsLoading() ? 'Loading models…' : 'Select an agent to load models…' }} </source>
<target> {{ ticketAiModelsLoading() ? 'Modelle werden geladen…' : 'Agent wählen, um Modelle zu laden…' }} </target>
</trans-unit>
<trans-unit id="featureTicketsBoard-acceptanceLabel" datatype="html">
<source> Acceptance <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;text-muted small d-block fw-normal&quot; [attr.aria-label]=&quot;percentage + &apos;%&apos;&quot;
&gt;"/><x id="INTERPOLATION" equiv-text="{{ percentage | number: &apos;1.0-0&apos; }}"/>%<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span
Expand Down
20 changes: 19 additions & 1 deletion apps/agenstra/frontend-agent-console/src/i18n/messages.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -2953,11 +2953,14 @@
<trans-unit id="featureTicketsBoard-runFailureAgentProviderError" datatype="html">
<source>Agent provider error</source>
</trans-unit>
<trans-unit id="featureTicketsBoard-runFailureAgentNoCompletionStatus" datatype="html">
<source>No completion status</source>
</trans-unit>
<trans-unit id="featureTicketsBoard-runFailureAgentNoCompletionMarker" datatype="html">
<source>No completion marker</source>
</trans-unit>
<trans-unit id="featureTicketsBoard-runFailureMarkerWithoutVerify" datatype="html">
<source>Completion marker without verify profile</source>
<source>Completion without verify profile</source>
</trans-unit>
<trans-unit id="featureTicketsBoard-runFailureVerifyCommandFailed" datatype="html">
<source>Verify command failed</source>
Expand Down Expand Up @@ -3088,9 +3091,24 @@
<trans-unit id="featureTicketsBoard-agentLabel" datatype="html">
<source> Agent for chat / AI </source>
</trans-unit>
<trans-unit id="featureTicketsBoard-modelLabel" datatype="html">
<source> Model for chat / AI </source>
</trans-unit>
<trans-unit id="featureTicketsBoard-modelPlaceholder" datatype="html">
<source> {{ ticketAiModelsLoading() ? 'Loading models…' : 'Select agent to load models…' }} </source>
</trans-unit>
<trans-unit id="featureTicketsBoard-agentPlaceholder" datatype="html">
<source>Select agent…</source>
</trans-unit>
<trans-unit id="featureTicketsBoard-automationPreferredModel" datatype="html">
<source>Model for automation</source>
</trans-unit>
<trans-unit id="featureTicketsBoard-automationPreferredModelHint" datatype="html">
<source> Required when automation is enabled. OpenCode has no auto model selection. </source>
</trans-unit>
<trans-unit id="featureTicketsBoard-automationModelPlaceholder" datatype="html">
<source> {{ ticketAiModelsLoading() ? 'Loading models…' : 'Select an agent to load models…' }} </source>
</trans-unit>
<trans-unit id="featureTicketsBoard-acceptanceLabel" datatype="html">
<source> Acceptance <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;text-muted small d-block fw-normal&quot; [attr.aria-label]=&quot;percentage + &apos;%&apos;&quot;
&gt;"/><x id="INTERPOLATION" equiv-text="{{ percentage | number: &apos;1.0-0&apos; }}"/>%<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span
Expand Down
2 changes: 1 addition & 1 deletion docs/agenstra/ai-agents/agent-client-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Built-in provider:
- **User chats** — `-chat-{uuid}` where `{uuid}` is the chat session id (one ACP session per user-created thread)
- **Reserved / hidden** — not listed in agent `chats` / UI: `-prompt-enhance`, `-ticket-body`, and `-ticket-auto-*` (for example `-ticket-auto-pre`, `-ticket-auto-loop`, `-ticket-auto-commit-msg`)
After an API restart, the manager opens a new stdio transport and calls `session/load` when the container id still matches. That covers primary, user, and background sessions the same way in-memory reuse already did within a process.
- **Permissions:** `session/request_permission` is auto-approved when `ACP_AUTO_APPROVE` is not `false` (default for headless agents)
- **Permissions:** `session/request_permission` is auto-approved when `ACP_AUTO_APPROVE` is not `false` (default for headless agents). **OpenCode ticket automation does not use this flag** — reserved `-ticket-auto-*` sessions use OpenCode session permission rulesets and runtime auto-reply instead (see [Ticket automation](../features/ticket-automation.md)).

## Configuration

Expand Down
15 changes: 13 additions & 2 deletions docs/agenstra/features/ticket-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,29 @@ There is no separate “start run” HTTP call for this path: eligible tickets a
- `new_per_run`: ephemeral branch `automation/run/{first 8 chars of run UUID}` per run.
- `force_new_automation_branch_next_run`: with `reuse_per_ticket`, forces one ephemeral branch for the next run, then clears the flag.
4. **Optional pre-improve** If autonomy `pre_improve_ticket` is true, one chat turn asks the agent to clarify the ticket only (no implementation).
5. **Implementation loop** Up to `max_iterations` remote chat turns (default `20`). The agent must include the exact completion marker `AGENSTRA_AUTOMATION_COMPLETE` in assistant text to exit the loop successfully.
5. **Implementation loop** Up to `max_iterations` remote chat turns (default `20`). Each turn uses an OpenCode automation session with session-scoped allow-all permissions and structured `json_schema` turn status. The loop exits successfully only when structured output reports `status: "complete"`.
6. **Verification** If a `verifier_profile` with `commands` is configured, those commands run in the agent workspace (bounded timeout); any non-zero exit fails the run.
7. **Finalize** Stage changes, generate a Conventional-Commits-style subject (with fallback), `git commit` and `git push` unless the tree is already clean.
8. **Success** Run status `succeeded`, ticket status set to **`prototype`**, failure counters cleared, lease released, board and activity events emitted.

### Unattended OpenCode sessions

Reserved resume-session suffixes (`-ticket-auto-pre`, `-ticket-auto-loop`, `-ticket-auto-commit-msg`) create **hidden ephemeral** OpenCode sessions that:

- Apply a session-scoped permission ruleset (`allow` for all actions) so interactive ask/deny from the worker UI config does not block the run.
- Auto-reply residual permission asks (`always`) and questions on those sessions only; interactive chat suffixes are unchanged.
- Force the platform agent `agenstra-automation` and skill `agenstra-ticket-automation` (injected at config sync so UI overlays cannot remove them). The skill documents the structured-status protocol; completion detection uses structured output only.
- Keep transcripts out of the user’s primary chat history; run progress still surfaces via automation chat cards / board events.

Ticket approval and agent autonomy prerequisites still apply. Only reserved automation sessions bypass interactive permission prompts.

Remote chat turns and commit-message generation are recorded in usage statistics as `autonomous_ticket_run_turn` and `autonomous_ticket_commit_message` interaction kinds (see [Usage statistics](./usage-statistics.md)).

## Failures and retries

Failures map to a terminal run status (`failed`, `timed_out`, `escalated`, `cancelled`) and may adjust ticket status. When policy says **requeue**, `next_retry_at` is set to approximately **one minute** ahead so the scheduler does not hot-loop; `consecutive_failure_count` increments.

Examples (not exhaustive): missing completion marker or budget-related timeout tend to move the ticket back toward `todo` and requeue; human escalation stops requeue; lease contention skips starting a run but schedules a retry. Product policy is centralized in code (`routeAutomationFailure` in the agent-controller library); adjust there when behavior changes.
Examples (not exhaustive): missing structured completion status or budget-related timeout tend to move the ticket back toward `todo` and requeue; human escalation stops requeue; lease contention skips starting a run but schedules a retry. Product policy is centralized in code (`routeAutomationFailure` in the agent-controller library); adjust there when behavior changes.

## HTTP and realtime

Expand Down
7 changes: 6 additions & 1 deletion graph/graph.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"generatedAt": "2026-10-04T15:47:00.617Z",
"generatedAt": "2026-10-05T19:13:17.421Z",
"nodes": [
{
"id": "project:@forepath/test/mounted-plugin-fixture",
Expand Down Expand Up @@ -60821,6 +60821,11 @@
"to": "file:libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/ticket-automation.service.ts",
"type": "injects"
},
{
"from": "file:libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/autonomous-run-orchestrator.service.ts",
"to": "file:libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/knowledge-tree.service.ts",
"type": "injects"
},
{
"from": "file:libs/domains/agenstra/backend/feature-agent-controller/src/lib/services/client-agent-autonomy.service.ts",
"to": "file:libs/domains/agenstra/backend/feature-agent-controller/src/lib/repositories/clients.repository.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ flowchart TD
workspacePrep -- "No" --> failProvider["Fail run: AGENT_PROVIDER_ERROR"]
workspacePrep -- "Yes" --> loopStart["Agent loop (maxIterations)"]

loopStart --> markerSeen{"Completion marker seen?"}
markerSeen -- "No, iterations exhausted" --> failNoMarker["Fail run: AGENT_NO_COMPLETION_MARKER (timed_out)"]
markerSeen -- "Yes" --> verifierConfigured{"Verifier commands configured?"}
loopStart --> signalSeen{"Structured status complete?"}
signalSeen -- "No, iterations exhausted" --> failNoSignal["Fail run: AGENT_NO_COMPLETION_STATUS (timed_out)"]
signalSeen -- "Yes" --> verifierConfigured{"Verifier commands configured?"}

verifierConfigured -- "No" --> verifySkipped["Skip verify"]
verifierConfigured -- "Yes" --> verifyResult{"All verifier commands exit 0?"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ sequenceDiagram
end
Note over Orch,Vcs: Default strategy reuses `automation/ticket-{ticketId}`; `new_per_run` or one-shot force uses ephemeral `automation/{runPrefix}`.
loop maxIterations
Orch->>Chat: sendChatSync(sync, ephemeral, contextInjection)
Orch->>Chat: sendChatSync(sync, ephemeral, expectAutomationTurnStatus)
Chat->>Mgr: chat
Mgr->>Prov: sendMessage(continue, resumeSessionSuffix)
Prov-->>Mgr: NDJSON / text
Mgr-->>Chat: chatMessage
Chat-->>Orch: assistant text
Mgr->>Prov: sendMessage(continue, resumeSessionSuffix, format json_schema)
Prov-->>Mgr: structured status + text
Mgr-->>Chat: chatMessage (final result)
Chat-->>Orch: RemoteChatSyncResult (text, turnStatus)
Orch->>DB: append run step
end
Note over Orch,Chat: contextInjection carries includeWorkspace, environmentIds, and autoEnrichmentEnabled.
Note over Orch,Chat: Loop exits only when turnStatus is complete. Automation sessions use allow-all + auto-reply.
Orch->>Vcs: runVerifierCommands
Orch->>Vcs: getStatus; if dirty: stage all
Orch->>Chat: sendChatSync (conventional commit subject; autonomous_ticket_commit_message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7660,6 +7660,11 @@ components:
type: string
format: uuid
description: Preferred workspace agent for chat/AI when viewing this ticket (agent-manager agent id).
preferredChatModel:
type: string
maxLength: 256
nullable: true
description: Preferred OpenCode provider/model for chat/AI on this ticket (e.g. anthropic/claude-sonnet-4).
automationEligible:
type: boolean
description: Whether autonomous prototyping is enabled for this ticket (ticket_automation.eligible).
Expand Down Expand Up @@ -7745,6 +7750,11 @@ components:
type: string
format: uuid
description: Persist preferred workspace agent for chat/AI; send null to clear.
preferredChatModel:
type: string
maxLength: 256
nullable: true
description: Persist preferred OpenCode provider/model for chat/AI; send null to clear.
TicketCommentResponseDto:
type: object
required: [id, ticketId, body, createdAt]
Expand Down Expand Up @@ -7829,6 +7839,11 @@ components:
items:
type: string
format: uuid
preferredModel:
type: string
maxLength: 256
nullable: true
description: OpenCode provider/model for autonomous turns; required when eligible is true.
includeWorkspaceContext:
type: boolean
contextEnvironmentIds:
Expand Down Expand Up @@ -7862,6 +7877,7 @@ components:
- ticketId
- eligible
- allowedAgentIds
- preferredModel
- includeWorkspaceContext
- contextEnvironmentIds
- autoEnrichmentEnabled
Expand All @@ -7888,6 +7904,11 @@ components:
items:
type: string
format: uuid
preferredModel:
type: string
maxLength: 256
nullable: true
description: OpenCode provider/model used for autonomous turns on this ticket.
includeWorkspaceContext:
type: boolean
contextEnvironmentIds:
Expand Down Expand Up @@ -7978,7 +7999,7 @@ components:
- finishedAt
- updatedAt
- iterationCount
- completionMarkerSeen
- completionSignalSeen
- verificationPassed
- failureCode
- summary
Expand Down Expand Up @@ -8021,7 +8042,7 @@ components:
format: date-time
iterationCount:
type: integer
completionMarkerSeen:
completionSignalSeen:
type: boolean
verificationPassed:
type: boolean
Expand All @@ -8030,8 +8051,8 @@ components:
description: >
Present when a run ends unsuccessfully. Includes values such as verify_command_failed,
commit_failed (git commit after successful verification), push_failed (git push after that commit),
agent_no_completion_marker, marker_without_verify, agent_provider_error, vcs_dirty_workspace,
and others defined by the controller.
agent_no_completion_status, agent_no_completion_marker (legacy), marker_without_verify, agent_provider_error,
vcs_dirty_workspace, and others defined by the controller.
summary:
type: object
additionalProperties: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export class TicketAutomationResponseDto {
ticketId!: string;
eligible!: boolean;
allowedAgentIds!: string[];
preferredModel!: string | null;
includeWorkspaceContext!: boolean;
contextEnvironmentIds!: string[];
autoEnrichmentEnabled!: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class TicketAutomationRunResponseDto {
finishedAt!: Date | null;
updatedAt!: Date;
iterationCount!: number;
completionMarkerSeen!: boolean;
completionSignalSeen!: boolean;
verificationPassed!: boolean | null;
failureCode!: string | null;
summary!: Record<string, unknown> | null;
Expand Down
Loading
Loading