Missions · Watches · Wakes

How missions persist authority, sleep, and wake.

A mission stores delegated trading authority: an instruction, allocated capital, risk ceilings, and one bound chat. Watches are persisted conditions that can wake it. A wake acquires a one-run decision lease, writes a fresh snapshot into the turn, and resumes the chat.

watch armed trading_watches · active candle arrives WS candle delivery §13 fires once markTriggered + inbox key decision lease requestRun · §12.3 wake snapshot TradingWakeupComposer turn starts thread.turn.start THE SLEEP-WAKE LOOP the run settles · ensureNotDeaf re-arms the floor
closed loop: every settled run re-arms coverage before the lease path goes quiet TradingTurnCoordinator.ts:437

A mission is durable, delegated authority.

A mission carries an instruction (the mandate), allocated capital, an authority envelope of ceilings and risk policy, a bound agent runtime, called the harness in this subsystem (one provider instance and one chat thread), and the markets it may trade. A model turn is temporary. The mission rows written by TradingMissionService.ts persist after it ends. Its header names the two things it owns: mission creation with the one-active-mission invariant, and the §11.1 status transitions (TradingMissionService.ts:2-8).

One installation, one active mission. The invariant is scoped to a user, and upstream T3 is a single-user local server, so the reactor pins one owner, LOCAL_TRADING_USER_ID = "local", rather than inventing an identity contract the spec has not published (TradingMissionReactor.ts:136-143). A create that would overlap fails with TradingMissionAlreadyActiveError unless the existing mission is in a permanent terminal state (TradingMissionService.ts:119-126).

The explicit form is not the only door. A chat that has never held a mission can publish a plan or place an entry, and the first such call takes authority on the market it names. TradingAuthorityBinding.ts runs the same createMission, so the D4 per-market exclusivity check and the manual-exposure check are the ones already written; The service does not dispatch another command or start another run. The caller is already waiting inside the tool call, and the current turn already needs the authority. "An auto-created mission is a mission" (TradingAuthorityBinding.ts:1-18). It walks initializing → analysing → waiting inline, both edges now, and calls adoptTurn so the chat turn holds the decision lease every execution check asks about (TradingAuthorityBinding.ts:393-407).

A held market refuses, and the refusal names the holder. The refusing text quotes the holding chat's title, because the trade home lists positions and a waiting mission is invisible there; the gap is said out loud rather than papered over, and no new navigation is built to close it (TradingAuthorityBinding.ts:86-112).

D4 · one market, one hand

A market can be held by a mission or by the owner's manual trading, never both. The mission side refuses when the market is taken or manually exposed; the manual ticket refuses with market_owned_by_mission, "named so the refusal reads as the rule it is" (TradingManualEntryService.ts:13-16).

There is a third shape worth knowing: an observe mission is a mission in every way that matters to the runtime, a row, a thread binding, wakes, a stand-down and a resume, and in no way that reaches the exchange. Its allocation is 1 dollar, "the nearest thing to nothing it can hold", because a zero allocation would be a mission that cannot work rather than one that must not; every execution call is refused before a size is computed, and the mandate's last clause is the whole mission: "Never trade." (TradingAuthorityBinding.ts:429-431, 530-547).

Ten statuses, one table, no invented edges.

The §11.1 state machine is encoded directly in MissionTransitions.ts. A harness run may not invent transitions, and the deterministic services may only move a mission to blocked for one of the four persisted reasons. A companion test asserts the encoded table equals the published one edge for edge, so the meaning of each edge stays with the spec (MissionTransitions.ts:1-12, 38-45).

10
statuses in the §11.1 table, with no self-transitions
5
in the active loop, the only set the runtime may wake of its own accord
3 suspended
paused, agent_unavailable, blocked: authority held, exposure refused
2 terminal
revoked, completed: no outgoing transitions, ever
ACTIVE LOOP · MAY WAKE ITSELF SUSPENDED TERMINAL initializing analysing waiting executing position_open paused agent_unavailable 2 failed runs blocked blockedReason required revoked completed resume revalidates, then analysing (§14.6) any active state may exit
the active loop suspended: authority held, slot held terminal: no outgoing edges resume path (dashed)
the loop
initializing, analysing, waiting, executing, position_open, with edges verbatim from the table: waiting → [analysing, executing], executing → [position_open, waiting], position_open → [waiting, analysing, executing] (MissionTransitions.ts:47-55). One edge has two actors: analysing → waiting is taken both by publishing a plan and by arming a watch while a published plan exists; both take the same edge, the table stays one edge (MissionTransitions.ts:44-46).
suspended
paused, agent_unavailable, blocked block new discretionary exposure but still hold mission authority, so they continue to occupy the single active-mission slot; each is left only by an explicit user, provider, or harness action (MissionTransitions.ts:21-26). A paused or blocked mission may still hold a position and its protective orders (MissionTransitions.ts:63-69).
terminal
revoked permanently ends mission authority; completed means the harness declared the objective done. Neither has any outgoing transition (MissionTransitions.ts:15-19). Waking one would ask the harness to reassess something it has been explicitly told to stop reassessing, so the runtime never does (MissionTransitions.ts:71-80).
blockedReason
Entering blocked without a persisted reason is rejected, and a reason on any other status is rejected as meaningless: blocked_reason_required, blocked_reason_not_allowed (MissionTransitions.ts:117-130).
resume
A suspended mission resumes into a fresh harness turn, and §14.6 revalidates account, protection, authority, and provider availability before the harness reassesses (MissionTransitions.ts:96-100). A mission blocked for cumulative loss is harder to leave: the guard refuses harness self-resume, only an explicit user resume clears it (TradingExecutionGuard.ts:259-271).

