Skip to content

refactor: tlon-aligned agent architecture (UI core, abed peek/watch, linear migrations)#4

Merged
arthyn merged 29 commits into
masterfrom
refactor-peek-watch-via-abed
May 7, 2026
Merged

refactor: tlon-aligned agent architecture (UI core, abed peek/watch, linear migrations)#4
arthyn merged 29 commits into
masterfrom
refactor-peek-watch-via-abed

Conversation

@arthyn
Copy link
Copy Markdown
Owner

@arthyn arthyn commented May 1, 2026

Summary

Five focused structural refactors to align app/notes.hoon with tloncorp idioms. No semantic changes; backend tests stayed 53/53 green at every step.

  • 03ccf8b Extract HTTP handling into ++serve-http; move PWA static assets (manifest, sw, favicon-svg, icon-svg) into a |% core in lib/notes-ui.hoon. New scripts/build-notes-ui.sh regenerates the ++index arm from app/notes-ui/index.html without touching the static asset arms.
  • c1a6080 Mass-rename type imports: /- notes/- n=notes; all :notes refs in agent / lib / tests / mark files become :n.
  • 8bc56de Per-notebook peek/watch via abed: ++no-peek, ++no-watch in no-core; ++se-watch in se-core. Top-level +peek collapses to a generic [%x %v0 kind=@ ship=@ name=@ rest=*] delegate. ++no-abed relaxed (works on any net); ?=(%sub -.net) guard pushed into write/sign arms.
  • 90ceaac Wrap +poke in a |^ kelt — six poke-only handlers (join-remote, leave-remote, handle-{send,notify,accept,decline}-invite) nest inside +poke. Move 9 utility arms (slugify, strip-query, get-book, can-view-flag, etc.) to a contiguous block just before ++se-core.
  • 82de44f Restructure +load to follow the homestead/groups pattern: |^ kelt with +$ any-state head-tagged union and one ++state-N-to-N+1 arm per version step (1-to-2, 2-to-3, ..., 9-to-10). Replaces the prior nested ?: cascade where states 1–7 jumped directly to state-8.

Test plan

  • mcp__sidwyn__run-tests desk=notes path=/tests/app → 53/53 OK at every commit
  • Playwright suite — 12/12 in isolation; 2 environmental flakes under sequential load (pre-existing, identified in prior session — the host ship slows down running 12 tests serially with 30s timeouts; same tests pass at 11.6s in isolation)
  • Manual UI sanity (refresh published note, invite flow, sub→host edit propagation) — recommend doing once the ships are quiet

🤖 Generated with Claude Code

arthyn and others added 12 commits April 30, 2026 20:13
… core

Two related changes:

1. lib/notes-ui.hoon is now a |% core. The +index arm holds the HTML
   (regenerated from app/notes-ui/index.html via the new
   scripts/build-notes-ui.sh). +manifest, +service-worker, +favicon-svg,
   +icon-svg moved out of app/notes.hoon into this core. Agent imports
   as `/= ui /lib/notes-ui` and references arms as `index:ui`,
   `manifest:ui`, etc.

2. The %handle-http-request branch of +poke is lifted into a top-level
   ++serve-http arm (named to avoid shadowing the http namespace used by
   `response-header:http`). +poke now just delegates with a one-liner.

Net effect on app/notes.hoon: -45 lines.

scripts/build-notes-ui.sh splices index.html into the ++index arm body
without touching the static asset arms, so manifest/sw/svg can be
hand-edited independently. Triple-quote safety check kept.

AGENTS.md (CLAUDE.md symlinks to it) updated for the new workflow and
to switch the default dev ship reference from bospur to sidwyn.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mass-rename type references from :notes to :n by changing the import
declaration in each consumer:

  /-  notes  →  /-  n=notes
  flag:notes →  flag:n
  state-9:notes →  state-9:n
  ...

