feat(temporal): run the trajectory workflow, so a turn's record closes #1057

Merged
coilysiren merged 2 commits from feat/trajectory-completes into main 2026-08-19 05:09:11 +00:00
Member

Closes #1041. The workflow was already written. Nothing ran it.

What I got wrong on #1041, corrected

I filed that issue saying there was no worker, then commented that adding one would reverse #887's "mirroring, not orchestration" decision, and withdrew the suggestion. That was wrong, and I only found out by reading the file rather than the issue.

ToolTrajectoryWorkflow has been sitting in internal/community/temporalmirror.go since the mirror shipped:

// ToolTrajectoryWorkflow accumulates one turn's records and ends when they stop.
// It performs no activity.
func ToolTrajectoryWorkflow(ctx workflow.Context) ([]ToolCallRecord, error) {

It accumulates signals and returns on an idle timer. It runs no activity, the turn never waits on it, and no retry policy touches a tool call - which are precisely #887's two named hazards, so registering it reinstates nothing that decision rejected. The author clearly intended a worker and it was never wired up.

The measurements

DescribeTaskQueue sirens-dowel-tool-mirror   Workflow pollers=0  Activity pollers=0
DescribeTaskQueue sirens-deep-tool-mirror    Workflow pollers=0  Activity pollers=0

namespace coilyco.gcdqf:  Running 12   Completed 0   Terminated 1   TimedOut 60

Signalled onto a queue nothing polls, open until trajectoryLifetime, retired TimedOut. Zero completions ever.

The change

Two things.

Register the workflow. startTrajectoryWorker builds a worker on the client the mirror already holds, registers ToolTrajectoryWorkflow under the ToolTrajectory name the mirror starts by string, and stops it before the client closes. A failed start is logged and the mirror keeps recording: a record that does not close is worth more than no record.

trajectoryIdle 10 minutes to 2. This is the reason a completed turn would still linger. It bounds the gap between one turn's calls, not the turn, so it only needs to clear the slowest model round - and the median turn is 32.9s per #932. Two minutes has room and stays overridable by SIRENS_ECHO_TRAJECTORY_IDLE.

I did not go lower. If it drops under a model round, a late tool call lands after the workflow closed and SignalWithStart opens a second run under the same id, splitting one turn across two trajectories. That is worse for reading than a two-minute tail.

Tests

Two, through Temporal's own testsuite so the workflow really executes:

  • TestToolTrajectoryCompletesOnceTheCallsStop - two signals arrive, the workflow completes, and both records come back in order.
  • TestToolTrajectoryClosesWithoutASecondCall - a turn with one call still closes rather than sitting open.

Nothing asserted completion before, which is how a workflow with no runner shipped unnoticed.

What this does not do

The 60 existing TimedOut executions stay as they are. This changes what new turns do.

CI

go test ./... is clean apart from TestRenderedPromptsStayInsideTheirBudget, which main is currently red on and which #1055 fixes. This branch touches no policy root and neither failing profile.

Closes #1041. **The workflow was already written. Nothing ran it.** ## What I got wrong on #1041, corrected I filed that issue saying there was no worker, then commented that adding one would reverse #887's "mirroring, not orchestration" decision, and withdrew the suggestion. That was wrong, and I only found out by reading the file rather than the issue. `ToolTrajectoryWorkflow` has been sitting in `internal/community/temporalmirror.go` since the mirror shipped: ```go // ToolTrajectoryWorkflow accumulates one turn's records and ends when they stop. // It performs no activity. func ToolTrajectoryWorkflow(ctx workflow.Context) ([]ToolCallRecord, error) { ``` It accumulates signals and returns on an idle timer. **It runs no activity, the turn never waits on it, and no retry policy touches a tool call** - which are precisely #887's two named hazards, so registering it reinstates nothing that decision rejected. The author clearly intended a worker and it was never wired up. ## The measurements ``` DescribeTaskQueue sirens-dowel-tool-mirror Workflow pollers=0 Activity pollers=0 DescribeTaskQueue sirens-deep-tool-mirror Workflow pollers=0 Activity pollers=0 namespace coilyco.gcdqf: Running 12 Completed 0 Terminated 1 TimedOut 60 ``` Signalled onto a queue nothing polls, open until `trajectoryLifetime`, retired `TimedOut`. Zero completions ever. ## The change Two things. **Register the workflow.** `startTrajectoryWorker` builds a worker on the client the mirror already holds, registers `ToolTrajectoryWorkflow` under the `ToolTrajectory` name the mirror starts by string, and stops it before the client closes. A failed start is logged and the mirror keeps recording: a record that does not close is worth more than no record. **`trajectoryIdle` 10 minutes to 2.** This is the reason a completed turn would still linger. It bounds the gap *between* one turn's calls, not the turn, so it only needs to clear the slowest model round - and the median turn is 32.9s per #932. Two minutes has room and stays overridable by `SIRENS_ECHO_TRAJECTORY_IDLE`. I did not go lower. If it drops under a model round, a late tool call lands after the workflow closed and `SignalWithStart` opens a **second** run under the same id, splitting one turn across two trajectories. That is worse for reading than a two-minute tail. ## Tests Two, through Temporal's own `testsuite` so the workflow really executes: * `TestToolTrajectoryCompletesOnceTheCallsStop` - two signals arrive, the workflow **completes**, and both records come back in order. * `TestToolTrajectoryClosesWithoutASecondCall` - a turn with one call still closes rather than sitting open. Nothing asserted completion before, which is how a workflow with no runner shipped unnoticed. ## What this does not do The 60 existing `TimedOut` executions stay as they are. This changes what new turns do. ## CI `go test ./...` is clean apart from `TestRenderedPromptsStayInsideTheirBudget`, which **main is currently red on** and which #1055 fixes. This branch touches no policy root and neither failing profile.
feat(temporal): run the trajectory workflow, so a turn's record closes
Some checks failed
ci / image-build (pull_request) Successful in 38s
ci / test (pull_request) Failing after 45s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
da21ebdd59
ToolTrajectoryWorkflow has been in this package since the mirror shipped.
Nothing ever registered it, so every trajectory was signalled onto a task
queue with no poller, sat open to trajectoryLifetime, and retired
TimedOut. DescribeTaskQueue on both lanes reports pollers=0, and the
namespace holds 60 TimedOut against 0 Completed.

Registering it is the whole fix. This is not the orchestration #887
rejected: the workflow runs no activity, the turn never waits on it, and
no retry policy touches a tool call. Those were that decision's two named
hazards and neither is reintroduced. A failed worker start is logged and
the mirror keeps recording, because a record that does not close is worth
more than no record.

trajectoryIdle drops from 10 minutes to 2. It bounds the gap between one
turn's calls rather than the turn, and 10 minutes kept a finished turn
open long past anything worth reading on a stream. Two minutes clears the
32.9s median turn with room and stays env-overridable.

Closes #1041.

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Merge branch 'main' into feat/trajectory-completes
All checks were successful
ci / image-build (pull_request) Successful in 36s
ci / test (pull_request) Successful in 1m14s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
07c1cbbf33
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!1057
No description provided.