A watch is a typed predicate that fires exactly once.

A watch is a simple, deterministic, typed predicate persisted against the mission that registered it, not against a plan revision: a revision does not touch it (plan 29 step 4.2). TradingWatchService.ts owns the lifecycle writes: register, cancel, mark triggered, consume (TradingWatchService.ts:2-9). The agent runtime arms watches through the trading_watch tool. The server runtime arms others and stamps armedReason so a wake can explain why it happened.

Two durable guards · fires exactly once

Firing flips the watch active → triggered through an atomic UPDATE that only wins on an active row, so a concurrent replay, supersede, or cancel drops the firing; and the wake event is persisted with a deduplication key scoped per watch, so a replay after a restart cannot generate a second wake-up. "The evaluator never starts a run itself; the turn coordinator owns the lease and the wake path. A watch firing does not authorize a position (§12.4)" (WatchEvaluator.ts:11-25).

price_cross · candle_close

One level, two confirmations: touch fires the moment the level trades; close waits for the candle to confirm it (trading-contracts/watch.ts:419-421). Candle-close watches are evaluated the moment a WS candle arrives; the slow sweep covers everything price-shaped (WatchEvaluator.ts:4-9).

scheduled_reassessment

A clock. The sweep fires it once its runAt has passed (WatchEvaluator.ts:7-9). A plan's own reassess.afterMinutes is a cadence measured from the look that just ended, floored at 5 minutes: read as a deadline it once produced a wake-at-now hot loop the model could only escape by republishing an unchanged plan (trading-contracts/watch.ts:1305-1329).

pnl_above · pnl_below

Levels in dollars on the winning and losing side of the live position's unrealized PnL, which is exactly why they are position-scoped: zero and meaningless when flat (trading-contracts/watch.ts:1043-1047). The runtime arms pnl_below at about 70% of the planned loss as a stop_decision wake: thesis broken, or noise? The exchange-native reduce-only stop stays resting as the backstop (trading-contracts/watch.ts:985-988).

stop_proximity

The runtime arms a price_cross one ATR ahead of the resting stop while a position is held. A wake from it is the designed moment to decide the stop deliberately, tighten, hold, or exit, before the exchange decides instead (trading-contracts/watch.ts:980-983).

prediction_horizon + prediction_invalidation

publish projection horizon clock · byMinutes + invalidation · invalidationPrice

The only pair the runtime arms from a plan's projection, automatically, on publish. The horizon wake asks "did it happen?"; the invalidation wake says "it didn't, and here is the proof". Between them the mission can genuinely sleep, because the only two things that could change its mind are both armed (TradingPredictionWatches.ts:1-22, trading-contracts/watch.ts:995-1000).

The coverage floor: a holding mission is never deaf

3 bars
holding a position: 3 bars of the primary timeframe, clamped to 2-15 minutes. A 1m holder reassesses inside 3 minutes, a 1h holder at the 15-minute cap.
10 bars
flat with a thesis: 10 bars clamped to 5-30 minutes. Consecutive no-op wakes stretch it 10m, 20m, 40m, then hourly; any real event resets the count.

Cadence is unconditional for a holder, covered or not: a live 1m mandate once had seven armed watches, took the "covered" branch, and had no runtime-armed reassessment for its entire hold; its only clock was the model's own projection horizon (trading-contracts/watch.ts:1269-1303, TradingTurnCoordinator.ts:787-797, 720-724).

Evaluator cadence

sweep interval (price_cross, scheduled)2 s
follow-set sync (new market armed)30 s
direct candle intervals subscribed5
candles embedded in a wake5

WatchEvaluator.ts:90-104; a watch on a newly followed market starts delivery-driven evaluation within half a minute, and the sweep already covers everything until then.

Why position-scoped watches retire on flat

