Skip to content
Cirrus home
Sign in

Changelog

Every change you can see, newest first. Additive changes never break a client that ignores unknown fields; breaking ones are announced before they ship.

  1. Additive

    v2 trigger capabilities

    New operation GET /v2/triggers/capabilities (get_trigger_capabilities_v2, read scope; users /v2/triggers is enabled for): TriggerV2Capabilities with kinds (each kind's spec fields), expressions (variables, functions incl. crosses_above / crosses_below, operators, source / depth / node limits), limits (this deployment's TRG_MAX_*) and, with ?account=, account (broker_protection_enabled, the broker order types that can hold protection, and each protection fact verified or unverified). 404 account_not_found for an account that is not the caller's. Nothing changes for existing operations.

  2. Additive

    validate a v2 trigger without creating it

    New operation POST /v2/triggers/validate (validate_trigger_v2), same body and auth as create_trigger_v2 (triggers scope; users /v2/triggers is enabled for). Nothing is published, stored or counted against the account's order budget. 200 with TriggerV2Validation: valid, errors[] {field, message} (create's 422 messages), reads {symbols, fields, variables}, limits (legs / expressions / bytes and their maxima, ok, reason), protection (per part held_by broker|server, level, adjusted_by_server, note; plus reason). 400, 403, 404 account_not_found, 503 as on create. Nothing changes for existing operations.

  3. Additive

    trigger_update pushes (stream and webhooks, opt-in)

    Nothing reaches a client that did not ask for it.

    • /v1/stream: new client message subscribe ({"type":"subscribe","topics":["trigger_update"]}), answered with a new server message subscribed (topics granted; trigger_update only for users /v2/triggers is enabled for). After it, new server message trigger_update (data: TriggerV2Update). Neither subscribed nor trigger_update is ever sent to a connection that did not send subscribe.
    • Webhooks: new event trigger_update (WebhookTriggerUpdate), sent only to receivers with the new optional field trigger_updates: true (set with PATCH /v1/postbacks/{id} (WebhookPatch); a new webhook starts without it; shown on WebhookView only while on). trigger_update is not accepted in events and never appears there. Delivered only when the server runs with OMS_TRIGGERS_V2_WEBHOOKS=on.
    • TriggerV2Update: event_id (de-duplicate on it), trigger_id, kind, account, strategy, from, to, transition_seq, actions, tick {symbol, ltp, event_ts}, reason, result (a refused command: {op, command_id, outcome, reason}), protection (the trigger's map), at. One per change; never another user's trigger.
    • Stored / NATS: new record kind trg2_update on oms.trg2.{user}.{trigger_id} (kept by OMS_EVENTS, which now also captures oms.trg2.>); webhook records gain trigger_updates (default false); delivery jobs may carry event_type: "trigger_update" (readers first: see OMS_TRIGGERS_V2_WEBHOOKS).
  4. Additive

    v2 protection map shows the current stop-loss level

    GET /v2/triggers and GET /v2/triggers/{trigger_id}: a server-held protection.stop_loss.level is now the stop the engine holds now (after trailing or tightening), not the level planned at create. Same field, same type; example get_trigger_v2 (success) re-recorded: 2475 -> 2480 after the engine moved the stop. Agreed by the user on 2026-10-12.

  5. Additive

    pause and resume v2 triggers

    New operations POST /v2/triggers/{trigger_id}/pause (pause_trigger_v2) and POST /v2/triggers/{trigger_id}/resume (resume_trigger_v2): hand the engine's pause / resume command for the caller's live trigger, 202 with the same TriggerV2Accepted body as cancel; 404 trigger_not_found, 409 trigger_fired, 403, 429, 503 as on cancel. Only for users /v2/triggers is enabled for (OMS_TRIGGERS_V2_USERS); API keys need the triggers scope. Nothing changes for existing operations.

  6. Additive

    trigger engine v2 shadow mode records (off by default)

    Shadow mode (spec §12 step 2, OMS_TRIGGERS_V2_SHADOW=on) adds its own names, none shared with the live engine and none read by anything that places orders: subjects {ENV}.trg.shadow.cmd|evt|res.{bucket}.{user}.{trigger_id} and {ENV}.trg.shadow.ctl.released, streams {ENV}_TRG_SHADOW_CMD / {ENV}_TRG_SHADOW_EVT, KV {ENV}_TRG_SHADOW_STATE, Redis oms:triggers:{ENV}:shadow:members|serves, leases oms:lease:trg:{ENV}:shadow:{bucket} and oms:lease:trg:{ENV}:shadow:singleton, tick-gate keys {symbol}.shadow-{bucket} in the shared {ENV}_TRG_SYMBOLS. New record kinds: trg_shadow_event (the shadow book's transition: the trg_event fields with event_id named shadow_event_id and a mode: "shadow" marker, so it never reads as an event; fixtures in trg2_shadow_link (Redis hash oms:trg2:{ENV}:shadow:legacy, shadow trigger id → JSON {v, legacy_id, user, account, symbol, at}) and trg2_shadow_diff (Mongo trg2_shadow_diffs: per trigger per IST day {_id, kind: "trigger", date, legacy_id, shadow_id, user, symbol, v2[], python[], findings[], counts, updated_at} and per day {_id, kind: "summary", date, counts, known, triggers, skipped, updated_at}), fixtures commands reuse trg_command. Legacy CIRRUS:TRIGGERS:* records are only read. Record kinds introduced in this release; no REST, webhook or stream shape changes.

  7. Additive

    trigger engine v2 Redis keys carry the environment

    Every Redis key of trigger engine v2 now names its environment (OMS_TRIGGERS_V2_ENV, escaped with domain::subject), as its subjects already do, so two environments sharing one Redis never take each other's buckets or read each other's records: membership oms:triggers:{ENV}:members / oms:triggers:{ENV}:serves, leases oms:lease:trg:{ENV}:{bucket} / oms:epoch:trg:{ENV}:{bucket}, the read model oms:trg2:{ENV}:{id} / oms:trg2:{ENV}:user:{user} (stored kind trg2_record, hash fields unchanged) and the exit loop's claims oms:trg2:{ENV}:evt:{event_id} / oms:trg2:{ENV}:evt_lease:{event_id}. This replaces the unprefixed names in the entries below; no release ever wrote or read them (record kinds introduced in this release; the two trg2_record fixtures only had their description of the key updated). The per-position exit guards oms:trg2:position_lock:{position} / oms:trg2:position_exits:{position} stay environment-free on purpose: they guard one real broker position, whichever environment exits it. Protection: oms:trg2:{ENV}:protection:{id} (trg2_protection, fixture descriptions only), oms:trg2:{ENV}:protection:live, oms:trg2:{ENV}:protection_want:{id}, oms:trg2:{ENV}:protection_lock:{id}, oms:trg2:{ENV}:protection_watch; the guard oms:trg2:protection_guard:{position} stays environment-free (one real broker position). The engine's own keys (oms:engine:*, account leases) are unchanged. /v2/triggers reads now answer 503 (as writes already did) on a pod without OMS_TRIGGERS_V2_ENV. No REST, webhook or stream shape changes.

  8. Additive

    trigger engine v2 fixtures finalized before first write

    Record kinds introduced in this release; fixtures finalized before first write. The trigger engine v2 kinds (trg_command, trg_snapshot, trg_event, trg_result, tick_snapshot, trg_bucket_mark, trg2_record) are hand-written: no build on main writes or reads them (checked 2026-10-12 against origin/main 2296793: no trigger-engine / trigger-runtime crate, no trg.* subject, no TRG_* stream or bucket, no oms:trg2:* key). So the three trg_command fixtures were edited in place (milestone 2 added the required scope) rather than appended to, which CLAUDE.md otherwise forbids because old records exist in production; these never did. Against main every one of these fixture files is an addition and no existing fixture changed. From the first production write on, they are append-only like every other kind.

  9. Additive

    trigger engine v2 command results

    New stored / NATS record kind trg_result: the engine's answer to each command, {v, cmd_id, trigger_id, scope, op, cmd_seq, outcome (applied | duplicate | rejected), reason?, state?, transition_seq, at, epoch}, published on {ENV}.trg.res.{bucket}.{user}.{trigger_id} into {ENV}_TRG_EVT (which now also captures {ENV}.trg.res.>) with Nats-Msg-Id = cmd_id, after the command's effect is durable. Fixtures and The projector applies them: the read-model record trg2_record gains optional result_seq and last_result (new fixture handwritten-2026-10-12-rejected-by-engine.json), and a create the engine refused now reads state: "rejected" with its reason in /v2/triggers (state is documented as an open set; /v2/triggers has not shipped). Record kind introduced in this release; no reader or writer existed before.

  10. Additive

    trigger engine v2 subjects carry the environment

    Every trigger engine v2 subject now starts with {ENV}., as the tick feed's {ENV}.ticks.> does, so two environments on one NATS account never capture each other's messages: commands {ENV}.trg.cmd.{bucket}.{user}.{trigger_id} (stream {ENV}_TRG_CMD), events {ENV}.trg.evt.{bucket}.{user}.{trigger_id} (stream {ENV}_TRG_EVT), the release notice {ENV}.trg.ctl.released and the (unbuilt) risk shape {ENV}.risk.agg.{strategy}.{user}. This replaces the unprefixed names in the 2026-10-09 entries below; no release ever wrote or read those subjects (record kinds introduced in this release), and the payloads are unchanged. The runtime refuses to start on an existing {ENV}_TRG_CMD / {ENV}_TRG_EVT that does not capture the new subjects. The api and the runtime read the environment from one setting, OMS_TRIGGERS_V2_ENV (alias TRG_ENV).

  11. Additive

    /v2/triggers protection map

    get_trigger_v2 (GET /v2/triggers/{trigger_id}) and list_triggers_v2 (GET /v2/triggers) answer a new field protection on every trigger: what protects the position, part by part, keyed stop_loss, stop_condition, target, target_1 …, target_condition, trail (schema TriggerV2ProtectionPart: held_by broker | server, broker_order_id, level, adjusted_by_server, status protected | degraded | unknown | held_by_server | ended, note). It is null for alerts and risk limits and for triggers created before this release. modify_trigger_v2 refuses (422 invalid_request, existing code) a change of target or stop_loss.when while a broker order holds part of the trigger's protection. Broker orders are placed for v2 triggers only with OMS_TRIGGERS_V2_BROKER_PROTECTION on (off by default: every part shows held_by_server). API version 1.3.0. New stored record kind trg2_protection (oms:trg2:protection:{id}, fixtures in read by this release only. Every /v1 operation is unchanged.

  12. Additive

    /v2/triggers

    New operations create_trigger_v2 (POST /v2/triggers), list_triggers_v2 (GET /v2/triggers), get_trigger_v2 (GET /v2/triggers/{trigger_id}), modify_trigger_v2 (PATCH) and cancel_trigger_v2 (DELETE), tag triggers, scopes read / triggers as /v1/triggers, with recorded examples and schemas TriggerV2*, CreateTriggerV2Request, ModifyTriggerV2Request. Writes answer 202 (status: accepted); errors use existing codes only. Only users in OMS_TRIGGERS_V2_USERS may call them (403 otherwise; unset = nobody). A strategy-level risk_limit is refused (422 spec.level) until positions are tracked per strategy. API version 1.2.0. /v1/triggers and every existing operation are unchanged.

  13. Additive

    trigger engine v2 runtime records (no role runs it yet)

    The trigger runtime (crate trigger-runtime, milestone 2) writes, once a role runs it (M3): the bucket mark {v, cmd_seq, epoch} at key bucket/{bucket} in KV {ENV}_TRG_STATE (fixtures in keys {symbol}.{bucket} in KV {ENV}_TRG_SYMBOLS (value: the bucket number), Redis oms:triggers:members / oms:triggers:serves, leases oms:lease:trg:{bucket} / oms:epoch:trg:{bucket}, and an internal core-NATS notice trg.ctl.released (payload: the bucket number; not persisted). It reads oms.portfolio.> and oms:portfolio:*:positions as they are today. No REST, webhook or stream shape changes.

  14. Additive

    trigger engine v2 read model and leg product

    New stored record kind trg2_record: the Redis hash oms:trg2:{id} (and index set oms:trg2:user:{user}) that /v2/triggers reads, written by the api (submitted) and by the projector from engine events (newer transition_seq only). Fixtures and reader test in leg (trg.cmd create, KV snapshot) gains an optional product (MIS, CARRYFORWARD, DELIVERY, MTF), absent on older records; a new command fixture shows it with the command's scope. No existing REST, webhook, stream or stored shape changes.

  15. Additive

    trigger engine v2 record shapes (nothing writes them yet)

    New stored / NATS record kinds for the v2 trigger engine (spec docs/superpowers/specs/2026-10-09-trigger-engine-v2-design.md §5.1, §6): the command on trg.cmd.{bucket}.{user}.{trigger_id}, the snapshot in KV {ENV}_TRG_STATE, the event on trg.evt.{bucket}.{user}.{trigger_id} and the merged tick on {ENV}.ticks.{symbol}, with hand-written fixtures and writes or reads them yet (milestone 1 is the pure core); no existing REST, webhook, stream or stored shape changes. Before any writer exists, the command gained a required scope ({user, account, strategy?}): a command without it does not read and is never applied, and the engine refuses a command whose scope is not its target trigger's. The never-written command fixtures were updated to carry it.

  16. Additive

    broker safety (protection profile, read-back in observe mode, off by default: `OMS_PROTECTION_READBACK`)

    No REST, webhook or stream payload changes. One new refusal, with the existing error: PATCH /v1/orders/{order_id} changing the quantity of a partly filled bracket / cover exit leg is answered 422 invalid_request (field: quantity) before any broker call, unless the broker's meaning of a modified quantity is verified (none is yet); cancel the pending part and place a new order for the rest instead.

    • oms.anomalies.> may carry a new kind, protection_mismatch (a protective order read back after a place or modify is not what was asked). AnomalyEvent.kind is free text, so every reader already reads it; the watchdog now pages on it. Readers first: no pod writes it yet (OMS_PROTECTION_MISMATCH_ANOMALY, off; on in a later release).
    • Trigger hash (CIRRUS:TRIGGERS:{id}), broker-held triggers only: capabilities gains protection (facts: every broker protection fact, its value or "unverified"; trail_plan: leg / gtt). Every existing key keeps its value.
    • New Mongo collection broker_observations (what a modify's quantity meant at a broker): broker, operation, class (total, remaining, ignored, unexplained, frozen in wire_enums.json), quantities before / after, a keyed hash of the account (account_key, from OMS_OBSERVATION_KEY), no account or order id.
  17. Additive

    unanswered cancels and modifies are checked against the book

    A cancel or modify (DELETE /v1/orders/{order_id}, PATCH /v1/orders/{order_id}, DELETE /v1/orders/bracket/{order_id}) the broker never answers still answers status: "unknown" as before. The engine is now told about it too (oms.ingest amended message, new optional unconfirmed: true, left out when false so older pods read the same bytes), reads the order book shortly after even on streamed accounts, and raises the critical anomaly amend_unconfirmed (staff alert, admin anomaly feed) when the order is still open and unchanged. Nothing is re-sent to the broker. No REST field, type, enum value, status code, webhook or stream message changes.

  18. Additive

    paper accounts report native bracket, cover and GTT support

    GET /v1/accounts now reports protection bracket, cover, gtt and gtt_two_leg as true for Paper accounts, and protected Paper orders use them like any broker that has them: intraday stop-loss + target goes out as a bracket order, stop-loss only as a cover order, positional protection as a GTT (two-leg when both levels are set). GET /v1/gtt lists Paper GTTs. No field, enum value or status code changes.

  19. Additive

    bracket / cover exit legs named by the broker's leg identity

    An order's leg_role (stop_loss / target / square_off) now comes from the leg identity the broker reports (Pocketful leg_order_indicator, Tradejini legType, Kite cover variety), never from the order type, which brokers change once a stop triggers or exits. A Tradejini stop leg listed as limit after it exited now reads stop_loss (it read target), as does each slice of a leg split by a partial fill. An exit leg whose role the broker does not identify carries no leg_role (the field was already optional and is omitted, as for every non-leg order). No field, type, enum value or status code changes.

  20. Additive

    more broker refusals classed

    Classed from production: Dhan "Invalid SecurityId (DH-906)" is invalid_field; Fyers "exchange not enabled for account", Motilal "scrip blocked by risk department" and Zerodha "MIS orders are currently blocked for <stock>" are rms_block; a Zerodha GTT trigger too close to the last price is invalid_price_or_qty. Same refusal texts to users; no field, enum value or status code changes.