CodeNomad/dev-docs/MISSIONS_RECURRING_SIMPLE.md
Pascal André 81fb513558
feat(missions): native project coordination, briefings and tracking (#866)
# Missions: product specification

## Product goal
Missions coordinates complex work through native OpenCode conversations.
CodeNomad provides creation, controls, human attention and result
reading; OpenCode owns execution. One-time objectives and finite daily
recurring passages share the same planning and reading experience. No
OpenCode modification is required.

## Create and configure
- **Flexible mission**: free-form objectives and bounded delegation.
- **Debugging**: diagnosis, implementation, independent
standards/specification reviews and fresh validation.
- **Exploration**: bounded decisions and parallel investigation, with
explicit native human questions where required.

Creation presents an objective, a short optional title, verbatim working
notes, coordinator/task profiles and native or independent task-session
policy. Optional choices stay collapsed. A single **Create** button
saves a prepared one-time mission or paused recurring schedule without
starting anything. **Play** is a separate explicit action for both.

Settings → Missions contains defaults for **all new missions**, with
optional exceptions for each of the three types, including Flexible. A
type exception overrides the global value; an unset field shows the
inherited value. Defaults never reconfigure existing missions or busy
actors. Reusable briefs contain creation inputs only; loading copies
them into a normal draft and deletion preserves executions. **Save as
brief** is also available from a selected one-time mission, including a
completed mission: it copies objective, verbatim notes, template, stored
profile choices and task-session policy; the mission title prefills the
brief name, never copying execution identities or results.

Profiles choose an agent, model and optional reasoning variant.
Coordinators use primary/all profiles; native task actors use
subagent/all and independent actors use primary/all. Business roles
remain separate from native session identities. Actor continuation
preserves the accepted profile.

The native subagent-depth editor is separately Location-scoped and
applies to all OpenCode conversations there, not just missions. It shows
the configuration file, effective value and override. Conditional Save
preserves the original file expectation and never automatically reloads
or restarts OpenCode.

## Missions panel
The first toolbar holds only general actions: **Create**, **Settings**
and **Refresh**, aligned right. Settings opens the Missions settings
section directly, using the existing native Preferences window on
desktop. A native refusal shows an error rather than opening a second
embedded window; browser and remote windows retain their embedded
settings.

The current-mission line contains a leading chevron, the selected title
and a small status mark:
- Clicking the **chevron** expands a persistent inline list; selection
leaves it open.
- Clicking the **title** opens a transient popup; selection, outside
interaction or Escape closes it.
- Both lists show titles and status marks, with full labels in tooltips
and accessible text. Titles truncate only at the available row edge.
- Search appears only when the measured list exceeds its maximum height,
and remains available while a query is present.
- The closed chevron indicates activity/attention in other missions
using the same visual vocabulary as the session list.
- Only the picker changes the selection, remembered per project; opening
a mission's conversation never reselects a mission.

Selected-mission controls are separate: **Play/Pause**, **Stop**,
optional recurring **Run now**, then **Summary**, **Open coordinator
conversation**, **Edit** and **Delete**. Destructive actions require
confirmation. **Request an update** is a text button below the icons,
available only while meaningful; its pending/uncertain state is retained
without automatic replay.

The **Tasks** disclosure contains only the declared dependency tree, not
a duplicate task list. It is initially open, remembers its state and
uses the remaining panel height down to the window bottom rather than a
fixed task-height cap. Rows retain full-width selected/hover backgrounds
across connectors and controls. A task's exact conversation shortcut
appears on desktop hover/keyboard focus, and remains visible on touch.

Mission selection is remembered per project and changes only through
explicit mission selection/creation. Opening a coordinator or task
conversation in the session list never changes the selected mission.
Conversation shortcuts remain available from the mission panel to the
coordinator and exact task conversations. The session-list files are
unchanged relative to `dev`; restoring the full cached session hierarchy
is supplied by the separately merged #841.

## Plan and native execution
The coordinator declares tasks, dependencies, roles and expected
outcomes. Ready work can run in parallel through ordinary native
subagents. Recursive decomposition follows OpenCode permissions and
configured depth; the product imposes no delegation target or root
fallback.

Independent sessions remain available for an explicit Location, lifetime
or existing-root requirement. The task-session policy is frozen per
mission. Tasks have stable keys/generations; conversations have exact
native identities. Business dependency edges never stand in for native
ancestry.

Native task returns and business settlement are separate: a coordinator
records the actual returned result/evidence, then finalizes the mission.
A notification/admission acknowledgement is not proof of consumption or
completion.

## Results and human interaction
Long objectives, briefings, task results, evidence and plan history open
in the central reader above the transcript. Reader eyes toggle the exact
target and highlight it. Task results come before the original brief and
technical details. A missing/unavailable reader target must not leave
the transcript hidden.

The coordinator publishes dated, bounded briefings explaining delivered
results, remaining work, obstacles and the next step in plain language.
Briefings remain separate from task settlement and observed activity;
exact request identity is required to acknowledge an explicitly
requested update.

Attention contains real native Forms and permissions. Obstacles reported
in prose are not automatically human requests. Native interruptions
retain conversation provenance and drafts in the shared interruption
dock. A template's required human decision counts only when answered
through the CodeNomad UI, locally or from a live paired Remote Control
device; a simple UI-answer mark records that provenance.

Users give direction or ask questions by writing in the coordinator
conversation (**Open coordinator conversation**). From the panel itself,
the only coordinator message is **Request an update**; its
pending/uncertain state is retained by identity and never replayed
automatically.

Reports, plan history and cleanup receipts stay secondary in
reader/technical surfaces. Optional managed-session cleanup preserves
coordinators, shared/reused/moved sessions and roots with descendants.

## Lifecycle and recovery
For one-time missions:
- **Play** starts/resumes explicit mission intent; **Pause** parks
mission dispatch/notifications and interrupts the observed active native
family.
- **Stop** is terminal and interrupts observed descendants
deepest-first, then registered roots/coordinator, with bounded rechecks
for parent wakes caused by child cancellation.
- Native inbox cancellation uses exact authorized mission/subagent work.
Pause preserves deferred results for explicit Play; Stop removes the
identified queued work/results that would wake a stopped coordinator.
- Per-target receipts record observed interruption and family coverage.
Incomplete or unconfirmed interruption is shown as **Some sub-agents may
still be running**, never as proven recursive suspension.
- **Refresh** reconciles state first; an explicit refresh click may
retry a still-unconfirmed lifecycle action under its original identity.
Reconnect and ordinary display reads never resend mutations.
- **Play** offers targeted recovery only when native observation
establishes the applicable interrupted state, not merely because
activity is unknown. Active background descendants retain the
active/Pause presentation.

Native interruption does not undo completed filesystem/tool effects.
Resuming a paused mission lets the coordinator decide how to continue or
re-delegate interrupted work; there is no hidden task replay engine.

## Recurring missions
Recurring setup adds permanent instructions, a title, a daily local
time/timezone and explicitly followed project conversations. Saving
creates a paused schedule; Play arms it and Run now requests one finite
passage.

The OpenCode service/plugin owns scheduled passages while both CodeNomad
UI and its intermediary backend are closed. One native Job per running
schedule sleeps until the earlier of the next due time and one hour. On
waking it rereads the clock/document and admits only the latest due
civil day newer than the stored daily cursor; there is no backlog.
Native execution terminal events also wake observation after a debounce,
but are not settlement evidence.

Before native effects, conditional storage records deterministic
passage, coordinator and start-message identities. Lost replies
reconcile those exact identities rather than creating new attempts. A
passage archives only after the coordinator and descendants are
inactive, inboxes are empty and no relevant native Form, permission,
Shell or background subagent remains. Unknown coverage stays pending.

Passages end as completed with a final report, native terminal failed,
or ended-without-report/interrupted. Only completed passages advance
followed-conversation cursors. Schedule history is bounded to 30
passages. Civil-time DST gaps/repeated times and entirely skipped days
resolve deterministically.

Pause/Stop cancel the schedule Job; those schedule actions do not prove
an already-running passage family stopped. Stopped is terminal. After
service restart, a desired-running schedule without its Job is
**Interrupted**, awaiting explicit Resume. Resume reconciles pending
work, never sends a continuation; a turn cut by restart settles
interrupted/ended-without-report.

## Durability and UX
Mission journals and schedule documents retain tasks, roles/profiles,
results, plan revisions, controls, notifications and bounded history.
Conditional writes and original request identities protect mutation
retry. Authority, Location ownership, fresh per-send environment and
connection/deletion fences are checked at admission. Ordinary native
permissions govern tool use.

UI remains compact and square, localized in all supported languages,
keyboard-accessible and usable on touch/narrow/RTL layouts. Cache-first
display reads revalidate on visible demand and native invalidation,
without polling or clearing known data on uncertain recovery. Loading,
missing capability and uncertain effects remain explicit.

## Shared integration scope
The PR also includes supporting shared-runtime corrections; these are
not limited to the Missions tab:
- Session outcome and generation-recovery reconciliation retains
uncertain interruption state across reconnects and checks native
outcomes before clearing it. Native message pages clamp to the service's
200-message limit.
- Pending-request recovery retains known requests through incomplete
coverage and connection transitions; native ownership and deletion
fences remain authoritative.
- Keyboard offset and the compact composer help strip preserve reachable
input controls without changing composer height limits. Touch overflow
actions suppress duplicate compatibility clicks.
- Workspace text saves enter the existing worktree mutation fence;
sandboxed panel asset reads fence connection changes. Worktree scans
retain validated family identity and custody.
- The worker-based Git process runner uses an ESM-safe worker entry, and
CLI service discovery shares bounded command deadlines.
- Desktop/backend profile identity is explicit for Missions' owned
storage. Profile-scoping signals are internal to that relationship, not
permission for independently launched applications to inherit a profile.
- Shared rendering/localization includes a Turkish tool-call label
correction.
- Session-list startup restoration exposes a settled signal
(`stores/session-list-restoration.ts`, hooked in `session-api.ts` and
`instances.ts`). Missions display reads wait for it so the session list
loads first; it settles on success, failure, timeout or abandonment and
is reset when a workspace is removed. Session-list components and styles
are unchanged.

These callers are included in integration review and their regression
tests, rather than treated as an unchanged dependency surface.

## Current limitations
- Editing/deleting recurring schedules is not implemented; those icons
in the selected mission's action bar are disabled for schedules.
- An uncertain one-time creation (lost reply after the request was sent)
is held in window memory with no status endpoint; Create stays blocked
for that window until reload. Definitive refusals before any native
effect re-enable Create immediately.
- Saving a brief from an existing recurring schedule is not implemented;
the selected-mission action currently covers one-time missions only.
- Task conversation lookup for older unbound native tasks reads a
bounded coordinator-message window and recognized exact assignment
evidence. Older/unrecognized delegations may have no shortcut; no
conversation is guessed from titles or timings.
- Running native calls may not expose their child ID until completion.
The panel can instead identify a direct child by its first canonical
assignment message, within a bounded direct-child page; unrecognized or
out-of-bound children remain unlinked.
- Native task profiles are recorded, but ordinary subagent launch still
relies on the coordinator passing the declared agent/model/variant to
OpenCode. Omitted model parameters can inherit the coordinator's model;
profile storage alone does not enforce launch parameters.
- One-time recursive interruption is best-effort and bounded, not a
durable guarantee that every descendant stays stopped. Partial/unknown
coverage remains visible.
- Recurring Pause/Stop cancel scheduling, not guaranteed recursive
suspension of a running passage.
- Native qualification uses isolated services; mock-only and
source-review evidence never substitute for real native acceptance.
Final validation results are published separately in the PR reviews.
- Managed-service restart and some
lost-reply/clock-conflict/human-decision journeys still have
offline-only evidence.
- If an admitted passage start is removed by pruning before its lost
durable write is reconciled, that original start identity can become
unavailable for deduplication.

## References
- [Mission
contract](https://github.com/NeuralNomadsAI/CodeNomad/blob/experiment/missions-native-subsessions-20261003/dev-docs/MISSIONS.md)
- [Recurring
contract](https://github.com/NeuralNomadsAI/CodeNomad/blob/experiment/missions-native-subsessions-20261003/dev-docs/MISSIONS_RECURRING_SIMPLE.md)
- [Native
qualification](https://github.com/NeuralNomadsAI/CodeNomad/blob/experiment/missions-native-subsessions-20261003/dev-docs/MISSIONS_RECURRING_SIMPLE_QUALIFICATION.md)
2026-10-11 14:55:49 +02:00

29 KiB
Raw Permalink Blame History

Recurring Missions — simple native contract

Authoritative user decision: 2026-10-08. This is the recurring-missions target, not a claim of implementation or acceptance. It supersedes historical recurring authority and acceptance requirements wherever they require signatures, grants, epochs, effect receipts/budgets or event-log replay. One-time Missions published in PR #866 retain their existing receipts and are not refactored here. The simple Wayfinder UI-answer mark below applies to both modes.

Product boundary

Scheduled work runs in the OpenCode service/plugin with the CodeNomad UI and intermediary backend closed. Neither a persistent CodeNomad backend nor a window timer satisfies this contract. Do not modify OpenCode to implement it. Creation stores a paused schedule; it never starts work or arms a timer. The UI's single Create button only creates; the schedule runs after its separate explicit Play, whose requests carry the then-current revision so CAS refuses a duplicate. Creation is never resent. New schedule titles follow the Mission title rule (1–60 characters); stored schedules keep their 120-character bound. Explicit user controls and native execution are separate responsibilities.

Schedule document and revision

Store one schedule document in project-scoped plugin kv, including:

  • id and a user-visible title.
  • Instructions and daily clock: { time, zone }.
  • Frozen coordinator/role profiles, taskMode and template.
  • Watched conversations and their processed-message cursors.
  • Desired state: paused | running | stopped; stopped is terminal.
  • lastDaily, identifying the last settled scheduled civil day.
  • pending, either null or the exact passage identity and its progress.
  • Result history bounded to 30 entries.
  • A monotonically checked custom revision.

Native kv provides no CAS. The plugin must serialize document updates and compare the supplied expected revision before committing the new revision. A read followed by an unguarded write is not CAS. Controls, due admission and settlement use the same guarded document mutation path; concurrent attempts cannot replace each other's pending identity or lose an archive. Keep this small: no authority epochs, signing keys or per-effect ledgers.

User controls

Create paused, Play, Pause, Stop, Resume, Run now and Check passage are admitted only through:

  1. The authenticated CodeNomad UI/backend route.
  2. The existing HMAC-authenticated bridge.
  3. The plugin's schedule-control RPC.

The route's human principal, with local auth enabled, is a live login cookie session or, from the Remote Control ingress (socket membership), a paired device that is still authorized; the bridge callback revalidates the same principal and never treats a device as a cookie. Any other principal is refused before a deletion permit or control hold exists, so a refusal is definitive, never uncertain.

Never expose these controls as model tools. Validate project/Location ownership, connection identity, inputs and frozen execution profiles at the existing boundary. Each control has requestID idempotency and expectedRevision CAS. A duplicate request must not start a second Job, passage or coordinator message. A stale revision is a conflict, not permission to overwrite current state. Native stays the authoritative CAS: the exact status read reports rejected (revision-conflict or control-conflict) only when storage proves a request with no record never applied and never can — a later revision whose record provably was never evicted (the document bounds evicted revisions), or a pure pre-write refusal at its own revision. The route then releases the permit and answers 409 recurrence-revision-conflict / recurrence-control-conflict. A control or Run now that fails before the route dispatches it (connection unavailable, Location ownership or deletion admission, another request's held permit) answers recurrence-not-dispatched (409 when another permit or a deletion blocks the hold or the permit table is full; otherwise 503). The code proves only that this attempt sent nothing: it means "no effect" solely for an identity the client never sent before. A refused repeat of an identity the backend still holds stays uncoded, but a backend that no longer holds it (for example after a restart, failing before any native read) cannot know an earlier send, so clients that repeat an identity must keep it uncertain and read its exact status. /control refuses run-now with 400 before any permit; Run now has its own endpoint. The UI releases these coded refusals and the route's pre-permit 400/401 for a request's first attempt only, refreshes the row and explains the refusal; a refused exact partial retry keeps the original held request and server permit. After a backend restart, an explicit partial Pause/Stop retry may take a fresh physical admission. Before dispatch, an authenticated exact native status read must prove the original action, request ID, expected revision, recorded revision and partial result in the current owned Location. Missing, foreign, completed or unknown records cannot authorize it. This never transfers an old permit, starts a Job or changes the request identity; native idempotency still decides effects. Failed preparation releases this fresh admission, but preserves a surviving original partial permit. Within one backend, a partial or uncertain Pause/Stop permit can outlive its retired OpenCode connection (event-stream end, failed request, new daemon). An explicit exact retry through the current connection may then prepare a fresh admission beside it, never transfer it: the same backend workspace object, native project ID and canonical path (which OpenCode derives from the Git root), directory, checkout, physical root and Settings profile must be re-proven, plus the same exact native partial record (random request ID, revision and state). These are real ownership gates, but there is no daemon-storage identity challenge: a byte-identical copy of the native database served by another daemon at the same project/path is indistinguishable, and a retry's effects would land in that copy while the backend retires its permit. The old permit is retired only at dispatch, after the fresh one is entered. An in-flight, still-preparing or already-replacing original, a live original connection, or any mismatch refuses without effects; a deletion already waiting on the old permit blocks the fresh entry. Status reads through the current connection may still settle that permit (committed and complete, with the owner re-proven) while such a deletion waits, and never resend. A retry or a known held identity's repeat stays uncoded on early failure: no dispatch in this attempt says nothing about earlier effects. A dispatched control or Run now that native refused before writing any record (transport-proof callback timeout, transient Git/family read, profile check, Resume not interrupted, service death) first reads unknown: a timeout alone proves nothing. Every record-creating CAS (beginControl, reserveManual) rechecks the signed proof's 30 s freshness synchronously inside its write transaction. Once the backend's clock considers the exact proof it still holds expired, its status reads (and the route's immediate read after a lost reply) send that backend-signed issuedAt — never a browser value. Native, serialized with those CASes, answers rejected (control-conflict) only when its own clock also finds it expired by a further 5 s grace (so a CAS racing the boundary in another process sharing the storage has committed) and the request has no record that could ever have been evicted; it then retires the request ID in that native host, so an in-flight original (or any later attempt of that ID) fails at the CAS even if the wall clock moves back, and keeps answering rejected for it. The route releases the permit holding exactly that issuedAt, Location and request (with the owner re-proven through a replaced connection), unblocking controls and deletion. An unexpired proof or a mismatch stays unknown, and an unheld identity (for example after a backend restart, with no hint) cannot trigger retirement: it stays unknown unless native has already retired it. Retirement is process-local and bounded (4096 IDs); another daemon sharing the same storage relies on the expired proof alone, and clients must not reuse a rejected request ID. Everything else (including an unprovable profile change) stays unknown.

If a reply is lost, retain that request identity and reread its status. Refresh, reconnect, remount and status reads never automatically resend it. An uncertain control must not be replaced with a fresh request to evade the hold. Explicit Resume is a new user action, not an automatic retry of an unknown send. Stopped schedules cannot be played, resumed or run now.

A schedule document keeps at most 64 control records. Admitting a new control evicts the oldest completed record; unresolved records are never evicted, and an evicted request's exact status reads unknown, which never authorizes a resend. evictedControlRevision bounds every evicted record's revision (a first bound is the current revision, covering evictions by older stores). A Play/Resume/Check whose Job or observer start stayed unknown is resolved by a later completed control that restarted or cancelled it. While such an intent, or a reserved but unadmitted Run now, stays unknown, the snapshot offers only Resume/Pause/Stop/Check and the UI enables exactly those that native offers once its snapshot records the intent; the original is never resent. The backend admits that new request with its own permit only after a fresh exact native read through the current connection shows the original recorded and incomplete under the same owner; a committed superseding control retires the original permit, a no-effect refusal restores it. If another client's exact read proves a retained middle intent had no effect while a newer control still supersedes it, only that intent's permit is released and the newer control inherits its predecessor, so a later no-effect restores (and a commit retires) the earlier intent instead of orphaning its permit. Run now itself still completes only on admission or archive. In-flight, preparing and partial Pause/Stop permits are never superseded. A Play/Resume whose native Job start fails while a fresh read positively shows no running daily Job completes its record with the schedule Interrupted (reason error), releasing the backend permit for an explicit Resume/Pause/Stop; a failed or running read stays unknown. Interrupted(error) and the completed record are one CAS revision, so no crash can separate them; a crash before that write is an ordinary unknown start. A status read also resolves an incomplete current start intent already marked Interrupted(error) (for example by the daily Job's own fatal exit) when a fresh definite read shows no running daily Job. A Job writes that reason inside its own run, before native reports it not running, and Resume admits only after such a read, so a stale reason cannot land after Resume cleared it. Nothing retries the start. A Run now is resolved by its own passage's admission or archive; archiving also resolves Checks of that passage. Pause/Stop on a reserved passage without a journal mission has no actor to interrupt (dispatch of an unadmitted passage is refused once paused/stopped).

Honest threat model and rationale

HMAC authenticates the ordinary desktop bridge; it is not an OS sandbox. An agent with unrestricted shell can read the service password and kv database. No in-process mechanism, including signatures, prevents that agent from acting with the service's own privileges. Native shell permissions and OS isolation are the real security boundary. Do not describe simple marks as tamper-proof proof.

Ed25519 parent/child grants, signed standing authorization, epochs, effect budgets, per-effect reservations/receipts and signed human-answer receipt state machines are removed from recurring passages. They complicated ordinary useful work and restart reconciliation without providing the claimed unrestricted-shell boundary. Native execution identities, guarded schedule updates and conservative observation cover the narrower product need. Unknown effects still remain unknown. Existing ownership, authentication, permission and data-loss protections remain.

Native guarantees and missing primitives

The implementation relies on existing native contracts, qualified at their callers:

  • Session creation with a caller-supplied session ID is idempotent.
  • Prompt admission with a caller-supplied message ID is first-admission-wins.
  • Plugin tools receive native session, message and call identity.
  • Jobs are process-local and disappear when the service restarts.
  • A turn holds a durable execution claim (session_v2.time_suspended) until its terminal event. A hard kill leaves the claim with no terminal event. Observed with OpenCode 2.0.26 and an owned unmanaged serve (evidence f60TKB): after restart the coordinator stayed inactive with no resumed provider turn. Upstream sources sweep orphaned claims only in the managed service at boot; that path is not qualified here. Do not rely on native continuation.

First admission is not proof of model consumption, useful work or completion. Resume never sends a continuation or replacement coordinator prompt. A claim written before the current service process started and not live now is a turn cut by a restart: once the rest of the family is quiescent, the passage settles ended-without-report with reason interrupted ("Interrupted by a restart"). If native work later resumes such a claim anyway, its effects remain unknown. These guarantees do not make arbitrary shell/tool side effects exactly-once.

Missing primitives are handled honestly:

  • No native kv CAS: implement the small revision guard above.
  • No native family-settled operation: observe the whole passage family and queues.
  • No native Form answerer identity: use the simple backend UI-answer mark below.

Qualify actual capabilities/contracts, not an untested-version allowlist.

Scheduler and civil time

Keep one native Job per running schedule in the live OpenCode process. It sleeps until min(nextDueAt, now + 1h), then rereads the wall clock and document. This hourly ceiling bounds clock-change detection; it is not per-minute polling. A native session.execution.succeeded|failed|interrupted publication (Bus listen) also wakes the Job after a 3 s debounce, so settlement follows family quiescence in seconds; it is only a wake hint. Without that native contract the Job falls back to 30 s → 2 min → 5 min (capped) only while a passage is pending. On waking, verify desired running, no conflicting pending passage and a due civil day newer than lastDaily before attempting the guarded passage start. If several days were missed, admit the latest missed day only, never a backlog.

Use the saved time zone and daily civil clock rather than 24-hour elapsed timers. Preserve the clock contract for DST gaps, advancing day-end gaps; a wholly skipped civil day has no passage. Repeated clock hours must not create two daily passages. A wall-clock rollback must not make an already processed day new again. nextDueAt is a display projection, not execution authority.

Pause and Stop cancel the schedule Job and interrupt the pending passage's registered root coordinator, without proving that coordinator or its descendants stopped; Stop is terminal. Neither control alone proves the coordinator or descendants have stopped. Retain pending passage identity and observations rather than clearing live work.

Desired running without a live native Job is displayed as Interrupted with an interruption reason. Service restart therefore leaves scheduling Interrupted. Do not rearm from plugin load, a read, UI reopen or backend reconnect. Only explicit Resume may restore scheduling; pending work is reconciled first. Scheduler-interrupted is not passage-stopped; a passage cut by the restart settles as interrupted after Resume rather than waiting for a native continuation.

Write-ahead passage start and recovery

Daily and manual passages use the same start/observation/settlement path. Before any native effect, CAS-write a pending record containing:

  • Deterministic passageID.
  • coordinatorSessionID.
  • Caller messageID for the coordinator's original instructions.

The daily identity is tied to its schedule/civil day; a manual identity is tied to the accepted Run now request. Never use recovery to invent a new identity. Create/admit only the original native identities after the pending commit. If a crash occurs, inspect the exact native session, message and inbox. Never resend under a new identity or infer missing work from a lost RPC reply.

Resume is allowed when a passage is pending, but it is reconcile-only for that passage: no second coordinator message. Unknown native coverage leaves pending uncertain and blocks a further passage. No expiry converts unknown into safe replay.

An unadmitted pending passage is reconciled by reading natively whether its deterministic start message exists (delivered or inbox):

  • Present: record the admission and continue observing.
  • Absent, and the start input can never be built (watched conversation deleted or moved, input capacity) or the schedule no longer allows dispatch: archive failed with reason not-started. This is the only archive without an admission; nothing was sent, so nothing can replay. The coordinator session is kept, lastDaily advances normally and watched cursors do not.
  • Absent for a transient cause: retry admission on a later wake with the same session and message identities (native first admission wins), never a new one. Retry applies only while no admission was ever recorded. Once recorded, a later native absence of the start message (pruned history) is observed history: the passage settles by family quiescence and the message is never sent again. Run now cannot overlap an unresolved pending passage or bypass terminal Stop. Manual runs do not masquerade as a different scheduled civil day's completion.

Run now without a live schedule Job (paused, or Interrupted after a restart) starts a separate settlement-only observer Job using the same sleep/wake seam, including the native execution-event wake and the pending fallback backoff. It is reconcile-only (never a second coordinator message; at most the original start identity while dispatch is allowed), never starts daily passages and exits once the passage settles. Pause cancels it. Stop keeps (restarts) it for a pending passage so the passage still archives honestly; the schedule stays terminal. A live running schedule's own Job observes its manual passage instead. The observer starts only on a definite native read that no daily Job runs, or on an unknown read of a schedule not desired running; an unknown read of a desired-running schedule starts nothing (if its Job is in fact gone it shows Interrupted and Resume reconciles the passage), so no second observer runs beside the daily Job. After a service restart with a pending passage on a paused or stopped schedule, the snapshot offers an explicit Check passage (check) control, admitted like the others (requestID, expectedRevision, authenticated route), which only restarts that observer and keeps the schedule paused or stopped.

A failed wake does not end the Job. It records a display-only lastError { code, at } in a sibling kv key (outside the document revision) and retries with a capped backoff (30 s, 2 min, 5 min, 15 min, 1 h). The next successful wake clears it. Only definitively fatal causes (Location or Job binding gone) end the Job as Interrupted with reason error; a missing or stopped schedule simply exits.

Passage tools and journals

Register mission_* tools independently of CodeNomad backend presence. Use native session identity to route a passage and its actors to its isolated journal; otherwise route to the one-time journal. Do not accept arbitrary journal selectors from the model. Native message/call identity remains available for ordinary tool admission and journal ownership, not a new effect-receipt layer.

Native permissions govern every other tool. Read, shell, ordinary subagents and business tools are allowed under those permissions; there is no Mission whitelist and no per-tool receipt requirement. Preserve frozen profiles and taskMode. Both native descendants and passage-owned independent actors must be accounted for without inventing ancestry from task dependencies.

Settlement, result and watched cursors

Settlement requires the coordinator and all descendants to be inactive, their inboxes empty, no applicable pending Form/permission, and no running Shell or background subagent. Passage-owned independent actors also cannot remain live. Activity alone is insufficient; a final report alone is insufficient. Use bounded native observations, recheck identities and retain pending on unknown, incomplete or over-bound coverage. No exact event-log replay proof is required. The family walk shares the 4096-session whole-family control ceiling (MAX_CONTROL_FAMILY_SESSIONS) with one-time lifecycle control, so ordinary recursive delegation settles; a larger family stays pending as unknown coverage (no archive, no cursor advance); it is never assumed complete.

Supported outcomes are:

  • completed: the settled passage has a final mission_report(outcome: "completed"). It stands even if a native failure occurs later during wrap-up.
  • failed: either a final business mission_report(outcome: "failed"), or a native terminal failure in the family without a completed final report. Never guessed from prose or an idle flag.
  • ended-without-report: settled native work ended without a final report and without native terminal failure; reason interrupted when a family claim was cut by a service restart.

Atomically archive the result, clear pending and update the scheduled lastDaily in the guarded schedule document. Keep history at ≤30; do not let a crash between separate document writes permit a second passage for the same settled day. Advance watched processed-message cursors only on completed. Failed and ended-without-report leave those cursors unchanged. Keep watch cursor continuity when a watch is removed; removal does not prove unprocessed source messages were consumed. No signed source-read receipts.

Wayfinder human decision mark — both execution modes

When the user answers a native Form through CodeNomad's InterruptionDock, the backend records { formID, sessionID, answeredAt, via: "ui" } in two phases: a pending mark with a fresh attemptID before forwarding, promoted to confirmed only after the native reply returns positively for that attempt. A definitely failed forward (Form still observed pending) removes the mark; a lost/uncertain reply leaves it pending forever. Pending never qualifies, so a later non-UI answer cannot borrow an earlier failed UI attempt; Wayfinder may ask again. The Wayfinder human gate accepts only a native Form with the matching confirmed UI mark. An ordinary native answer without that mark is not a proven UI human decision. A refusal reaches the model as "Human decision required: the user must answer this question from the CodeNomad interface", with the gate's cause as a detail. The shipped bundle publishes no native task binding, so the gate also proves the decision session natively: a fresh child of the expected parent's exact subagent call whose prompt carries the declared assignment. Native 2.0.26 serve writes no event rows; answers and execution failures are read from durable message projections (completed question part, idle outcome). Do not infer identity from answer text, tool completion or the agent's assertion.

Only Forms whose session belongs to a Mission family take the mark path: a root with codenomad.mission metadata, or a metadata-less root (an attached existing coordinator) that the plugin finds as the coordinator actor of a Mission in the project's durable one-time journal, after walking the bounded native parent chain to exactly that root. Every other Form uses the ordinary native reply unchanged and gets no mark. Marks live under codenomad-missions/human-marks-v1, never the retired authority-v2 namespace.

The UI principal is explicit in the private bridge proof: a local login cookie session (auth enabled), or a paired Remote Control device chosen by ingress socket membership. A device qualifies only while Remote Control is connected and the device is still paired, unrevoked and unexpired; the route and the bridge callback revalidate that principal across native I/O. A device ID never poses as a cookie session. When the mark cannot be prepared before forwarding (no authenticated principal, plugin/binding unavailable, revocation or a session move), the answer falls back to the ordinary reply without a mark; the user is never blocked from answering. The human-answer header alone (automation, Yolo, SDK) never mints a mark. Only a dispatched mark reply with a lost outcome is reported as uncertain.

There are no signatures or reserved/replied/settled answer receipts. OpenCode does not record answerer identity natively. Keep the ordinary answer path's ownership and uncertainty checks; missing evidence leaves the gate unmet, never causes automatic answer replay. This replaces signed answer provenance in both one-time and recurring Wayfinder, not unrelated one-time lifecycle receipts.

Per-schedule snapshot

Expose id, title, clock, display nextDueAt, revision and available actions. Expose state including Interrupted and interruptionReason when applicable. Expose pending status starting | running | settling | uncertain, exact identity, latestResult and history. starting means a live Job/observer is handling a not yet admitted start; uncertain (with reason not-observed or admission-failing) only when no observer is live or that admission keeps failing. History and pending items carry their daily | manual trigger; a non-blocking lastError is exposed while relevant. Do not expose epochs/grants/receipts/budgets. Actions reflect terminality, unresolved controls and pending passage state. Read-only projections and invalidations never start work or settle unknown sends.

Acceptance journeys

  1. Daily useful work: create paused → explicit Play → close UI/backend → due passage uses ordinary read/shell and Mission tools → final report → whole family settled → atomic archive → next day's passage. Verify no overlapping passage, one Job and one coordinator message, frozen profiles and completed-only cursors.
  2. Restart with pending: restart the service during a pending passage → reopen shows Interrupted → explicit Resume reconciles existing session/message/inbox → no second coordinator message → settled archive (ended-without-report / interrupted when the restart cut the turn) → next day. Missing/uncertain evidence stays pending, not silently rearmed.
  3. Run now: explicit authenticated Run now uses the shared write-ahead path; duplicate request/status reads produce no second passage. An unresolved pending passage blocks overlap. A lost reply is reread, never automatically resent.
  4. Pause/Stop: cancel the Job, verify no later due admission; pending work is still observed honestly. Pause can be explicitly resumed; Stop cannot. Exercise CAS conflicts and cancellation uncertainty without claiming recursive suspension.
  5. Human gate: native UI Form answer has the matching mark in both modes; an agent/native answer without it does not satisfy Wayfinder's human gate.

These are integrated journeys, not acceptance inferred from an isolated helper.

Coverage boundary: the offline recurring-day.e2e.test.ts fixture drives the real plugin, scheduler, storage and mission_* tools, but its scripted model only records read/shell call names; it executes no real shell or backend tool. Real shell/backend execution under native permissions is covered only by the isolated native qualification fixture (MISSIONS_RECURRING_SIMPLE_QUALIFICATION.md, journeys A–G, W, Q and N; acceptance remains partial), not by the offline e2e. Native tests must use isolated service/database/provider fixtures, never the shared daemon or user Mission. No install, daemon mutation or push is authorized by this doc.

Out of scope

  • Refactoring PR #866's one-time execution, lifecycle or cleanup receipts.
  • Reintroducing signed standing/child authority or per-tool effect accounting.
  • Exact event-log replay settlement or exactly-once arbitrary external side effects.
  • Unattended cold wake, automatic scheduler restoration or missed-day backlog replay.
  • A persistent CodeNomad backend, browser timer or upstream OpenCode modifications.
  • OS isolation implementation or security against unrestricted same-user shell.
  • Legacy namespace migration, automatic unknown-effect replay or disk reclamation.

Historical branch work remains preserved; superseded evidence is not deleted and does not qualify this simplified contract. See MISSIONS_REFACTOR_VALIDATION.md.