On the mission this was measured on, four profit targets armed for the first long fired three hours later against an unrelated trade, and two armed for a short fired on a long. Each was evaluated against the live position's PnL, because a pnl_above "asks the position what it is worth and does not ask which position" (TradingMissionReactor.ts:2073-2088). Retirement is wired into both ways a mission goes flat; the first wiring saw only the watchdog shape, so six harness-driven closes in a row retired nothing. A related live finding: a mission was woken 5m43s after its position closed by the target level of the position that no longer existed (trading-contracts/watch.ts:1019-1026).

One deliberate consequence runs against intuition: the take-profit target is a wake, not an order. Plan 36 item 6 made it so, and reconcileTakeProtection now withdraws any resting take-profit an earlier build placed. The protective stop remains on the exchange and is non-negotiable. The mission evaluates the profit target when awake (TradingProtectionService.ts:33-40).

A wake is a run, and a run is leased.

"Wake" is not a notification; it is the act of resuming the bound provider session as a recorded harness run, one at a time per mission. TradingTurnCoordinator.ts is the single gate between an observed event and a started run (TradingTurnCoordinator.ts:1-11).

announce

The evaluator announces, it does not act

On a match it flips the watch active → triggered, persists an inbox event keyed for deduplication, and announces trading.mission-watch-fired on the orchestration stream. The reactor turns that announcement into a run request (WatchEvaluator.ts:11-14).

queue

A fired watch behind an active run retries

processWatchFired requests a run; on queued_behind_active_run it sleeps 5 seconds and tries again, at most 60 times. The inbox event stays pending either way, so the next run still sees it; the retry stops the moment the event is no longer pending, which means a run claimed it and a follow-up resume would be redundant (TradingMissionReactor.ts:126-133, 998-1023).

checks

§12.3 pre-run checks, in their listed order

Mission active. Wake budget: runs are the count and the authority version is the epoch; at the cap the mission blocks visibly with wake_budget_exhausted, and a user resume re-issues the envelope, moving the epoch and resetting the counter. Provider binding present. Provider instance available (TradingTurnCoordinator.ts:1098-1152).

lease

Acquiring the lease is the check

The lease is an INSERT of a starting run. The partial unique index idx_trading_harness_runs_one_active_per_mission (migration 035) makes a concurrent second insert a unique violation, which the coordinator catches and reports as queued_behind_active_run: exactly one non-terminal run per mission can ever exist (TradingTurnCoordinator.ts:272-306). Only after the lease does it claim the pending inbox events, so a superseded event cannot drive a run (TradingTurnCoordinator.ts:1158-1163).

snapshot

The composer writes the turn's opening text

TradingWakeupComposer is the single place that assembles what a resumed turn is told: what woke it, the mark, the position, one cost line, the plan's numbers, the armed set, and pointers at trading_look for everything else. The serialized wakeup is written into the resumed turn's message.text, the same bounded payload whatever the cause. It carries five recent candles, down from eight, because each embedded bar is a line repeated for the life of the thread (TradingWakeupComposer.ts:1-19, 101-110).

dispatch

thread.turn.start, never the provider directly

The coordinator dispatches through the orchestration engine; dispatch is the only sanctioned way for trading code to resume a session (§6.3), and the persisted resumeCursor is what makes the resumed turn continue the same conversation (TradingTurnCoordinator.ts:13-18). The turn-end watcher is forked before the dispatch so a fast turn cannot slip past it (TradingTurnCoordinator.ts:1168-1176).

release

The lease leaves with the turn

The watcher waits for the session to run the turn, then for the first session-set where it leaves running; starting is deliberately excluded, or every run would release its lease before the harness said anything. It marks the run completed or failed, flips the run's inbox events to consumed, re-arms coverage, and delivers any operator messages that queued behind the run (TradingTurnCoordinator.ts:350-366, 412-438). A dispatched turn that never starts releases the lease after 10 minutes (TradingTurnCoordinator.ts:368-377).

The operator is inside the same machinery. Typing into a bound thread used to take the ordinary turn path: no wakeup snapshot, no decision lease, so the operator's turn could race a watch-fired run and work from whatever state the harness had left in its context. A message on an operative mission is an event like any other; it gets the same fresh snapshot, and if the lease is held it is queued and delivered the moment the run releases it (TradingTurnCoordinator.ts:112-131, 441-474).

Publish a projection, then genuinely sleep.

A plan's projection says where price is going, by when, and what would prove it wrong. Those three facts are exactly a pair of triggers, a clock and a level, and before this existed the model had to write them itself, which meant a plan could be published with a confident read and nothing armed to wake it. Such a mission either sat blind or was woken by the coverage floor on a cadence that had nothing to do with what it believed (TradingPredictionWatches.ts:3-15).

So the runtime arms them, and nothing in that path is allowed to fail a publish: the plan is durable before this runs, and a mission whose prediction watches could not be armed wakes on its coverage floor instead, "which is worse but not unsafe" (TradingPredictionWatches.ts:17-20). A revision that moves the target and leaves the invalidation where it was re-levels through one transaction, so the mission is never momentarily unwatched on the side being moved; an identical level republished keeps its row and its history, and only the version is re-stamped (TradingPredictionWatches.ts:67-80). A later revision sweeps the prediction watches of the version it replaces, the only armed reasons it may sweep (trading-contracts/watch.ts:1013-1017).

Sleeping is the design goal, not an idle state that needs excusing: between the horizon clock and the invalidation level, the only two things that could change the mission's mind are armed, and the coverage floor guarantees it will still be asked whether the read holds (TradingPredictionWatches.ts:12-16).

The mission remembers, and the boot sweep does not delete.

Memory comes in two layers, and both outlive any single turn. The plan document is the workspace's TRADE.md, persistent human-readable trading context, the peer of AGENTS.md: the agent reads it with its native tools and acts through the typed trading tools (TradingPlanDocument.ts:1-5). The journal is append-only, two operations and no third, because the plan document is replaced on every revision and everything the model wanted to remember across revisions was being lost with it (TradingJournalService.ts:2-10).

trade.md
One row per workspace root pins one activated revision, and a restart recovers the active revision, hash, and snapshot from SQLite alone. Drift between the live file and the pinned revision is classified as observable state, not error (TradingPlanDocument.ts:6-17).
journal
Append one note, read the recent ones back; no update and no delete. A note is refused rather than truncated or silently dropped (TradingJournalService.ts:2-14).
strategy versions
The composed wakeup carries the current plan's numbers and armed set, so a target or a cadence survives the turn that set it (TradingWakeupComposer.ts:5-9).
boot sweep
An orphan is a mission whose thread was deleted out from under it while the server was down. Nothing can wake it, nothing can settle it, and it holds authority over its market forever. The sweep withdraws that authority by revoking: mission rows are the permanent record of what was traded (plan 27 H1), and revoking is safe even if the orphan test is ever wrong again (TradingMissionSweep.ts:2-15).
exposure first
An orphan still holding a position is left alone entirely: revoking it would leave real exposure nobody is authorized to manage. It is reported, not touched; live deletion goes through the reactor's close-then-revoke path instead (TradingMissionSweep.ts:16-20).
lease-gated
The sweep only runs while the process holds the trading lease, so a second runtime against the same database cannot revoke missions the live holder still believes in. Idempotent and cheap: on a clean database it finds nothing (TradingMissionSweep.ts:22-25, 50-60).
The soak a projection rebuild erased

An earlier version of the sweep did delete, and read orphanhood off projection_threads: the first projection rebuild made every live mission look orphaned, and the boot sweep erased a running soak. That is the whole reason the sweep now revokes instead of deleting, and why its orphan test being wrong again would cost authority, not history (TradingMissionSweep.ts:9-15).

Go deeper.

Every claim above traces to these files. The reading order that repays itself: transitions first, then the evaluator, then the coordinator with the composer open beside it.

Lifecycle

  • MissionTransitions.ts the §11.1 table, verbatim
  • TradingMissionService.ts creation, transitions, one-active invariant
  • TradingAuthorityBinding.ts authority from a chat turn, observe missions
  • TradingMissionSweep.ts boot revocation, lease-gated
  • TradingPlanDocument.ts TRADE.md pinning and drift

Cognition

  • WatchEvaluator.ts evaluation, firing, the two guards
  • TradingWatchService.ts the persisted watch registry
  • TradingTurnCoordinator.ts §12.3, lease, dispatch, release
  • TradingWakeupComposer.ts the bounded §12.2 snapshot
  • TradingPredictionWatches.ts the auto-armed pair
  • TradingEventInbox.ts deduplicated mission events

Contracts

  • trading-contracts/watch.ts types, armed reasons, coverage floor
  • TradingMissionReactor.ts:126-133 the 5 s × 60 queue retry
  • TradingMissionReactor.ts:2073-2088 position-scoped retirement
  • TradingProtectionService.ts:33-40 the target as a wake
  • TradingPlanWake.test.ts plan-aware wakes under test

Where this connects: the envelope a mission carries is re-checked at every execution preview (Execution); exhaustion moves a mission to blocked and only an explicit user resume clears it (Risk control); the interim signer resolves under the mission's account, fail-closed (Signer & authority); observe missions and the keyless archive never reach an order (Research mode); every step above rides the typed command and event spine (Event spine); and the profit-target and erased-soak stories have case files (Failure stories).