Skip to content

release - #74

Open
bharathm03 wants to merge 10 commits into
mainfrom
development
Open

release#74
bharathm03 wants to merge 10 commits into
mainfrom
development

Conversation

@bharathm03

Copy link
Copy Markdown
Contributor

No description provided.

Pin the terminal packages to the squash merge of antgrid-ai/dart_terminal#10 so Antgrid receives the origin-safe scrollbar synchronization and selection auto-scroll fix.
* A frame that answers one sentence is never spent on the next

A partial cap hit appends, records instruction_dropped AND emits a status
snapshot whose backlog that same sentence just moved. Only the amendment
path was credited for that, so the cap hit's frame was left to answer for
the next sentence in the queue — retiring its row and lifting the edit
lock while its extraction was still running. A survivor is now always
credited; crediting a frame the bridge never sends costs one re-baseline,
which is what the survivor was doing anyway.

Alongside it, four places where one rule had two homes: oneLine is defined
once in the import-free leaf and re-exported, hostsIn is built on
destinationsIn so "the subset of" is true by construction, clipQuote
escapes through previewForUser like every other user-facing preview, and
a shared clip() stops a cap landing between a surrogate pair. The
drawer's waits-on lines resolve against one map per build rather than
re-walking the backlog per link.

* The goal seeded at arm answers for its own append

A goal is extracted on the same per-terminal chain instructions queue on, and lands ahead of them. Its items moved the backlog count that was the only evidence a sentence had, so a preset tapped while the goal was still running was retired by the goal's own append — taking the "sending" row away, lifting the debounce, and lifting the drawer's edit lock while the preset's extraction had not started. The next wholesale edit then went out built from a list missing the items about to arrive.

One client reaches this: arm from the new-session prompt, tap a chip before the goal comes back. The mark is set on exactly the condition the bridge queues that pass on — a goal with words in it and no backlog beside it — spent by the first frame that actually moved, and dropped once the backlog is non-empty, since a goal is only ever extracted into an empty one and a mark left standing would swallow the frame the user's own sentence raised.
* fix(app): show the provisioning run instead of calling a queued session stopped

While worktree.setup runs the bridge holds the session's start, so the entry reports running:false with setup.pendingStart set. TerminalScreen branched on running alone and rendered "Session stopped" over a Start button whose press only re-entered the same gate — a dead control directly under a banner saying the workspace was being prepared.

The pane now branches on sessionStartQueued to a provisioning state whose body is the setup transcript itself: during the run that PTY is the only live output the session has, and it was collapsed behind a chevron above an empty pane. It carries the two verbs that end the wait — Start agent now (skip) and Cancel setup, the latter a bridge verb no surface had offered since it shipped.

Renames the banner's Skip to Start agent now: nothing about the run is skipped, the queued agent is released and the install keeps going. The banner drops its tail line and log disclosure while a start is queued so the transcript is not mounted twice, both derived from the same wire field.

* feat(bridge): let a project launch its agent alongside worktree.setup

Adds worktree.setup.startAgent: afterSetup | immediate. The default keeps today's behaviour — the session:start is queued and fired when the run settles — while immediate lets the agent come up with the first step, for a project whose setup is a cheap .env copy rather than a cold install.

The mechanism is one seeded field: beginSetup births the run with gateReleased true, which is the same state a Skip produces, so setupGate declines to report it and start() falls through to the spawn with no branch of its own. Skip stays idempotent, cancel and rerun are untouched, and firePendingStart finds nothing queued.

checkoutDeclaresSetup widens to checkoutSetupPolicy, returning declares plus startAgent; an unreadable config now fails closed on both axes rather than one. The rerun path re-reads the policy instead of remembering the create-time answer — a rerun is exactly when the checkout's branch has changed it. The services: deferral stays tied to declares alone: bun run dev against an unprovisioned node_modules fails with nobody watching, unlike an agent.

* feat(app): warn when the agent is live in an unprovisioned tree

Under startAgent: immediate — or after a hand-pressed release — the agent is running in a checkout that has no node_modules yet, which is a different claim from a neutral progress line. The banner is the only surface that can make it, so it takes the warning tone, says 'Workspace still installing' rather than promising a wait, and offers 'Cancel setup' in place of a release that has nothing left to release.

Derived from the two live facts (setup running AND session running) rather than a mode flag, so a config-set immediate and a hand-pressed Start agent now reach the same warning.

* feat: make the provisioning wait legible

