Skip to content

Commit d2c7915

Browse files
alicodingclaude
andcommitted
feat: request lifecycle honesty — cancel_write, staleness tiers, badge fix (goal 0026)
Delivers all seven Mill-side items: a cancel_write MCP tool (the missing fourth verb alongside park/poll/resolve, ungated, a status distinct from denied); age-tiered staleness presentation with "expires in Nh" across Review, the MCPWriteApprovals banner, and the floating approval prompt; a requester-liveness heartbeat (LastPolledAt, shown only past a 5-minute no-noise bar); resolved MCP writes now durable in Review's Recently-resolved; Activity's MCP-write rows are expandable with a jump-to-workflow preview; and stuck-ENQUEUED runs get age emphasis plus Stop in WorkflowRunsPanel and Activity's runs explorer. Fixes a real bug found live: ResolveMCPWrite/CancelMCPWrite/the expiry sweep never fired the pending-changed signal at all, so the sidebar badge could hold a phantom count against an empty queue — traced to Wails3's own registered-event type check silently dropping an empty-struct payload against a typed event registration. Also: e2e's shard matrix gains fail-fast:false after a real incident (PR 11) where one shard's real failure cancelled two others mid-run before they could report their own verdict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft
1 parent 332dfaa commit d2c7915

30 files changed

Lines changed: 1682 additions & 189 deletions

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,19 @@ jobs:
269269
runs-on: ubuntu-latest
270270
timeout-minutes: 15
271271
strategy:
272+
# fail-fast: false -- real incident, not a hypothetical (PR 11,
273+
# run 31557343422, 2026-08-12): shard 3 failed on a real e2e
274+
# regression, and GitHub Actions' own fail-fast DEFAULT (true when
275+
# unset) immediately cancelled shards 1 and 2 mid-run rather than
276+
# letting them finish -- both were killed ~2.5 minutes into a
277+
# passing-so-far run, so their own genuine pass/fail signal was
278+
# simply never collected ("shard-2-cancelled-while-green": it
279+
# wasn't red, it never got the chance to report). A 3-way sharded
280+
# suite needs every shard's own verdict to know what's actually
281+
# broken, not just "at least one shard is red" -- the same
282+
# reasoning build-go's own 2-platform matrix comment already
283+
# states for its own fail-fast: false, applied here too.
284+
fail-fast: false
272285
matrix:
273286
shardIndex: [1, 2, 3]
274287
shardTotal: [3]

docs/SPEC.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2375,6 +2375,44 @@ turns out to solve this without touching that dispute).
23752375
all Go-tested; a Review-queue row appearing and its Approve executing
23762376
the write is e2e-tested (`mcp-write-approval.spec.ts`).
23772377