Affects desk/app/notes.hoon, desk/lib/notes-json.hoon,
desk/tests/app/notes.hoon, and the four mark files under
desk/mar/notes/.

Two collision fixes in the test suite where local face names shadowed
the import:

- ++nb-flag took `n=@ud` for the count, masking the n alias for the
  ^- flag:n cast. Renamed the parameter to `nid`.
- ++ex-history-len took both `nid=@ud n=@ud` for the same reason.
  Renamed the count parameter to `expected`.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move per-notebook peek and watch logic into arms inside the per-notebook
cores, so the top-level dispatchers shrink to flag-parse + delegate. The
notebook-state, flag, members, etc. are loaded once at abed time and
visible to all the inner arms.

- ++no-peek (in no-core) — handles all 7 per-notebook scry paths
  (%notebook, %folders, %notes, %note/<id>, %note-history/<id>,
  %folder/<id>, %members). Top-level +peek catches them with a single
  [%x %v0 kind=@ ship=@ name=@ rest=*] arm.
- ++no-watch (in no-core) — handles the local UI %stream subscription.
- ++se-watch (in se-core) — handles remote-subscriber %updates watch
  (delegates to existing ++se-watch-sub).
- ++no-abed relaxed: no longer asserts ?=(%sub -.net), so it works for
  any net type. The %sub assertion moved into ++no-action,
  ++no-start-watch, ++no-leave, ++no-agent — the write/sign arms that
  actually require subscriber semantics. ++no-response left without an
  outer guard because adding one breaks type narrowing on the %update
  branch.

Cross-cutting peeks (%notebooks, %published, %invites, /x/ui,
/x/debug/dummy) and watches (%inbox %stream) stay at the top level.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two structural moves in the helper core, no logic changes:

1. Wrap +poke's body in a |^ kelt so the six poke-only handler arms
   (join-remote, leave-remote, handle-{send,notify,accept,decline}-
   invite) live inside +poke instead of polluting the outer subject.
   The kelt arms still reach the outer subject (state, cor, give-inbox-*,
   se-core, no-core) since |^ extends rather than replaces it.

2. Move the nine utility arms to a contiguous block just before
   ++se-core, in this order:
     slugify, a-notebook-to-c-notebook, get-book, strip-query,
     can-view-flag, find-flag-by-nid, notebooks-changed-card,
     give-inbox-received, give-inbox-removed.

Net effect on +helper-core arm order: dispatchers (init, load, poke,
serve-http, watch, peek, agent, arvo) up top, utilities below them,
per-notebook cores at the bottom — so a reader sees the entry points
before the helpers.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructure +load to follow the tloncorp/homestead groups.hoon shape: a
|^ kelt with a +$ any-state head-tagged union and one ++state-N-to-N+1
arm per version step. Replaces the prior "jump to state-8" cascade
where states 1–7 each migrated directly to state-8 in a deeply nested
?: chain.

  ++  load
    |^  |=  =vase
    ^+  cor
    =+  !<(old=any-state vase)
    =?  old  ?=(%1 -.old)  (state-1-to-2 old)
    =?  old  ?=(%2 -.old)  (state-2-to-3 old)
    ...
    =?  old  ?=(%9 -.old)  (state-9-to-10 old)
    ?>  ?=(%10 -.old)
    =.  state  old
    cor
    +$  any-state  $%(state-10:n state-9:n ... state-1:n)
    ++  state-1-to-2  |=  s=state-1:n  ^-  state-2:n  [...]
    ...
    --

Each step is small and focused (5–30 lines) with a `~> %spin.[...]`
trace label. Cumulative 1 → 10 transformation matches the prior
cascade. The legacy v0/v8 entity types and intermediate state types
remain in sur/notes.hoon (used by the test suite to seed each starting
version).

Net: -39 lines in app/notes.hoon (-193 of the old cascade, +154 new).

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scry endpoints used to all return %json+!>(...). Now each returns a
typed mark with both noun and json grow arms, so callers can scry the
same path and get either the raw noun (e.g. via %notes-notebooks) or
JSON (.json extension on Eyre, or %json mark coercion).

10 new mark files in mar/notes/:
- notebook       — wraps notebook-detail (flag + notebook record)
- notebooks      — list of notebook-summary (flag + notebook + visibility)
- folder         — wraps folder
- folders        — list of folder
- note           — wraps note
- notes          — list of note
- note-history   — list of note-revision
- members        — list of member-record (ship + role)
- invites        — list of invite-record (flag + invite-info)
- published      — list of published-record (flag + note-id + html)

5 listing wrapper types added to sur/notes.hoon (notebook-summary,
notebook-detail, member-record, invite-record, published-record).

JSON encoders for the new types added under enjs:notes-json
(notebook-detail, notebook-summary, notebook-summaries, member-record,
member-records, invite-record, invite-records, published-record,
published-records).

++peek and ++no-peek now build the noun-typed value first, then return
it via the typed mark — Eyre's grow:json handles JSON on the wire.
"Not found" cases switched from json+!>(~) to outer-null `~`, the
idiomatic absent-scry response.

Test peek helpers updated: ++ex-mark asserts the new mark; ++peek-history
binds (list note-revision:n) instead of (list json); ++ex-gone simplified
to outer-null check.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit pass: every =/ that just renames a wing (e.g. =/  nid=@ud
id.nb-act) is reconsidered:

- 12 multi-use + verbose aliases switched to =* (the proper aliasing
  rune): n-act, who, fid in se-rename-folder/se-move-folder/
  se-delete-folder/se-create-note, nid in se-rename-note /
  se-move-note / se-delete-note / se-update-note / se-restore-note,
  nid-nb in se-batch-import-tree.
- 15 aliases inlined: short or single-use (nb-act inlined since
  a-notebook.act is only 14 chars; u-nb in no-apply-update — 11 uses,
  but u-notebook.upd is 14 chars so still better inlined; title in
  handle-send-invite at 1 use; etc.).
- 1 dead-code =/ deleted (fid in se-dispatch-folder, never referenced;
  the dispatch arm passes cmd wholesale).

Computed values (function calls, literals) keep =/ — they're not pure
aliases. The =/  =face:type  source style (e.g. =/  =flag:n  flag.act)
also stays since the =face syntax is a face-mold cast, not aliasing.

Net: -16 lines in app/notes.hoon.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the rule: use the natural type name as the face when the type is
short or used in only 1-2 places; only deviate when a real shadowing
collision forces it.

Renames in app/notes.hoon (no logic changes):

- new-nb / placeholder-nb / nb (×2)  → notebook
- nt (×12 sites in se-create-note, se-rename-note, se-move-note,
  se-update-note, se-delete-note, se-restore-note, batch-import arms,
  loop bodies, scry binding)  → note
- nf (folder, ×2)  → folder
- nf (flag, ×3 in state-9-to-10 migration)  → flag
- f=flag:n in se-abed / no-abed gate samples  → flag

Wing resolution (right-to-left dot lookup) means notebook.notebook-state
still finds the .notebook field of notebook-state correctly even with a
local face named notebook. The slight stutter in lines like
se-core(flag flag, ...) is acceptable per the rule — readability of the
type name wins over avoiding stuttery wing-replace syntax.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoon's =type syntax auto-creates a face matching the type name, so
`=/  flag=flag:n  ...` is just `=/  =flag:n  ...`. Same for =notebook:n,
=note:n, =folder:n in the spots where the previous pass renamed
abbreviations to their natural type names.

19 sites updated.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two style fixes across the helper-core arms:

1. ++se-rename-notebook had the read-mutate-write triplet:
     =/  =notebook:n  notebook.notebook-state
     =.  notebook  notebook(title ..., updated-at ..., updated-by ...)
     =.  notebook.notebook-state  notebook
   Three lines for one in-place field update. Collapsed to a single
   =. notebook.notebook-state ...notebook.notebook-state(...) write
   using %_ for the multi-field block. No alias, no copy.

2. 21 arms had ?> ?=(...) before ^+ se-core, opposite of the convention
   (cast right after the gate sample, then assertions). Swapped order in
   se-create-notebook, se-rename-notebook, se-delete-notebook,
   se-set-visibility, se-invite, se-member-{join,leave},
   se-dispatch-folder, se-dispatch-note, se-create-folder,
   se-{rename,move,delete}-folder dispatch arms,
   se-create-note, se-{rename,move,delete,update,restore}-note
   dispatch arms, plus a few more.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cells

Two stylistic rules applied across the helper core:

Rule 1 — when typing a =/ as a tuple, use * for parts not accessed at
that site:

- two %notes-action dispatch arms (poke %note branch + main): only
  -.net.entry is checked, so entry=[=net:n *]
- handle-send-invite: only title.notebook.notebook-state.entry is read,
  so entry=[* =notebook-state:n]
- the /v0/notebooks peek gate sample: only notebook-state half used
- the find-flag-by-nid gate sample: only notebook-state half used
- the state-9-to-10 xlat builder: only notebook-state half used

Rule 2 — same-subject cells collapse to [a b c]:subject when the cell
is the LAST element of the outer expression (right-associative noun
shape splices through):

- (slugify title.notebook.notebook-state id.notebook.notebook-state)
  → (slugify [title id]:notebook.notebook-state)
- /v0/notebooks listing item: [flag notebook.notebook-state visibility.
  notebook-state] → [flag [notebook visibility]:notebook-state]
- se-create-notebook constructions: [...our.bowl now.bowl now.bowl
  our.bowl] → [...[our now now our]:bowl]   (notebook + root folder)
- se-create-folder construction: bowl tail collapsed
- se-batch-import-tree folder: bowl tail collapsed

Note constructions (se-create-note, batch-import, batch-import-tree
notes) tried-and-reverted: revision=@ud follows the bowl block, so the
bowl-cell is NOT the last sub-expression and the right-associative
splice doesn't apply. Left as-is.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The %notes-action branch of +poke was a chain of ?: ?=(%X -.act) tests
ending in a ?> %notebook fall-through, with another nested chain inside
the %notebook case for %invite/%note/default-route, and a third nested
chain inside %note for %publish/%unpublish/default-route.

Restructured to:

  ?.  ?=(%notebook -.act)
    ::  switchable cases — %notebook is the meaty fall-through
    ?-  -.act
      %create-notebook  ...
      %join             ...
      %leave            ...
      %accept-invite    ...
      %decline-invite   ...
    ==
  ::  meaty %notebook body
  =/  =flag:n  flag.act
  ?+    -.a-notebook.act
      ::  default: route to host (se/no) based on net
    ...
  ::
      %invite
    (handle-send-invite ...)
  ::
      %note
    ?+    -.n-act
        ::  default: route
      ...
    ::
        %publish
      ::  local
    ::
        %unpublish
      ::  local
    ==
  ==

Default-routing logic appears in two places (outer ?+ default and inner
%note ?+ default) — kept the duplication; small enough.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arthyn and others added 12 commits May 5, 2026 16:13
Follow the +go-a-group pattern from tloncorp/homestead/groups.hoon:
the action handler converts to a c-notes command and pokes the host
unconditionally. If host == our.bowl, Gall loops the poke back through
+poke %notes-command and dispatches to se-core. Removes the host/sub
discriminator from the action surface.

Agent changes:
- +poke %notes-action default cases (both outer ?+ and inner ?+ for
  %note) drop the `?:  ?=(%pub -.net.entry)` branch. Each just calls
  no-abet:(no-action:(no-abed:no-core flag) act).
- ++no-action drops the `?>  ?=(%sub -.net)` assertion. Other no-*
  arms keep their %sub guards (those are genuinely sub-only).

Test harness changes (lib/test-agent.hoon):
- ++drain-self-pokes + ++drain-loop helpers detect any [%pass wire
  %agent [our.bowl <our-dap>] %poke =cage] cards in the emitted list
  and re-deliver them through on-poke. Bowl state is threaded via
  play-cards so wex/sup remain coherent. Recursion has a depth-16 cap.
- ++do-poke and ++do-agent run the drain after the initial agent call,
  so test assertions on state see the fully-applied result, not just
  the queued self-poke card.

Tests: 53/53 backend (no test changes needed — the drain is transparent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AGENTS.md still described state-4 (state-0..4 in sur, current state-4)
from before the migration cascade was rebuilt. Updated the State and
API Surface sections to match what's actually deployed:

- Current state is state-10, with shape books/next-id/published/invites
- flag is [=ship name=@tas] with slugified names
- notebook-state contains notebook/members/visibility/folders/notes/history
- Migrations follow the tlon |^ kelt + linear =? chain pattern
- Scry endpoints now return typed marks (notes-notebooks, notes-note,
  etc.) with both noun and json grow arms
- Action surface routes through no-action which always pokes the host
  (Gall loops self-pokes back through %notes-command); only %publish /
  %unpublish stay local
- HTTP routes lifted into ++serve-http
- Watch arms split: ++no-watch for local UI, ++se-watch for remote subs

For /v0/published, the published-record type carried html=@t but the
encoder never emitted it — the listing has only ever returned host /
flagName / noteId pairs in JSON (which is what the FE consumes).
Dropping html from the type so the noun-form mark also reflects the
documented shape; saves the wasted bandwidth on the noun-encoded peek.

Tests: 53/53 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The %publish / %unpublish action handlers used to mutate published.state
directly with no checks — any local poke could publish HTML keyed
under any flag, including subscribed remote notebooks. Tightened to:

- new ++publish-note / ++unpublish-note arms in cor (since published is
  in outer state, not notebook-state). Each arm:
    ?> =(src.bowl our.bowl)        — local-action only
    ?> =(ship.flag our.bowl)       — host-only (only the host's ship
                                     maintains its public URL)
    abed se-core to get notebook context
    ?> (se-can-edit:se src.bowl)   — edit permission
    publish also asserts the note exists
- action handler delegates: (publish-note flag nid html) /
  (unpublish-note flag nid).

New tests:
- test-publish-note-rejects-non-host (~bus tries to publish under
  ~zod's flag → crash)
- test-publish-note-rejects-missing-note (publish a non-existent note
  id → crash)

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inbox events were the last surface emitting raw json+!>(...) cards;
every other client surface now uses typed marks. Consistency fix.

- New +$ u-inbox tagged union in sur/notes.hoon — three variants:
  %invite-received, %invite-removed, %notebooks-changed.
- ++u-inbox encoder under enjs:notes-json reproduces the existing
  {type:'update', update:<payload>} wire shape so the FE's
  applyNotebookUpdate / applyInboxEvent dispatch keeps working.
- New mar/notes/inbox-update.hoon with grad %noun and grow noun+json.
- ++give-inbox-received / ++give-inbox-removed / ++notebooks-changed-card
  emit notes-inbox-update+!>(<u-inbox value>) instead of building the
  JSON inline.

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Master added several functional improvements between when this branch
forked and now. Reconciled them onto the refactored layout:

- v0.10.1 / v0.11.0 UI work (drafts in localStorage, faster autosave,
  preview shows title, list/task editing UX, checkable preview
  checkboxes, suppress join modal on already-joined notebooks): merged
  via auto-merge of index.html; lib/notes-ui.hoon regenerated via
  scripts/build-notes-ui.sh.
- leave-remote: emit %member-leave to host before unsubscribing
  (so the host's members.notebook-state reflects reality). Ported
  into the |^ kelt arm.
- /v0/notebook scry: now includes visibility. Updated notebook-detail
  type in sur and the encoder; the no-peek %notebook arm builds
  [flag notebook visibility].
- +agent: handle [%notes %leave ship name ~] poke-ack as a no-op
  (best-effort).
- +arvo: handle [%behn %wake] for [%notes %rewatch ship name ~] —
  re-arms a subscription after a transient failure.
- ++no-agent watch-ack failure: instead of silently dropping the sub,
  schedule a 30s rewatch via behn timer.
- Syncer (Tauri menubar app) v0.2.0 alignment with state-10 agent.

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ioning

The previous publish-note / unpublish-note arms in cor were checking
host ownership and edit permission via se-core — too much. The action
poke layer already enforces ?> =(our.bowl src.bowl), and no-abed
already validates that the notebook exists. Beyond that, publish is
just a per-ship cache write (the local /notes/pub/<flag>/<id> URL
serves whatever this ship caches), not an authority assertion that
needs the host's edit-role gate.

Replaced with two arms in no-core:

  ++  no-publish    |=  [nid=@ud html=@t]   :: write published.state
  ++  no-unpublish  |=  nid=@ud             :: del

Action handler dispatches via no-abet:(no-publish:(no-abed:no-core
flag) ...). no-abed crashes if the flag isn't in books; the action poke
layer crashes on non-self src. That's the full check set.

Tests:
- test-publish-note-rejects-non-host renamed to *-rejects-non-self
  (the rejection is the action-poke self-check, not a ship-flag check).
- test-publish-note-rejects-missing-note replaced with
  test-publish-note-rejects-unknown-notebook (no-abed rejects unknown
  flags; we don't validate that the note-id exists within the
  notebook anymore — published is a key-value cache).

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The agent-door header has =| current-state followed by =* state -, so
the agent state value sits at the head of the subject. Wing resolution
walks into it for free, which means books, next-id, published, invites
each resolve directly without the .state suffix.

52 sites updated across the helper core: books.state → books, etc.
The local-face .s in migration arms (state-N-to-N+1) is unrelated and
stays — that's the migration input, not the agent state.

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The host's self-poke from the action handler comes back as %poke-ack
on the [%notes %sub ship name ~] wire — same wire used by genuine
subscriber agent signs. The outer ?> ?=(%sub -.net) at the top of
no-agent crashed the host because its net is %pub.

Move the sub-only check into the per-sign-type branches that actually
need it:
- %fact: route to no-response unconditionally (no-response itself
  already short-circuits when the host emits to itself, since we don't
  watch our own /updates wire).
- %kick: only re-watch if we're a sub; host doesn't watch self.
- %watch-ack: only re-arm the rewatch timer if sub.
- %poke-ack (default): always no-op.

Surfaced by an action poke crashing on sidwyn with `take %poke-ack
failed at no-agent` — host's self-poke ack was tripping the assertion.

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two FE bugs surfaced while exercising the e2e flows:

1. ++triggerAutoCreate captured the user's pending title/body once early
   (right after the create-note poke) and restored them after several
   awaits (loadNotes, selectNote). Any keystrokes typed during that
   ~150-400ms window were silently dropped on restore — symptom: typing
   a title quickly then switching to body sometimes "snapshotted" an
   empty body. Fix: capture pending values just before selectNote.

2. channelId was initialized to "" and only set inside ++openChannel,
   which runs from ++connect. Fast user clicks (or e2e tests) firing a
   poke before connect() finished produced PUT /~/channel/ (empty id)
   → 400. Fix: initialize channelId at module load with a Date.now()
   id; the channel comes into being on the first PUT, so the name is
   just a label we pick eagerly.

Bumped ++dummy to v0.11.0-refactor.

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same race shape as the channelId one — SHIP is fetched via /~/name
during ++connect, and pokes that fire before that resolves carry
ship:"" → Eyre 400.

- Expose window.__notesGetShip() returning the current SHIP value once
  /~/name has resolved.
- Test fixture (notes + openSubscriberContext) waits up to 15s for the
  probe to return a non-empty ship before yielding the page to the
  test body.

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a cross-ship-invite test fails before the sub accepts/declines,
the entry stays in the sub's invites.state and accumulates across runs.
Same for cross-ship-decline-invite if it fails before the decline.

- wipe-leftovers: scry /v0/invites for each ship; if any e2e- titled
  invite is present, navigate the page to /notes/ and call
  declineInvite() via the FE.
- tryDelete (per-test cleanup): sweep matching invites for the title
  before attempting the notebook deletion. Best-effort, errors
  swallowed.

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ergonomics for diagnosing flaky cross-ship behavior — when a sub edit
doesn't appear on the host, you usually want to know what SSE events
the FE actually received. Adds:

- handleEvent gates a `console.log("[sse]", JSON.stringify(data))` on
  localStorage.e2e-log-sse === "1". Off by default (no-op for users).
- Test fixture sets the localStorage flag on every page (host + sub),
  and pipes browser console / pageerror through to the test runner —
  but only when E2E_TRACE=1 is set, so green runs stay quiet.

Usage: `E2E_TRACE=1 npm run test:e2e -- --grep 'cross-ship edit'`

Tests: 55/55 backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arthyn arthyn force-pushed the refactor-peek-watch-via-abed branch from cdba5ff to 4a76627 Compare May 6, 2026 21:49
arthyn and others added 4 commits May 6, 2026 16:53
Playwright UI's Console tab auto-captures browser console.log output
per action — but only if the FE actually logs. Gating the localStorage
flag on E2E_TRACE=1 meant UI mode showed nothing.

Always set the flag now; E2E_TRACE=1 still gates the runner-side pipe
(attachConsole) for CLI runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleEvent only sees fact/snapshot/inbox messages — poke-ack
short-circuits before it. Move the trace log up to eventSource.onmessage
so every raw SSE payload (including poke-acks) is captured. Both layers
now log when localStorage.e2e-log-sse === "1".

Bumped ++dummy to force a reload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
forceSave's "Saved" indicator only means the FE's local poke-ack came
back — for cross-ship subscribers, the host's se-update and the fact's
trip back through the stream subscription happen AFTER that. Without a
settle, cross-ship-edit asserts on the host's editor before the update
event has been delivered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Host was already in the notebook from the create+invite steps and is
subscribed to its /stream — the sub's create-note should propagate via
the existing subscription. The page.goto + selectNotebook re-navigation
masked any real propagation bug in the original test.

If this fails now, the failure is the actual stream-propagation bug we
want to surface, not a workaround.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arthyn added a commit to tloncorp/hoon-reference that referenced this pull request May 7, 2026
Two more rules from arthyn/notes#4 follow-up commits (68e0e1a, b6b8d48).

architecture.md
- New "Always poke the host" subsection in ACUR: the action handler
  emits a poke to ship.flag unconditionally; Gall loops self-pokes
  back through +poke %notes-command. Removes the host/sub branch
  from the action layer entirely. c-command processing becomes the
  single dispatch point for all state-changing logic.
- New "State fields resolve without .state" callout under standard
  boilerplate: =| / =* state - puts state at subject head, so wing
  search finds books, invites, etc. directly. Prefer the unprefixed
  form except where local shadowing forces it.

patterns.md
- Updated existing examples (Tuple Types with *, Per-Entity Engines)
  to use books / cor(books ...) instead of books.state / cor(books.state
  ...) for consistency with the new rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marks the refactor cycle on PR #4: tlon-aligned dispatch (action →
no-action → command → host), typed marks for every scry endpoint,
linear migrations, ?- / ?+ dispatch over chained ?:, =* aliasing,
=type face shortcut, no-publish/no-unpublish through the engine,
inbox-update mark, and a handful of e2e harness fixes (drain self
pokes, settle after forceSave, eager channelId, wait for SHIP).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arthyn arthyn merged commit 47e6476 into master May 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant