Fix check_pr_ready follow-ups from the #378 review - #381
Conversation
CI's formatting step failed on #378: the file had been run through prettier, not the repo's oxfmt, and the two disagree on one wrap. Co-authored-by: Michiel Westerbeek <[email protected]>
Review on #378: a ready PR with one passing and one skipped check was summarized as "all 1 check passing", and one with only skipped checks as "all 0 checks passing", contradicting the rollup underneath. The sentence now says "1 check passing and 1 skipped" or "1 check skipped, none run". Co-authored-by: Michiel Westerbeek <[email protected]>
🤖 OS review · request changes · quality 3/5 · risk highSafe once the P2 below is fixed. 🔴 Risk high · not fully recoverable
1 inline comment below. Reviewed 🔁 1 finding → owning session · fix round 1/6 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| ? `all ${plural(checks.passing.length, "check")} passing` | ||
| : "no checks reported", | ||
| : !checks.passing.length | ||
| ? `${plural(checks.skipped.length, "check")} skipped, none run` |
There was a problem hiding this comment.
🟠 P2 — Neutral checks are incorrectly reported as never run
checkOutcome groups GitHub's NEUTRAL conclusion with SKIPPED, so a ready PR whose only check completed with NEUTRAL reaches this branch and says 1 check skipped, none run. That check did run and completed neutrally, making the new spoken verdict factually wrong. Distinguish neutral outcomes from skipped ones, or use wording that does not claim every item in this bucket never ran, and add a neutral-only test.
There was a problem hiding this comment.
Fixed in 943f57d: NEUTRAL is now worded apart from SKIPPED everywhere the bucket is spoken. Each check carries its conclusion, and the ready sentence reads "1 check neutral, none failing" or "1 check passing, 2 skipped, and 1 neutral"; the note and the Checks rollup line split the counts the same way. Test added for neutral-only and mixed.
Review on #381: NEUTRAL and SKIPPED share the no-result bucket, so a ready PR whose only check completed neutral was summarized as "1 check skipped, none run". A neutral check did run. Each PrReadinessCheck now carries its conclusion, and the sentence, the note, and the rollup line count skipped and neutral separately: "1 check neutral, none failing", "1 check passing, 2 skipped, and 1 neutral". Co-authored-by: Michiel Westerbeek <[email protected]>
* Merge risk: name the evidence behind each factor
The review comment listed bare factor categories ("auth or billing,
delivered output, CI or deploy") next to one sentence of reasoning, so a
reader could not tell which files earned which label or why. On
tella-fusion#6468 "auth or billing" came from a parameter-object refactor
of refreshSessionWithToken and read as unrelated noise.
Each factor now carries its own evidence (which files, what changed) and
the comment renders one bullet per factor with that evidence, ordered by
the factor that sets the recovery time. Reasoning is refocused on what a
revert leaves behind. auth_or_billing splits into auth and billing so the
label matches what was actually touched; the auth path hint also covers
login and sso files.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Move the web Desk voice call to GPT-Live (tellahq#350)
* Keep a video's letterbox black from darkening its border
.md-video paints a black background for letterboxing, and the default
background-clip lets it show through the 25% grey border, which then reads
as near-black (rgb 32,32,32) against a light page. Clip the background to
the padding box so a video's edge matches an image's.
Co-authored-by: Kent de Bruin <[email protected]>
* Desk: float the panel with drag and resize instead of a modal
On desktop the Desk is now a floating, non-modal panel: no backdrop or
blur, no focus trap, and the page underneath stays live, so it can sit
open in a corner while you work in other sessions. Its header drags it,
its edges and corners resize it, and the place it was left is kept per
browser, anchored to the nearest corner so it stays put when the window
changes size. It opens over the Desk trigger in the bottom-right corner.
Escape closes it only from its own focus, so an Escape meant for the
session you are working in does not take the Desk with it. Cmd+J now
summons it, jumps back into it when it is open elsewhere, and dismisses
it from inside. The phone sheet is unchanged.
Adds a `floating` variant to ui/modal: full bleed like the palette, no
backdrop, positioned by the caller, for use with `modal={false}`.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Desk voice: link spoken PR and session references in the transcript (tellahq#369)
A GPT-Live Desk call mirrors what was said as plain text, so a PR comes
out as "six four seven four" and a session the Desk just started is only
named by title; neither renders as a chip. A per-call ledger now records
every PR (repo + number) and session (id + title) the call's tool calls
surfaced, and each assistant row is rewritten before it is mirrored: a
spoken number that matches exactly one PR in the ledger becomes
`repo#N`, and the first row after start_session/steer_session (or a row
that says a listed session's title) gets a `Session: <id>` trailer. A
number that matches nothing, or PRs in two repos, is left as said; user
rows are never touched.
list_current_work and inspect_session now include prNumber, and the
backend prompt asks for PR numbers as digits with their repo.
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Desk voice: choose the Live backend and report call usage (tellahq#370)
* Add Desk voice backend choice and call diagnostics
Co-authored-by: Michiel Westerbeek <[email protected]>
* Add desktop and phone proof for Desk voice settings
Co-authored-by: Michiel Westerbeek <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Desk voice: stream the call's transcript as live captions (tellahq#371)
* Desk voice: stream the call's transcript as live captions
The web Desk received session.output_transcript.delta and
input_transcript.delta on the RTC data channel but only used them to
update the call status; the mirrored row appeared only once the server
settled the utterance (2s timeline gap, 2.5s idle, or the other speaker).
The client now hands every fragment to a call-scoped caption store that
renders one streaming row per speaker until the durable row lands.
Mirrored row ids carry the row's timeline span (-end-<endMs>) so the
captions come down by position rather than text, which keeps them
correct when the server links what the Desk said to PR and session
references before mirroring. Legacy ids without the end still match by
content.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Desk voice: normalize sideband caption timestamps
Co-authored-by: Michiel Westerbeek <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Native PR panel: hold a merge for five seconds with Undo and a countdown (tellahq#365)
* Native PR panel: hold a merge for five seconds with Undo and a countdown
The web client holds a confirmed merge for five seconds before the request
goes out, keeps the button in place reading "Merging…", and puts an undo glyph
with the seconds left in front of it. The native panel sent the request the
moment the confirmation was tapped.
DeferredMerge is the held merge: one per panel, a clock-driven countdown in
whole seconds, refused while one is already held or in flight, cancellable only
inside the window. The PR panel keeps its merge-method confirmation, then holds
the request; the Merge row stays where it is as "Merging…", an Undo control with
the digit lands in the toolbar in front of the actions menu, and the panel's
busy state and spinner wait for the request itself so nothing reads as merged
before anything was sent. Closing the panel inside the window takes the merge
back; a request already on the wire runs to its end.
Tests drive the window with the manual clock: the countdown, undo, one-shot
execution, a replaced schedule that must not fire, no undo once in flight, and
a failed send freeing the slot.
Co-authored-by: Kent de Bruin <[email protected]>
* PR panel: keep Review closed while a merge is held or on the wire
Review can squash and merge after approving, and that path sent its own
/pr-merge while a held merge was still armed, so one tap sequence could put two
merge requests on the wire and report a failure for a PR the first had merged.
Review now waits until the panel has no merge held or in flight.
Co-authored-by: Kent de Bruin <[email protected]>
* PR panel: disable Close while a merge is held
Co-authored-by: Kent de Bruin <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Native: move a host session into a Sandbox (tellahq#364)
* Native: move a host session into a Sandbox
The web gained "Move to Daytona/Box" for a code session that runs on this
machine (POST /api/sessions/:id/sandbox/attach, commits ef430bc and
0d07410); the native app had no way in.
OS1API.attachSandbox posts { provider, confirm? } and answers a
SandboxAttachOutcome: the 428 is an answer, not an error, so the server's
own sentence about the uncommitted files or unpushed commits reaches the
person before a repeat with confirm. SandboxMove mirrors the server's
refusals (a materialized Sandbox, a Runner, an automation by flag, name or
id, a non-code or repo-less session) so no surface offers a move the
server would 409, and takes the composer's Ready providers minus the host.
Three surfaces share one SandboxMoveViewModel and the same rows: a Move to
Sandbox submenu in the iOS session overflow menu, a cube toolbar menu on
the Mac, and a Runtime section in the worktree details sheet. The rows are
disabled while the agent runs, with the server's reason. A 428 becomes a
"Move to Daytona anyway?" confirmation carrying the server's sentence and
a Move anyway; a refusal is an alert. On success the session row is
refreshed from the server and the sheet overlays the recorded provider
until the poll agrees, so the Sandbox section says Preparing at once.
DEBUG hooks OS1_SANDBOX_MOVE, OS1_SANDBOX_MOVE_CONFIRM and OS1_SCROLL_TO
let the capture tool drive the real flow on a simulator that takes no taps.
Co-authored-by: Kent de Bruin <[email protected]>
* Native: no Move to Sandbox once a provider is recorded
The eligibility rule let a session whose Sandbox was still preparing move
again. The server takes that second attach and provisions twice; when the
first finishes, the provider mismatch means that Sandbox is neither
recorded nor destroyed. The web ends the offer as soon as any non-local
provider is recorded; the native rule now does the same. A failed Sandbox
is retried from the Sandbox section's Recreate, not by moving again.
Co-authored-by: Kent de Bruin <[email protected]>
* Native: record the Sandbox the moment the move lands; allow a failed one to retry
Two holes in the Move to Sandbox eligibility rule.
The success path only refreshed the row from the server in a detached task,
so until that GET landed (or for ever, if it failed) the surfaces still
held the host snapshot and offered a second move the server would take.
SandboxMoveViewModel.adopt now writes the attach response onto the open
session synchronously, through updateSessionSnapshot, before the refresh
starts, and the model remembers the moved session so the rows stay
disabled even if a sessions poll hands back the host snapshot late. All
three surfaces go through it.
The previous round's rule refused every recorded provider, which pinned a
session whose provision failed before making anything (needs_attention,
no id): Recreate needs an id, and the server permits the attach retry in
exactly that state. The rule now refuses a materialized Sandbox and a
preparing one, and lets that explicit failure move again.
Co-authored-by: Kent de Bruin <[email protected]>
* Native: allow Sandbox move retry after provisioning failure
Co-authored-by: Kent de Bruin <[email protected]>
* Test Sandbox failures with immutable decoded snapshots
Co-authored-by: Kent de Bruin <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Answer the native ask card's letters from the keyboard (tellahq#366)
* Answer the native ask card's letters from the keyboard
The question card labels its options A, B, C, but the native app only
heard those letters through a tap. A question lands while you are reading
the transcript or sitting in the composer, so in practice the letters
were dead and you reached for the pointer.
AskQuestionCard now wears the letter on each row and listens for it: a
bare letter typed anywhere in the card's key window that is not a text
field picks that option, and on the lone single-select ask it answers,
exactly as a tap would. Text fields keep their letters, a chord or a held
key stays the system's, and only the card in the key window while the app
is frontmost answers, so two windows never answer one keystroke.
SwiftUI's keyboardShortcut cannot carry a bare letter safely on macOS,
where a key equivalent is matched before the field editor sees the key,
so the composer would answer instead of typing. AskKeyBridge scopes an
NSEvent monitor to the card's own window on the Mac (the same way the
composer reads Shift-Return out from under a focused field) and rides the
option rows as key equivalents on iOS, where text input already wins over
an unmodified key command. AskLetterShortcuts holds the pure
keystroke-to-option mapping and the "which card hears this" rule, both
unit-tested without a window.
The composer is the one place the letters cannot reach, so a rebindable
"Answer the Question" command (mod+i, the web's ask-focus) takes the
keyboard back from the composer and rings the card; the command palette
carries it while a question is waiting.
Co-authored-by: Kent de Bruin <[email protected]>
* Focus the answer field for native free-text questions
Co-authored-by: Kent de Bruin <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Re-read sidebar maps when another client writes them (native) (tellahq#363)
* Re-read sidebar maps when another client writes them
The native app re-read lanes, snoozes and hides only at launch, on
foreground, and on a 30-second tick. A Mac window that stays in front
never foregrounds, so a workspace claimed, snoozed or hidden from the
phone or the browser reached it late.
The server already sends a user_map_changed frame to that person's
sockets (5c11d43). Decode it, route it from the active account's
presence socket to the matching store, and refetch the sessions list
once the store has re-read: these maps decide which rows the scoped
list carries. The frame is scoped to the account on screen and to its
user, and each store's hydrate keeps pending local writes over the
response. WorkspaceSnoozeStore gains the same applyHydrated seam the
other two stores have, so the merge is covered by tests.
Verified on the Mac target against the live server: a snooze written
by a second client moved the row out of the Active band within
seconds, and the unsnooze brought it back.
Co-authored-by: Kent de Bruin <[email protected]>
* Order sidebar map re-reads so an older GET cannot win
The lane, hide and snooze stores release the main actor while their GET
is in flight, and a user_map_changed frame lands whenever it likes
during the 30-second tick. When both re-read at once, the tick's
response could have been served before the other client's write and
still publish last, dropping that write until the next tick. A write
this client confirmed meanwhile had the same effect: the GET may carry
the pre-write map.
Each store now keeps a HydrationClock: a GET takes a ticket when it
begins and its response is applied only while no newer GET has begun
and no write has been confirmed since. The hide and snooze stores gain
the applySaved seam the lane store already had, which is where the
confirmation is counted. Same rule as the web user-map's
hydrationVersion and confirmedVersions.
Co-authored-by: Kent de Bruin <[email protected]>
* Preserve newer sidebar resyncs when a PUT response arrives late
Co-authored-by: Kent de Bruin <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Forgive typos in the native sidebar search, command palette, and @ people (tellahq#361)
* Forgive typos in the native sidebar search, command palette, and @ people
Port the shared fuzzy matcher (shared/fuzzy-match.ts, commit 5b51f4d) to
Swift as Models/FuzzyMatch.swift, rule for rule: normalization, per-term
edit budgets, adjacent transpositions as one edit, in-word subsequences, and
multi-term queries, with the same parity fixtures ("relase" finds Release,
"wrokspace" finds workspace).
The sidebar filter now scores title, repository, branch, and workspace name
with it. SidebarSearch computes the matched ids in a detached task so a list
of thousands of rows is never scored on the main actor; the row predicate is
a set lookup. The Mac command palette ranks by the same scorer, title
matches a band above subtitle/keyword matches, and the @ palette ranks
people by score with the signed-in person and roster order breaking ties.
Co-authored-by: Kent de Bruin <[email protected]>
* Stop a superseded sidebar search scan instead of letting it finish
Task.detached does not inherit the cancellation that .task(id:) issues on
the next keystroke, so every superseded scan of the session list ran to the
end and competed with the live one. The view now keeps the detached handle
and cancels it from a cancellation handler, and SidebarSearch.matches reads
the cancellation flag once per row and per archived session, returning nil
so the caller publishes nothing for a scan that gave up.
Co-authored-by: Kent de Bruin <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Native app: show merge risk beside the review's quality score (tellahq#362)
* Native app: show merge risk beside the review's quality score
The server's review now scores two things apart: quality (the 1-5
confidence) and merge risk, how hard a mistake is to undo. The native app
decoded only the score, so a correct migration and a sloppy CSS tweak read
the same on a phone.
OsReviewSummary gains optional risk, recovery and riskFactors. Decoding
stays tolerant: a level or recovery word this build does not know costs
that field and never the session row it rides in. Risk shows on every
surface that already carried the score, always as its own phrase in its
own ink so the two axes cannot be read as one: the long-press preview
strip, the workspace review row, the report sheet (with recovery time and
factors above the write-up), the review loop's verdict facts, and the PR
status card as separate Quality and Merge risk rows. Colours are the app's
status inks (high red, medium yellow, low dim), not the web's, and a stale
reading goes faint with the rest of the review. VoiceOver names each axis
("quality 5 of 5, merge risk high") instead of reading "5/5 · high risk".
The screenshot fixture gains high-risk and stale-high-risk cases and now
takes the Mac's detail column, since an overlay on a NavigationSplitView
never reaches the AppKit split view.
Co-authored-by: Kent de Bruin <[email protected]>
* Native app: name both review axes in the preview strip's VoiceOver text
The strip shows "5/5 · approved" and "high risk" as two phrases in two
inks, which tells the eye which is quality and which is risk. Read aloud
they were one number and one word with no axis named, so this surface
never met the "quality 5 of 5, merge risk high" wording the workspace row
and loop verdict already use.
A fact now carries an optional spoken form; the review and risk facts
fill it in and the strip hands it to VoiceOver. Facts whose text already
says everything keep reading their text.
Co-authored-by: Kent de Bruin <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Let Desk navigate from text chat and voice (tellahq#373)
Use the same fixed-route action in the voice sideband and a dispatch-scoped text MCP. Bind text prompts to their verified browser at intake, preserve the dispatch identity through run RPC, revoke stale or cross-browser authority, and await browser acknowledgment.
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Compare block: 2-up, Swipe and Onion skin views
The before/after block only had the slider. GitHub's image diff offers
three views and this adds the other two under a switch below the stills:
2-up shows both whole, side by side under their names, each opening the
gallery on click; Swipe is the existing divider; Onion skin lays the after
still over the before at an opacity set by a range beside the switch.
The view is a per-user preference (user-pref.ts), 2-up until picked
otherwise, and one switch flips every comparison on the page. The pref is
created on the first build rather than at import so the module stays inert
in the DOM-less unit tests and the copy control that imports the registry.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Link sessions MCP tool rows to the session they name
A get_session, send_to_session, task_status or similar call showed the
target only as an id in its summary. The row now carries an Open chip
that opens that session in place through the transcript's delegated
session-link handler, and is reachable by keyboard. It is not offered
when the call is about the session being read.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Add a personal Agentation toggle under Debug settings, default off
Lands the two commits from os/agentation-debug-setting (58922fe,
ba62518) on main as one change, so the live release no longer has to
run from a branch that diverged from main.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Remove blanket Git publishing restriction from run instructions (tellahq#376)
Keep PR attribution and co-author requirements while removing the blanket instruction against merging, approving, or pushing the default branch. Leave command guards and MCP behavior unchanged.
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Fix MCP relay token sharing across runner processes (tellahq#375)
Publish immutable per-identity grant records atomically and read relay tokens asynchronously without process-local snapshots. Preserve legacy tokens for detached hosts and cover concurrent minting, token validation, and authenticated forwarding.
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Keep MCP relay token issuance compatible with gateway rollback (tellahq#377)
Stage the rollout: retain legacy token issuance while shipping fresh asynchronous legacy reads and v2 reader support. Defer v2 issuance until the rollback target supports it. Add rollback coverage and keep cross-process discovery and forwarding tests.
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Add check_pr_ready: one deterministic PR merge-readiness verdict (tellahq#378)
The Desk had no reliable way to answer "is this PR ready to merge?" for a
session's PR; it pieced the answer together from transcript reads that do
not surface CI, review state, or mergeability. opensession-repos now has
check_pr_ready: pass a PR URL, a repo id and number, or a session id (its
Review-tab PR: primary branch, attached repos, linked PRs), and get a
spoken one-sentence verdict, the blockers in fix order, every check's
latest run by name, the review decision and who gave it, the base branch's
rulesets, and the same verdict as JSON.
The fetch runs gh as the bot for the PR's repository through the existing
resolveGithubCredential path. GitHub computes mergeable lazily after the
base moves, so an open PR answering UNKNOWN is re-queried a few times
before an honest UNKNOWN reaches the verdict.
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Open session pills clicked inside the Desk (tellahq#380)
* Open session pills clicked inside the Desk
Session chips in markdown and the tool row's spawned-session pill carry
only a data-session-id and rely on the transcript's scroll container to
delegate the click. SessionViewer's pane does that (handleMessagesClick);
the Desk's pane never did, so every session reference inside the Desk
overlay was inert. Extract the click reading into one helper shared by
both panes and route the Desk's click to onOpenSubagent, which already
closes the overlay and opens the session in the full viewer.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Format pr-merge-readiness.ts
oxfmt output for the file tellahq#378 landed unformatted, so format:check
and every check gated behind it run again on this branch.
Co-authored-by: Michiel Westerbeek <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Desk: tab-aware show_in_app, live-call indicator, fuzzy session lookup (tellahq#382)
* Desk show_in_app: fuzzy title matching that prefers a primary over its derivatives
The resolver behind show_in_app matched titles exactly or as a substring, so
"Profile subtitle sidebar opening" was a full miss against "Profile Subtitles
sidebar opening", and a session and its own PR review or auto-fix worker came
back as two equal candidates.
Score titles with the shared fuzzy matcher (every spoken term lands whole or
within a small edit budget), drop filler a voice request carries when other
terms remain, take a clear leader among fuzzy matches and ask back on a near
tie. Drop a derivative (parentSessionId/spawnedBy, or an automation-owned or
agent-started session sharing the primary's workspace) whenever the primary
scores at least as well; phrasing that names the derivative outscores the
primary and reaches it directly.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Desk show_in_app: land on a named tab
show_in_app carried only a session or workspace id, and the browser handoff
mapped that onto the page route. A session's Review tab is not a route: it is
the workspace view tab the sidebar foregrounds. So the Desk had no way to
show "the review tab for this" and said it could not switch tabs.
Add an optional tab (chat, review, conversation, video) to the voice and text
tools, validated in the shared wire schema next to the id; panes that spawn
something stay out of reach. A workspace tab rides the existing route suffix.
For a session, review reuses the sidebar's pending-open pulse, chat clears the
workspace's remembered pane, conversation and video open the session's
workspace on that pane. The voice and Desk instructions name the parameter.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Desk: show a live voice call while Desk is minimised
The call state lived inside the Desk body, so after minimising the overlay
nothing on screen said a call was still running.
DeskOverlay reports the call through onCallActiveChange. While a call is live
and Desk is closed, the Desk trigger carries the app's pulse dot and reads
"Desk call in progress"; pushed phone pages have no trigger, so the mobile top
bar shows a handset control instead. Both reopen Desk. The indicator clears
when Desk is back up or the call ends.
Co-authored-by: Michiel Westerbeek <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Fix check_pr_ready follow-ups from the tellahq#378 review (tellahq#381)
* Format pr-merge-readiness.ts with oxfmt
CI's formatting step failed on tellahq#378: the file had been run through
prettier, not the repo's oxfmt, and the two disagree on one wrap.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Count skipped checks in the ready sentence
Review on tellahq#378: a ready PR with one passing and one skipped check was
summarized as "all 1 check passing", and one with only skipped checks as
"all 0 checks passing", contradicting the rollup underneath. The sentence
now says "1 check passing and 1 skipped" or "1 check skipped, none run".
Co-authored-by: Michiel Westerbeek <[email protected]>
* Word neutral checks apart from skipped ones
Review on tellahq#381: NEUTRAL and SKIPPED share the no-result bucket, so a
ready PR whose only check completed neutral was summarized as "1 check
skipped, none run". A neutral check did run. Each PrReadinessCheck now
carries its conclusion, and the sentence, the note, and the rollup line
count skipped and neutral separately: "1 check neutral, none failing",
"1 check passing, 2 skipped, and 1 neutral".
Co-authored-by: Michiel Westerbeek <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
Co-authored-by: Michiel Westerbeek <[email protected]>
* Add Databases: named SQLite databases sessions and automations keep
A database is a sibling of a report: a durable thing a run produces that
outlives it, kept by Open Session outside every repo and browsed in its
own view. Where a report is an append-only document, a database is a
named SQLite file a session keeps coming back to.
Store: databases-sqlite.ts owns the files (<id>.sqlite + <id>.json
sidecar under stateDir("databases")) and runs on a dedicated Bun Worker
behind the async facade in databases.ts, so an agent's slow query never
touches the gateway thread. database-sql-guard.ts screens every
statement (no ATTACH, DETACH, VACUUM INTO, load_extension, or non-schema
PRAGMA), reads run on a read-only connection, and results are capped.
MCP: opensession-databases (create, list, describe, query, execute,
insert_rows, update, delete) for interactive runs, and for every
automation run scoped to that automation's own databases, the way
opensession-report only publishes into its own group. Also on the
workflow-script allowlist.
Routes: /api/databases for list, schema, paged rows, read-only SQL,
rename, delete, .sqlite download and CSV per table.
Web: a Databases sidebar tool and view (rows per table, a Query tab,
desktop and phone), a Databases card on the session overview, and a
data-grid primitive shared by the two grids.
Co-authored-by: Michiel Westerbeek <[email protected]>
* Restore direct GitHub actions and fix ingress classification (tellahq#345)
* Restore direct GitHub actions and fix ingress classification
Remove the dedicated PR tools and blanket restrictions for connected-person code turns while retaining automated and read-only guards. Wait for bounded HTTP headers before routing stable frontend requests. Stabilize the actor-service verification fixtures without changing production timeouts.
Co-authored-by: Jaap Frolich <[email protected]>
* Lift the merge guard for projected person tokens in sandboxes
A sandboxed owner turn resolved no login because the guest has no person
store, so the merge guard stayed on even though the launcher had projected
the person's own token. The launcher now writes the resolved login as a
non-secret marker beside the token, and the guard reads that marker in the
guest. App-token and simple-mode projections carry no login and stay
guarded.
Co-authored-by: Jaap Frolich <[email protected]>
* Resolve the run's owner login from the selected credential
In simple mode the run holds the sole connected account's token, but the
owner login came from githubUserLoginForRun, which is null there, so the
merge guard stayed on for personal code turns. Read the login from the
credential githubCredentialForRun actually selects, on the host and in the
launcher's sandbox projection, so the guard follows the token the run holds.
Co-authored-by: Jaap Frolich <[email protected]>
* Allowlist the Slack thread identifier in the catalog test
The exact fixture value is a channel/thread identifier, not a credential. Allowlisting it also covers the historical commit scanned by CI.
Co-authored-by: Jaap Frolich <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Render rich transcript blocks in the native Swift client (tellahq#367)
* Render the web's transcript blocks natively in the Swift client
The native app kept every block the web renders as a raw fence: a
```choices list read as code, a ```tree as box drawing, a placed
OPENSESSION_IMAGE as a bare markdown image the library declines, a
```compare as two URLs. Now `TranscriptRichBlocks` lifts each kind out
of a durable message (the fence grammar is `MarkdownFenceSegmenter`,
which `MermaidSegmenter` is a thin claim on) and `MarkdownBody` draws
it with a view of its own under `Views/Blocks/`:
- choices: chips that send the reply on the composer's own path
(`SessionViewModel.sendQuickReply`, queued while a run is busy) or,
from the context menu, fill the composer; they go quiet once a later
user message exists (`QuickReplyRelay`, observable, in the environment)
and wherever there is no session.
- tree: a collapsible tree, top two levels open; a file row opens the
Changes panel when the session touched that file (`FileLinks.paths`).
- placed media: a paragraph that is one `/media?path=` image renders
where it was written with its caption, and the thumbnail strip shows
only what the body did not place (`PlacedMedia`).
- compare: a before/after control with a draggable divider, adjustable
for VoiceOver, expanding into the image viewer; both stills load with
the session's credentials.
- chart: Swift Charts for a unit-view subset of Vega-Lite (bar, line,
area, point, tick; nominal, quantitative and temporal x; colour and
xOffset series; count/sum/mean aggregates) with inline data only.
- artifact and svg: a WKWebView with content JavaScript off, a
`default-src 'none'` policy ahead of the source and every navigation
after the first refused; Source toggle, drag grip, expand sheet.
- slides: a 16:9 deck rendered through `MarkdownBody` (rich blocks off
inside), arrows, dots, counter, arrow keys, swipe, expand.
- palette, csv/tsv/table (sort, filter past eight rows, Copy CSV, laid
out by `MarkdownTableView`), json tree (Tree/Raw toggle), ansi
(through `TerminalScrollback` and the Terminal panel's palette), diff
(washed rows), math (a TeX vocabulary set as text, no KaTeX),
metrics cards and GitHub callouts.
Every parser is a pure model with a test beside it, and every one falls
back to the plain fence when it refuses the source, so a block still
streaming stays readable. Streaming rows are untouched; blocks are
parsed once per distinct text behind a bounded cache. Each view uses
OS1VisualStyle tokens, honours Reduce Motion and carries labels.
Co-authored-by: Kent de Bruin <[email protected]>
* Add native rich-block screenshots for the Swift client port
macOS and iPhone captures of the chart, metrics, CSV table, palette,
tree, callout and choices blocks rendering natively.
Co-authored-by: Kent de Bruin <[email protected]>
* Fix native rich-block review findings: bound JSON depth, exact chart grouping, aligned sort headers
- JsonTreeBlock: thread a depth counter through parseValue/parseArray/
parseObject and refuse past 64 levels, so a deeply nested fence falls
back to a code block instead of exhausting the stack.
- VegaLiteChart: make Value hashable and aggregate by the exact x value
plus series, not the display label, so times within one day and
closely spaced numbers stay separate groups.
- DataGridView: render sortable headers through MarkdownTableView's own
column plan and scroll container, reserving room for the indicator,
so header buttons line up with their body columns and scroll with them.
- Tests for each, README note.
Co-authored-by: Kent de Bruin <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
* Stop host Portals on archive and expire idle previews (tellahq#336)
* Stop host Portals on archive and expire idle previews
Host Portals outlived their sessions: archiving left dev servers running,
and an unused preview held memory indefinitely. This makes their lifetime
match how they are used.
- Archiving a session stops the host Portals it owns, including those in
attached repositories, without touching a sibling's Portal in a shared
checkout. The five-minute reaper also treats archived owners like missing
ones, so an interrupted cleanup is retried.
- On Linux a host Portal expires after 30 minutes without authenticated
Portal traffic or an established connection to its port (from /proc/net
tcp tables, so WebSockets count). Timestamps are process-local: a gateway
restart grants a fresh window instead of killing a browser's Portal on an
old timestamp. Without connection telemetry, idle cleanup is skipped.
- New host Portal processes are refused when available RAM is below 5% or
2 GiB, memory full-stall pressure is at 10% over ten seconds, or the
shared workload slice is at 90% of its memory soft limit. Matching awake
Portals are still reused.
- Registry writes go through an atomic, per-worktree serialized update
that re-reads the file and applies only the caller's changed records, so
concurrent starts in one checkout no longer clobber each other. Stops
carry an expected generation so a stale sweep cannot kill a replacement.
- The reaper takes an async catalog snapshot and all supervisor file IO is
asynchronous, keeping it off the gateway thread.
Tests cover archived, sibling, legacy ownerless, idle and generation cases,
the admission thresholds, /proc tcp parsing, and concurrent registry
updates. Supervisor tests install a no-op capacity probe so the host's own
memory pressure cannot decide their outcome.
Co-authored-by: Jaap Frolich <[email protected]>
* Serialize host Portal operations and archive by canonical owner
Review round 1 on the lifecycle change:
- A stop validated a record's generation, then terminated the process and
wrote the stopped record without excluding a concurrent start or restart
of the same Portal. Because the scope unit is stable per worktree and
name, a stale sweep could kill the replacement and overwrite its record.
Start, stop, and restart now queue on one lock per canonical worktree and
name, with restart as a single transaction, so the generation check holds
through termination and persistence. The lock queues rather than
coalesces: a stop after a start runs after it.
- Archive cleanup compared Portal owners against the archived id, which may
be a historical alias; the record is owned by the canonical session that
the lookup resolves to. Compare and stop with the resolved id.
Tests: a stale stop queued behind a restart rejects and leaves the awake
replacement supervised; archiving through an alias stops the canonical
owner's Portal.
Co-authored-by: Jaap Frolich <[email protected]>
* Recognize merged aliases throughout host Portal ownership
Co-authored-by: Jaap Frolich <[email protected]>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
---------
Co-authored-by: open-session-os-tella-dev[bot] <309495949+open-session-os-tella-dev[bot]@users.noreply.github.com>
Co-authored-by: Michiel Westerbeek <[email protected]>
Co-authored-by: Kent de Bruin <[email protected]>
Co-authored-by: Kent de Bruin <[email protected]>
Co-authored-by: Jaap Frolich <[email protected]>
Co-authored-by: OpenSession <opensession@localhost>
Follow-up to #378, addressing its review round (the branch itself was already squash-merged, so the fixes land here, on top of
main). Replaces #379, which was opened from the wrong head branch.pr-merge-readiness.tshad been run through prettier, not the repo's oxfmt; CI'sformat:checkfailed onmain. This is the oxfmt output, no code change.bun run checkpasses (format, typecheck, lint, isolated unit tests, snapshots).Started by Michiel Westerbeek in this OS session