"2 of 5" is actively misleading on a real setup block: step 1 is a 10ms copy: and steps 2-5 are the minutes. The pane now carries the ledger — done, current, still to come — and the banner an elapsed readout beside the rule, which is what separates a slow step from a hung one when a bun install prints nothing for four minutes.

The step names are the one new wire field: CheckoutSetupProgress carries them on every report of a run rather than once, SetupRuntime retains them like terminalId, and they are optional in both directions so an older app ignores the key and a state recovered from disk (which knows how many steps ran but not what they were called) renders no ledger at all rather than a column of blanks.

The elapsed reading comes off the bridge's clock, which for a remote machine is not ours; a negative result is the one shape of skew we can detect and it is answered by saying nothing.

* chore: start this repo's isolated agents alongside setup

Five steps, minutes on a cold worktree, and the agent is useful for most of that — reading, planning and searching all work in a tree that is only checked out. Revert this one line to put the wait back.

* fix: close the review's findings on the setup-start work

The load-bearing one is a bug: a rerun under startAgent: immediate re-arms the previous run's prompt AND opens the gate, and nothing fires a start behind an open gate — firePendingStart runs only when a run settles. The policy was therefore silently ignored on the one path that queues a start of its own, leaving the user to press Start agent now by hand on a project configured never to wait.

The rerun test that was supposed to cover this passed for the wrong reason: settleSetup awaits startDeferredServices, so the synchronous stop() beat firePendingStart and lastQueuedPrompt was never banked — the rerun took the no-requeue path. It now waits for the queued start, and fails without the fix.

App side: a chat session mounts AgentTranscriptView where the provisioning pane would be, so standing the banner's log and tail down on a queued start left a four-minute install with no output anywhere; the suppression now keys on the pane actually being there. The banner also stood down its action, since two Start agent now buttons 100px apart race for a run only one can end. The pane is keyed by session so an in-flight verb and its refusal cannot land on whichever session is on screen when the reply arrives, a null registration is named rather than dropped, and an expansion the chevron no longer offers is cleared instead of masked — masking alone unfolded the log by itself the moment the gate released.

Also folded two duplicated helpers back into their existing versions: listEquals for the step-name comparison, and the transcript's formatDuration so the two live elapsed readouts on one screen spell the same seconds the same way.
Regression from #64. Retiring a sentence off an activity record now always credits the terminal's next status frame, but two bridge paths recorded an instruction row and returned without emitting one — an amend-only drop, and a cap hit with no room at all. The credit was then spent by the NEXT sentence's own append, so that sentence's row stood forever and held the backlog edit lock, which under a full backlog is the only way to free room.

Fixed on the bridge, because no app-side rule can work: nothing in an instruction_dropped record says whether a frame is coming. Every path that records an instruction row now emits a snapshot straight after it, making the app's blanket rule a real invariant. An unchanged snapshot answers for nothing and costs one re-baseline.

