[v0.1/P1] Track retarget completion with operation IDs #24
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#24
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent: #12 (P1.5). Supersedes the completion question in #6.
Why
Switch and retarget commands report
acceptedwhen a push is only queued. Dial, authentication, attach, and supersession happen later, so the field promises a stronger completion boundary than exists.Scope
acceptedresults toqueued.--waitreportinglanded,failed, orsupersededper target attachment.Acceptance criteria
queued.Triage plan (2026-09-03)
Source-grounded triage against
mainat69076d42, reviewed through seven rounds of an independent reviewer (pisol/luna) until it passed with no findings. The dependency order that supersedes the tracker's is posted on #12. Where a "Review amendments" section below conflicts with an earlier section, the amendment is the decision.Claim check
Accurate, and the current state is a recorded decision, not an oversight.
crates/felis-protocol/proto/felis.proto:1310-1329OpsSwitched.accepted: "Acceptance, not landing ... Operation ids that collect landed/failed/superseded are deliberately deferred; the envelope leaves room for them as an additive minor."crates/felis-cli/src/cli_sessions.rs:1659-1666and:1722-1727("Accepted, not landed: each window now runs its own attach"); thenot_acceptederror kind atcrates/felis-cli/src/cli_output.rs:143,:186, listed in the closed set atdocs/reference/cli.md:109.docs/reference/ipc.md:834-838anddocs/explanation/architecture/ipc.md"Acceptance, not landing": deferral with a Revisit if: "a script that retargets a window and then acts on the window's new home".skills/felis/SKILL.md:343-354tells an agent not to readacceptedas "the move was accepted" and to "confirm withsessions info", i.e. the polling workaround for exactly this gap, written for the product's own agent consumer.Session::Attach { id }(crates/felis-client-core/src/connector.rsAttachSessionReq), which the daemon could correlate; a carrier retarget (PushRetargetHost,felis.proto:1615) lands on another daemon, which the origin daemon can never observe (docs/reference/ipc.md:816-820: "this daemon cannot resolve another daemon's roster").Verdict
accept-with-changes, split into a v0.1 part and a post-v0.1 part.
The recorded reason for deferring ("needs the daemon to model the client's post-push lifecycle") is still true for
failed: a window that fails to land has no connection to the origin daemon left to report on, sofailedis only ever inferable by timeout.landedandsuperseded, by contrast, are observable inside the session actor with one field on the wire. And--waitfor carrier targets is unobservable by construction and should be refused, not emulated.v0.1 (freeze-relevant, small):
accepted→queuedinOpsSwitched, the CLI/bridge objects, andnot_accepted→not_queued(coordinated under #23, which owns theerror.kindset).operation_idper push in the session actor and carry it onOpsSwitched, onPushReattach/PushRetargetHost, and as an optional field onSessionAttach, so a landing can be attributed later. This is the part that is expensive after the tag (a missing echo field means the verdict can never be attributed) and cheap before it.landedfor same-daemon targets in the session actor (attach carrying anoperation_idmarks it),supersededwhen a later push targets the same attachment before it landed. Bounded ring (e.g. 256 operations daemon-wide), oldest evicted.post-v0.1 (or in v0.1 if the above lands early):
--wait <timeout>andOps::OperationStatus { id }query withlanded | superseded | pending;failedis reported only as "the attachment's superseded connection closed and nothing landed within the wait", and documented as inferred.Principle check: this grows the IPC vocabulary with typed data (principle 1 explicitly allows it) and adds no heuristic. The "Revisit if" trigger is at least half met by the agent skill's own polling advice.
Approach
Protocol (
felis.proto,messages.rs,convert.rs)OpsSwitched:accepted→queued; adduint64 operation_id.PushReattach/PushRetargetHost: addoperation_id.SessionAttach: addoptional uint64 operation_id(echo; must not be aCorrelation, which #48 makes an exclusive request/stream identity). Later:OpsOperationStatus { id }/OpsOperationReport { oneof verdict { Pending; Landed { attachment }; Superseded { by }; } }.Daemon (
crates/felis-daemon/src/serve/session_task.rs):1170-1200, theaccepted += 1loop): mintoperation_idfrom a daemon-wideAtomicU64, stamp it on the pushed frame, and record{op, attachment_id, state: Pending}in aVecDequeon the pool (bounded). Any newer push to the sameattachment_idsets the older opSuperseded.SubscribeReq(serve.rs:571-580) carriesoperation_id; on a successful subscribe the actor marks itLandedif it is stillPending.route_ops:OperationStatuslookup (post-v0.1).Client core / CLI / bridge
connector.rsattach(id, intent)gainsoperation_id: Option<u64>; the GUI's reattach push handler (event_handler.rsRetargetRequested/reattach path) threads the id from the push into the landing.cli_sessions.rs:1600-1730: output objects gainoperation_id;--wait(post-v0.1) pollsOperationStatuswith a bounded deadline and prints per-attachment verdicts.Docs cascade
docs/reference/ipc.md:801-840(Switch/Switched, theacceptedparagraph), Push section, minor ledger or 2.0 base;docs/explanation/architecture/ipc.md"Acceptance, not landing" (rewrite: what became observable, whyfailedstays inferred, why carrier targets never get a verdict);docs/reference/cli.md:175-177,:476-483,:535(accepted→queued,operation_id),:109error kinds;docs/reference/control-surfaces.mdverb table;skills/felis/SKILL.md:327-354,:397-399;CHANGELOG.md.Tests
session_task.rstests beside:3640: push then attach-with-id →Landed; two pushes to one attachment → firstSuperseded, secondPending; a landed op can never later flip to superseded (state machine is monotone:Pending → Landed | Superseded).no_match.switch --format jsonobject shape withqueuedandoperation_id;not_queuederror.Dependencies
operation_idis a plain field, not correlation) and #47 (arm placement ofOperationStatus).failed).Risk/effort
M for the v0.1 part (one field on three messages plus a bounded table in the actor); L with
--wait/query. Main risk: promisingfailedand then reporting it on a timeout; the docs must call it inferred, or the contract over-promises exactly asacceptedwas accused of doing.Labels
Keep
priority/P1,release/v0.1.0for the rename +operation_id+landed/supersededrecording. Move--waitandOperationStatusto apost-v0.1follow-up issue unless #24 lands before #30 with room to spare; they are additive once the id is on the wire.Review amendments (round 2)
OpsSwitch, not per push.SwitchScope::Allfans out to several attachments (session_task.rs:1175-1200loops overtargets), so a scalaroperation_idonOpsSwitchedcannot name per-push ids. The daemon mints exactly oneoperation_idperOpsSwitchrequest, stamps the same id on every resultingPushReattach/PushRetargetHost, and records one entry{operation_id, targets: [{attachment_id, state}]}with one verdict per targeted attachment (Pending | Landed | Superseded { by }).OpsSwitchedreturns{queued, operation_id}; the laterOpsOperationReportreturns the per-attachment vector.{daemon_incarnation: u64 (random, minted at daemon start and reported inWelcome/daemon status), sequence: u64}encoded as oneOperationIdmessage (two fixed64 fields), so an id from a previous daemon can never resolve to a later operation; a query carrying a foreign incarnation getsUnknown { reason: OtherIncarnation }. This is decided now, even though the query arm lands post-v0.1.0, because the field shape freezes at the tag.MAX_OPERATION_RECORDS = 256, oldest evicted); a query for an evicted id returnsUnknown { reason: Evicted }; after restart every earlier id returnsUnknown { reason: OtherIncarnation }. Both are documented inipc.mdand tested.Review amendments (round 3)
session_task.rs:944), so an operation id alone cannot say which of several targets landed. EachPushReattach/PushRetargetHostcarries{operation_id, target_attachment_id}(the attachment the push was delivered to), andSessionAttachechoes both. The daemon validates the pair against the pending record (operation_idknown,target_attachment_idamong its targets and stillPending), marks exactly that targetLanded { new_attachment_id }, and ignores an unknown or already-settled pair with adebuglog. Test: an all-mirror switch with three windows whose landings arrive in reverse order produces threeLandedentries each naming the right original attachment.Review amendments (round 4)
PushRetargetHosttheSessionAttachlands on the target daemon, which holds no record; the origin daemon can only ever reportPending(orSuperseded) for a carrier target. Scope the{operation_id, target_attachment_id}validation and the reordered-landing test toPushReattach; document carrier targets as unobservable by design (matching the "carrier targets never get a verdict" statement above). A client-to-origin completion report is rejected for v0.1.0 (it would make the client a participant in daemon bookkeeping across hosts); "Revisit if" a cross-host verdict is needed.The "Track retarget completion" scope here is resolved by the decision on #296: operation ids leave the v0.1 contract, and
queued(queue admission) stays the only completion claim a switch makes. The reasoning is in #296's decision comment (#296 (comment)); the removal lands in #298. No follow-up issue is filed for completion tracking: it is deferred until a consumer that must act on the window's new home appears, which is the recorded revisit trigger indocs/explanation/architecture/ipc.md"What a switch reply can report".