2378+
**A fourth verb — `cancel_write` — and a requester-liveness heartbeat
2379+
are now built (docs/goals/archive/0026-request-lifecycle-honesty.md),
2380+
completing park/poll/resolve/**WITHDRAW**.** `cancel_write {id}`
2381+
(`mcpsvc`, ungated — cancelling your own request only ever reduces
2382+
pending work) lets the requesting client withdraw its own still-pending
2383+
write; `cancelled` is a distinct `MCPWriteStatus`, never conflated with
2384+
`denied`, sharing `ResolveMCPWrite`'s at-most-once locking discipline.
2385+
`check_write_status` now also records `LastPolledAt` on every real poll
2386+
— the requester's own heartbeat — surfaced to Review as a muted
2387+
"requester last checked Nm ago" hint, shown only past a 5-minute
2388+
staleness bar (never on fresh polling, no noise). **A real BUG found
2389+
live in the same pass**: `ResolveMCPWrite` and the lazy expiry sweep
2390+
never fired the `mcp-write-approval` pending-count signal at all — a
2391+
resolved write's Activity/Review traces were correct, but the sidebar
2392+
badge and any poll-less surface (the banner, the floating prompt) could
2393+
hold a phantom pending count against an already-empty queue. Root
2394+
cause, once traced: `main.go`'s `application.RegisterEvent[MCPWriteRequest]
2395+
("mcp-write-approval")` binds that event name to an exact Go type
2396+
(Wails3's own documented behavior — "data types are matched exactly and
2397+
no conversion is performed") — emitting a bare `struct{}{}` silently
2398+
failed that check and the event was dropped before ever reaching a
2399+
browser client. Fixed by emitting a zero-value `MCPWriteRequest`
2400+
instead; every resolution path (approve/deny/cancel/expiry) now pings
2401+
it unconditionally. **Resolved MCP writes are now durable in Review's
2402+
own Recently-resolved section** (`MCPWriteResolved`/`ResolvedMCPWrites`,
2403+
merged newest-first alongside resolved runs, distinct `PlugIcon`
2404+
identity, not clickable) — previously the only trace of a
2405+
denied/expired write was session-only Activity, gone on restart, even
2406+
though the 24h outcome record was already persisted. **Activity's own
2407+
MCP-write rows are no longer action-dead**: expandable (the existing
2408+
`canExpand`/`result` mechanism) with a jump-to-target-workflow
2409+
`WorkflowHoverPreview` icon when the gated tool named an existing
2410+
workflow (`update_workflow`/`publish_workflow`/`delete_workflow`'s own
2411+
`id` argument — empty for `import_*` tools, which mint a new entity).
2412+
Proven end-to-end against a real MCP client, including the exact
2413+
phantom-badge regression (deny from Review, assert the sidebar badge
2414+
clears with no other page event) — `mcp-write-cancel.spec.ts`.
2415+
23782416
### 3.7 Global app settings
23792417

23802418
`SettingsService` (`settingsservice.go`) owns Mill's global settings
@@ -2640,6 +2678,28 @@ the integration-http/decision-outcome transport tail) +
26402678
HTTPRequest, independent of the presence gate — the layer that reaches
26412679
the owner with no local Mac to notify on at all.
26422680

2681+
**Staleness presentation — `LOCKED` and built
2682+
(docs/goals/archive/0026-request-lifecycle-honesty.md), applying the §1
2683+
thesis to time-honesty of a pending ask.** Owner-observed 2026-08-11: a
2684+
4-hour-old test write sat in Review looking exactly as urgent as a
2685+
fresh one ("feels like I missed something"). Age-tiered, not binary:
2686+
fresh (<15 minutes) renders as-is; older gets visible emphasis (an
2687+
attention-colored age badge) plus an "expires in Nh" caption counting
2688+
down the shared 24h clock (`frontend/src/shared/staleness.ts`'s
2689+
`ageTier`/`formatExpiresIn`, one implementation reused by ReviewView's
2690+
pending rows — both guardrail/human-review/debug parks and MCP write
2691+
requests — the `MCPWriteApprovals` banner, and the floating approval
2692+
prompt, `shared/StalenessBadge.tsx`). **No auto-dismiss** — the VS Code
2693+
severity-rule precedent (§9.1-adjacent) holds; expiry is the only
2694+
terminal timer, staleness is presentation only. The same age-tier
2695+
mechanism, at its own 5-minute bar, now also flags a **stuck-ENQUEUED
2696+
run** (a real zombie run — queued forever, never dequeued — found in
2697+
production data) in `WorkflowRunsPanel` and Activity's runs explorer,
2698+
alongside the Stop/`CancelRun` affordance DBOS's own `CancelWorkflow`
2699+
already supports for that status ("cancels a running or enqueued
2700+
workflow," confirmed against a real constructed ENQUEUED run, not just
2701+
trusted from the doc comment — `TestListRuns_EnqueuedRun_PresentationFieldsAndCancelPath`).
2702+
26432703
**Still `OPEN`, real named gaps:** a menu-bar/dock presence toggle (see
26442704
above); appearance settings beyond light/dark; a default working
26452705
directory/scope (blocked on §6); fullscreen window-state tracking

docs/adr/0034-git-ci-operating-model.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,10 @@ Both are resolved now, the second one reversed.
164164
build now that the mechanism is understood and the felt cost (a
165165
15-minute-plus e2e matrix run on a pure `docs/goals/*.md` edit) is
166166
real.
167+
- **`e2e` job's shard matrix gained `fail-fast: false`** (goal 0026),
168+
after a real incident (PR 11, run 31557343422): shard 3 failed
169+
legitimately, and GitHub Actions' own fail-fast default immediately
170+
cancelled shards 1 and 2 mid-run rather than letting them report
171+
their own verdicts — signal lost, not just noise, the same class of
172+
problem `build-go`'s own 2-platform matrix already carried this flag
173+
for.

docs/goals/BACKLOG.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,19 @@ this pipeline and on this code)**
4444
backend-side `isAway`), alert-style authorization request (notify.Start),
4545
cross-device forward (`composition.SendJSONWebhook`,
4646
`ForwardPendingApproval`) — see ADR-0032's Update note
47-
4. [ ] [0026 — Request lifecycle honesty](0026-request-lifecycle-honesty.md)
48-
— withdrawal verb (`cancel_write`, the MCP Tasks `tasks/cancel`
49-
precedent ADR-0032 already mirrors) + staleness/expiry presentation
50-
(owner-observed 2026-08-11: a 4h-old stale ask reads as breakage)
47+
4. [x] [0026 — Request lifecycle honesty](archive/0026-request-lifecycle-honesty.md)
48+
— delivered 2026-08-12: `cancel_write` MCP tool (a distinct
49+
outcome from denied, ungated, at-most-once); age-tiered staleness
50+
presentation (Review/banner/floating prompt) + "expires in Nh";
51+
requester-liveness hint (`lastPolledAt`, >5m-stale gate); the
52+
phantom-badge BUG fixed (every resolution path — approve/deny/
53+
cancel/expiry — now pings the pending-changed signal, found live:
54+
an empty-struct payload silently failed Wails3's own registered-
55+
event type check); resolved MCP writes now durable in Review's
56+
Recently-resolved; Activity MCP-write rows are expandable with a
57+
jump-to-workflow preview; stuck-ENQUEUED runs get age emphasis +
58+
Stop in WorkflowRunsPanel/Activity's runs explorer. Item 4
59+
(session-side hygiene) intentionally not a Mill code change.
5160

5261
**Ratified 2026-08-10 (owner): three groups, A→B→C. 0001 stays standing
5362
live-review material, interleaved during owner reviews, not a lane.**

docs/goals/0026-request-lifecycle-honesty.md renamed to docs/goals/archive/0026-request-lifecycle-honesty.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,43 @@ An actionable item must carry its freshness, and the requester must be
88
able to take back a request that stopped mattering.
99

1010
## Plan
11-
1. [ ] **`cancel_write` MCP tool** (the missing fourth verb —
11+
1. [x] **`cancel_write` MCP tool** (the missing fourth verb —
1212
park/poll/resolve/WITHDRAW; `tasks/cancel` in the MCP Tasks spec is
1313
the direct precedent ADR-0032 already mirrors): the requesting
1414
client cancels its own still-pending write by id; cancelled ≠
1515
denied (a distinct outcome, recorded in Activity like
1616
denied/expired — never traceless). Ungated (cancelling your own
1717
request needs no human approval; it only ever REDUCES pending
1818
work). At-most-once semantics shared with resolve.
19-
2. [ ] **Staleness presentation** in Review + banner + floating
19+
2. [x] **Staleness presentation** in Review + banner + floating
2020
prompt: age-tiered treatment (fresh <15m renders as-is; older gets
2121
a visible age emphasis + "expires in Nh" from the 24h clock) — the
2222
§1 thesis applied to time-honesty of asks. No auto-dismiss of
2323
actionable items (the VS Code severity rule holds; expiry is the
2424
only terminal timer).
25-
3. [ ] **Requester liveness hint** (design question, research first):
25+
3. [x] **Requester liveness hint** (design question, research first):
2626
should a pending write surface "requester last polled Nm ago" —
2727
check_write_status calls are the natural heartbeat — so an
2828
abandoned request is visibly abandoned? Cheap to record; decide
2929
presentation against the no-noise bar.
3030
4. [ ] Session-side hygiene rule for THIS workflow (rides the memory,
3131
not Mill): a test write parked for demonstration gets cancelled by
32-
its requester when the demonstration ends.
33-
5. [ ] **Badge staleness on write resolution (BUG, diagnosed live
32+
its requester when the demonstration ends. (Session-side, not a
33+
Mill code change — outside this delivery's own scope.)
34+
5. [x] **Badge staleness on write resolution (BUG, diagnosed live
3435
2026-08-11)**: ResolveMCPWrite (and the expiry sweep) never emits on
3536
the pending-changed channel, so the sidebar badge held a phantom 1
3637
against an empty queue — resolution paths must ping the same ONE
3738
signal parks do (goal 0005's model, missing emit).
38-
6. [ ] **Resolved writes appear in Review's Recently-resolved** — today
39+
6. [x] **Resolved writes appear in Review's Recently-resolved** — today
3940
a denied/approved/cancelled write vanishes from Review entirely
4041
(only trace: session-only Activity, gone on restart). The queue's
4142
own history must include write resolutions, durably (the persisted
4243
24h outcome records already exist — surface them).
43-
7. [ ] **Activity MCP-write rows get actions**: expandable detail +
44+
7. [x] **Activity MCP-write rows get actions**: expandable detail +
4445
jump-to-the-target-workflow (runs already drill down; writes are
4546
action-dead — owner: "so what I can do and nothing I can do").
46-
8. [ ] **Stuck-ENQUEUED runs surface honestly** (a run enqueued-forever
47+
8. [x] **Stuck-ENQUEUED runs surface honestly** (a run enqueued-forever
4748
reads as live; found: a zombie ENQUEUED run from a morning error) —
4849
age-visible like item 2, plus a Stop affordance where cancel is
4950
legal.

frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
export type {
55
DataChanged,
66
MCPWriteActivity,
7-
MCPWriteRequest
7+
MCPWriteRequest,
8+
MCPWriteResolved
89
} from "./models.js";

frontend/bindings/github.com/alicoding/mill/internal/services/mcpsvc/models.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,37 @@ export interface DataChanged {
1313
}
1414

1515
/**
16-
* MCPWriteActivity is pushed for a missed (expired) or denied MCP
17-
* write so it's no longer traceless (docs/goals/0005-pending-attention-
18-
* model.md item 3). Reuses the same activity-push shape App.tsx's
19-
* hotkey-activity handler already established, under a distinct
20-
* "mcp-write" ActivitySource so it's filterable, not conflated with a
21-
* workflow trigger.
16+
* MCPWriteActivity is pushed for a resolved (denied/cancelled/expired,
17+
* or approved-but-failed) MCP write so it's no longer traceless
18+
* (docs/goals/0005-pending-attention-model.md item 3). Reuses the same
19+
* activity-push shape App.tsx's hotkey-activity handler already
20+
* established, under a distinct "mcp-write" ActivitySource so it's
21+
* filterable, not conflated with a workflow trigger.
2222
*/
2323
export interface MCPWriteActivity {
2424
"description": string;
2525

2626
/**
27-
* Outcome is "denied" or "expired" -- an approved write never
28-
* reaches here, there's nothing traceless about it.
27+
* Outcome is "denied", "cancelled", or "expired" -- an
28+
* approved-and-succeeded write never reaches here, there's nothing
29+
* traceless about it.
2930
*/
3031
"outcome": string;
32+
33+
/**
34+
* ToolName/WorkflowID/Result back Activity's own MCP-write row
35+
* actions (docs/goals/0026 item 7: "so what I can do and nothing I
36+
* can do") -- ToolName is the gated tool this record was for;
37+
* WorkflowID is the workflow it targeted, when the tool names one
38+
* (update_workflow/publish_workflow/delete_workflow's own "id"
39+
* argument -- empty for import_* tools, which mint a NEW entity
40+
* rather than referencing an existing one, so there's nothing to
41+
* jump to); Result is what the Activity row's expandable detail
42+
* panel shows.
43+
*/
44+
"toolName"?: string;
45+
"workflowID"?: string;
46+
"result"?: string;
3147
}
3248

3349
/**
@@ -41,4 +57,33 @@ export interface MCPWriteRequest {
4157
"id": string;
4258
"description": string;
4359
"createdAt": string;
60+
61+
/**
62+
* LastPolledAt mirrors MCPWriteRecord's own field (docs/goals/0026
63+
* item 3) -- nil when the requester has never called
64+
* check_write_status on this id yet.
65+
*/
66+
"lastPolledAt"?: string | null;
67+
}
68+
69+
/**
70+
* MCPWriteResolved is the frontend-facing shape for an already-resolved
71+
* write (docs/goals/0026 item 6) -- Review's Recently-resolved section
72+
* reads this alongside RunSummary's own resolved rows, merged
73+
* newest-first. Retained for the same 24h window check_write_status
74+
* already promises (sweepLocked's own retention) -- "durable across a
75+
* restart" and "still visible for the same window an MCP client can
76+
* still poll" are the same guarantee, not two.
77+
*/
78+
export interface MCPWriteResolved {
79+
"id": string;
80+
"description": string;
81+
82+
/**
83+
* approved / denied / cancelled / expired
84+
*/
85+
"status": string;
86+
"error"?: string;
87+
"createdAt": string;
88+
"resolvedAt": string;
4489
}

frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/settingsservice.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ export function ClearKeybinding(commandID: string): $CancellablePromise<void> {
6464
return $Call.ByID(3036020909, commandID);
6565
}
6666

67+
/**
68+
* DebugBackdatePendingMCPWrite is an e2e-only test knob (docs/goals/0026
69+
* item 2's staleness presentation) -- see MillMCPService.
70+
* DebugBackdatePendingWrite's own doc comment for why this has to be an
71+
* in-process call rather than an external settings-file edit. Refuses
72+
* outside isolated test data (the same IsIsolatedData signal every
73+
* e2e run already sets via MILL_SETTINGS_PATH) -- never reachable
74+
* against a real production instance.
75+
*/
76+
export function DebugBackdatePendingMCPWrite(id: string, ageMinutes: number): $CancellablePromise<void> {
77+
return $Call.ByID(561097732, id, ageMinutes);
78+
}
79+
6780
/**
6881
* DismissApprovalPrompt hides the floating approval prompt and applies
6982
* the same focus-yield mitigation DismissPanel already uses -- called
@@ -283,6 +296,15 @@ export function ResolveMCPWrite(id: string, approve: boolean): $CancellablePromi
283296
return $Call.ByID(2304068895, id, approve);
284297
}
285298

299+
/**
300+
* ResolvedMCPWrites lists every already-resolved MCP write still in its
301+
* 24h retention window (docs/goals/0026 item 6) -- Review's
302+
* Recently-resolved section reads this alongside its own resolved runs.
303+
*/
304+
export function ResolvedMCPWrites(): $CancellablePromise<mcpsvc$0.MCPWriteResolved[] | null> {
305+
return $Call.ByID(3844643736);
306+
}
307+
286308
/**
287309
* RestoreMenuAccelerators reverses SuspendMenuAccelerators -- see its
288310
* doc comment for the full reasoning. Safe to call more times than

0 commit comments

Comments
 (0)