The arm-time goal mark also now mirrors the bridge's goalChanged gate, so re-arming with an unchanged goal no longer sets a mark nothing will satisfy. A goal the bridge rehydrates from its own disk record still cannot be predicted from the app.
… answer the judge question before arming (#69)

* The one undo that leaves this machine asks first

Three of the four snapshot actions undo locally; undoing a force push writes to a shared remote. The offer is a scrolling list row whose entire body is the tap target, newest first, so the row most likely to sit under a thumb was also the one that could overwrite a ref for everyone on it.

Confirmed for force_push alone. The other three keep the one-tap prevention §5.2 buys back, and the dialog promises no recovery: the bridge pins the remote tip before overwriting it only when the ref still exists there.

* An escalation that stopped the agent sorts above one that merely waited

urgency reached exactly one consumer, the OS notification title, and that path returns early while you are viewing the session it names. Inside the app the field was invisible: high and normal rendered identically, and oldest-first sorting filed a fresh urgent row last.

high is not judge opinion. The engine mints it with no judge call at all for a blocking prompt, so it means the agent is stopped right now. Banded ordering with age deciding within a band, applied on the live push too, and marked in the meta column the three escalation row shapes share.

* Escalate-only is answerable before arming, not after walking away

The catalog already carried judgeCapable and the app already resolved the agent the way the bridge does, so the pre-arm half of the coverage answer was one field away. Until now it surfaced only as an ESCALATE ONLY chip, found on returning to a session that had woken you for everything.

The prediction is exact rather than approximate because nothing writes a per-session judge override yet: observabilityFor falls back to the session's own tool, which is what the catalog describes. A judge picker inherits the job of keeping that true. Withheld from the unwatchable arm, which already carries the stronger fact.

* Corrections to the three fixes above

latestEscalationId read escalations.last, which the new banding turned into the newest NORMAL escalation — never an urgent one, i.e. never the row a caller asking for the latest wants. Folded on at instead. No live caller today, so this was a trap rather than a bug.

The undo tap started async work from a void callback with a bare unawaited, which app/CLAUDE.md forbids outright: a throw past the dialog reaches PlatformDispatcher.onError as a FATAL with no in-app frames. Uses detached now. The urgency test is a shared escalationMeta closure rather than three hand-written copies, which is what the adjacent comment already claimed.

compareEscalations' doc asserted high is engine-minted only. It is not: escalate passes the judge's own notify.urgency through, so a judge-authored high sorts into the same band. The band is still right; the claim was not. Two bridge comments calling backlog.ts import-free were wrong the same way — it imports zod and ./evidence; what it actually has is a position below every consumer.

* The shield answers the judge question every time it is asked

The escalate-only caveat added to the arm explainer almost never rendered: the explainer is gated on FirstRunState.handlerArmedOnce, a once-EVER latch, while coverage is per-agent. A user whose first arm was a judge-capable agent never sees that dialog again and would meet an escalate-only one with no warning at all.

The shield tooltip is the pre-arm surface that answers every time, and it already carried the observability half. handlerShieldTooltip is top-level for the reason handlerArmExplainerBody is — the precedence is testable without pumping the panel — and keeps the explainer's order: unwatchable outranks escalate-only, since a session reporting nothing makes its judge moot.
…g, and mono inline code

The file viewer rendered markdown through markdown_widget's defaults — GitHub's light-theme tokens on a dark ground — with no reading measure, no way to follow a link, and no sense of a document's structure.

Adds three files. markdown_document_config.dart is the whole-document config: a document-scale heading ramp pinned at all six levels, dark-appropriate blockquote/hr/table tokens, mono tables that scroll internally rather than overrun the measure, list markers whose shape encodes nesting depth, task boxes drawn with AbIcons, a copy button on every fence, and a chip for a repo-relative image the viewer can open in its own image view. markdown_outline.dart is the heading rail: MarkdownWidget publishes its heading list and scroll index only into its TocController, so MarkdownTocController subclasses it to get both out, and the rail is built from AbListRow instead of the package's Material ListTile. markdown_link_target.dart classifies an href so a relative repo link opens in the viewer, a #anchor jumps in-document, and only http(s)/mailto leave the app.

Also fixes inline code across the agent transcript. CodeNode.style resolves as codeConfig.style.merge(parentStyle), and merge lets the argument win every non-null field, so the paragraph's sans family overwrote the configured mono one and CodeConfig was inert — every backticked identifier, path and flag rendered byte-identical to the prose around it. markdownAntgridGenerator re-asserts the family after that merge, family only, and transcript/markdown_body.dart renders through it too.
1.4.1 removes the `SetUp(messenger_, nullptr)` call from `~WindowsHostApi`. That call ran from a plugin registrar destruction callback, which FlutterWindowsEngine fires from Stop() after its own destructor has already nulled the messenger's engine pointer — so it dereferenced null inside FlutterDesktopMessengerSetCallback, whose only guard is an FML_DCHECK that is compiled out in release.

The app never pinned the platform package directly, so this is a lock-only change: `webview_all: ^1.3.5` in pubspec.yaml already admits 1.4.1, and no dependency_override or fork was ever added.

Verified the fix is in the bytes we resolve: the published archive's sha256 (d9b81f1…) matches both pub.dev and this lockfile, and that archive's `~WindowsHostApi` no longer makes the call. Upstream: abandoft/webview_all#37, fixed by abandoft/webview_all#38.

Not yet confirmed at runtime in Antgrid. Four local configurations (profile/release × with/without a host, three launch-and-close runs each) exit cleanly even on the buggy 1.3.10, so no loose build reproduces the fault and a clean run proves nothing. The signature is an access violation escaping a window-proc callback (0xC0000005 paired with 0xC000041D at the same offset), which is plausibly swallowed outside the packaged app — so confirmation has to come from an MSIX build. Refs #63, whose third close condition (no Application Error event on exit) is still open.

Claude-Session: https://claude.ai/code/session_01Y8Fukr3ARpsyhoxrSMcPcm
… for it (#71)

Two reports from a live two-session run, both confirmed against the logs.

The injected reply never sent. A TUI tokenizes a PTY read as a whole, and
Claude Code emits a control character as its own key event only while the
read is under 64 characters — so `text\r` in one write inserted a literal
newline into the composer and waited for a human Enter. Every submit now
writes the line, waits out a gap, then writes the CR alone, through a
per-terminal queue that keeps any other writer from joining that read.
The queue is a synchronous pass-through whenever no submit is in flight,
so nothing else on the terminal pays for it. A bare slash verb is padded:
splitting the CR would otherwise leave the suggestion list armed, and the
key that used to submit would accept a completion instead.

Handler answered for the agent. It holds less context and fewer tools than
the model it supervises, so a confident guess reads as fact and costs the
agent a correction it has no reason to make. It asks now, and decides from
what comes back.

Seven further findings from the log dig, each adversarially validated:
the runaway guard reset on keystrokes that submitted nothing; the judge's
timeout was silent on two of the three legs that spend the budget; the
activity feed reported a blocked action with prose about the pause rather
than the text a guard refused; the destructive floor missed several
outward-moving operations, and the wrap-up push buried an expiring undo
offer behind an unbounded summary.

Floor patterns are one operation each. Section 5.4 keys an authorization
lift on the pattern source, so an alternation over two operations lets a
lift on either grant both.
…#72)

The driver re-sends terminal:resize only when its computed grid differs from _lastSentCols/_lastSentRows -- what the app BELIEVES the PTY holds. Three ways that belief goes wrong with nothing to report the break, and in all three the panel has stopped moving, so the wrapper recomputes the same grid forever and the gate never reopens.

_TerminalGridFreeze re-armed its settle countdown on every invocation, and LayoutBuilder re-runs its builder whenever the parent rebuilds, not only when constraints change. Any rebuild cadence faster than the 150ms delay -- a streaming agent, a selection drag, a session-list tick -- cancelled the timer forever, so the grid stayed pinned to whatever it held when the panel last changed size: content clipped at the stale column with dead space beside it, for exactly as long as the agent keeps working. _settlingTo now measures quiet from the last real move, _sameSize replaces exact Size == so sub-pixel flex jitter does not read as movement, and dispose() cancels.

sendResize dropped a request whose per-install client id had not resolved while the caller booked it as sent. It now reports whether the frame was QUEUED -- true is not a delivery receipt -- and the three paths that discard an armed frame later (the debounce's driver guard, a cancellation in _handleTerminalSize, disposal) each hand the geometry back through an invalidation.

Nothing re-asserted geometry across a reconnect or a same-id respawn. A resize sent into a keyless window vanishes unreported, and a respawned PTY takes terminal-manager's process-wide lastDriverGeometry -- whichever terminal on that bridge resized most recently, not the one this driver sent the dead process. TerminalTab.sizeEpoch is the invalidation edge, the exact parallel of the snapshot-seq cutoff dropped beside it in _rehydrateTerminals, bumped on re-drive and on both respawn signals (terminal:started is not in kCheckoutDurableReplayTypes, so a relay app builds its tabs from the replayed agent:status and sees only that one).

The wrapper's per-PTY latches lived in a State that is not per-PTY: only terminal_screen keys it by terminalId, so the pinned pane, the detail view and the setup banner reused the previous terminal's booking across a swap. didUpdateWidget retires them.

Also corrects a load-bearing false premise. Two comments justified the freeze by claiming ghostty_vte_flutter does not reflow. It does -- soft-wrapped rows re-join when the grid widens. The freeze's actual rationale survives intact and is the other half: a TUI that wraps its own output writes hard breaks, which reflow never re-joins, so a grid change under an Ink-style redraw leaks stale fragments. terminal_reflow_contract_test.dart pins both halves with margin-filling rows.
… comments from a missing spec (#73)

* Notify-only leaves, and takes the config file's last setting with it

`notifyOnly` meant "tell me, never act": every pause escalated without spending a judge call, and a park wake notified instead of typing `continue`. Nothing in the product could turn it on. It was only ever sent as `defaultNotifyOnly`, which is read from handler-config.json — a file the bridge and app only ever read and never write. Hand-editing JSON was the sole path in, so the mode was unreachable by construction.

Two engine branches go with it, and `outputSnippet()` behind them, which had no other production caller. `defaultNotifyOnly` was handler-config.json's only setting, so the schema, its v1 migration, the loader, the engine's config cache and `cfg()` go too; config.ts survives on ActivityRecord and appendActivity, which is what a later judge or personality picker actually needs from it. No message type is added or removed, so the checkout-variable sets are untouched.

The accepted trade, stated rather than denied: anyone who did hand-edit that file has `notifyOnly: true` persisted in their handler-session record. Their parked sessions will now inject `continue` into a terminal they had opted out of auto-driving, and every pause will spend a judge call. Pre-v1, with no users, that is worth more than a compatibility shim for a mode the UI never exposed.

One test is replaced rather than deleted: the notify-only sub-case was the only thing asserting that a standing `guard_blocked` report is not a pending question, and the park-wake gate reads exactly that predicate. It comes back as a park-wake equivalent.

* The morning-after summary outlives the session that earned it

The wrap-up was composed from three private helpers, spent on one push, and thrown away. The `wrapped_up` activity row kept only the goal, which the app renders nowhere. That left the phone-was-off case with nothing at all: handler-activity.jsonl is write-only, `handler:activity` is not a REPLAY_TYPE, and HandlerState is never persisted — so an app that reconnected after the disarm had no feed to go back to, which is exactly when the summary is read.

A WrapUpRecord now persists in handler-wrapups.json and replays on handler:status, which is already a REPLAY_TYPE. One module owns content selection so the push and the stored copy cannot drift: buildWrapUp decides which items and which caps, and the push body and the activity detail are two thin renderings of its output.

The undo count is the one thing that must never be frozen. It is an argument to the push renderer, not a field on the record, and the app re-derives it: an undo taken after the wrap-up, or a re-arm retiring the offers, would each make a stored count permanently wrong on a card whose whole job is to be read later. The blocked count and its reasons ARE frozen, deliberately — they die with the session when disarm drops it. Freeze what dies with the session, never freeze what outlives it.

Sized for the wire it rides: handler:status is emitted twice per handler event, held by reference in the replay cache, and crosses the relay to a phone, so item text is clipped at 120 chars and five records are kept — a worst case of roughly 22K chars per frame rather than the 190K the uncapped shape allowed. The push loses its "see the activity feed" tail, because that feed is not durable and pointing at it was the bug.

Wrap-ups survive a re-arm. Snapshots are retired because each pins a stash, a backup ref and a trash copy and owes a release(); a wrap-up owns no resource, so retiring one would keep that cost and drop its reason — losing the previous session's report is the failure this change exists to fix.

* A wrap-up is read hours after the disarm, so it gets a card

The bridge replays a WrapUpRecord on handler:status; this is the surface that reads it. The Handler screen gains a pinned Wrap-up section between Sessions and Undo, and a project whose only remaining artifact is a wrap-up no longer renders as "Handler is off" — the morning after, that is the whole screen.

The undo count is derived at the call site from the terminal's snapshots and passed to the card, mirroring the bridge's own renderer taking it as an argument. Neither side of the wire is given a field it could freeze the count into, which is the only reason the number is still true when the card is finally read.

Sans, not mono: a wrap-up is the user's own prose plus chrome labels, never a path or a command. The outcome labels are a local switch rather than a reuse of the activity-row labels — those are keyed on activity decisions (`item_done`), not wire outcome statuses (`done`), and bridging them would be a second hand-mirror to keep in lockstep for four words.

* Comments stop citing a spec nobody can open

Roughly 180 comments pointed at numbered sections of a handler spec — `§4.3`, `spec §5.2` — that is not in this repository and is not in its history. A reader who followed one found nothing, and the tag was doing the work a sentence should have been doing.

Where the prose already carried the reason, the tag is simply gone. Where the section number WAS the justification, the reason is written in from the code that enforces it and names the symbol a reader can grep: `§5.4` becomes instruction-scoped authorization and the file that implements it, `§5.3` becomes the HARD floor tier, `§2.2` becomes the terminal states.

The references that resolve are untouched: bridge/src/e2e/ and packages/ cite docs/protocol/e2e-handshake.md by named section and that file exists, and two more cite RFCs. A path plus a named section is a working pointer; a bare number pointing at nothing is not, and that is the whole distinction being applied here.

No spec document was written to make the numbers resolve. Reconstructing one from the code would invent a source of truth that never existed and leave 180 comments citing a document written after them.

* Test names say what they check, not which section asked for it

The same dangling references ran through test names — `describe("§2.2 allTerminal is the wrap-up predicate")`, `group('quick choices (§4.6)')`. A test name is read in failure output, where a section number from a missing document is worth less than nothing: it names an authority the reader cannot consult instead of the behaviour that broke.

Renamed to describe the behaviour, and the comments inside these files got the same treatment as the source. Pass counts are unchanged — 3159 bridge, 3087 app.
@bharathm03 bharathm03 added the changelog-ignore Not user-facing: keep this PR out of the generated release notes label Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog-ignore Not user-facing: keep this PR out of the generated release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant