Skip to content

fix(transport-matrix): typing is the room aggregate of in-flight turns - #108

Merged
orivibes merged 2 commits into
mainfrom
fix/typing-lifecycle
Oct 5, 2026
Merged

orivibes merged 2 commits into
mainfrom
fix/typing-lifecycle

Conversation

@zooidbot

@zooidbot zooidbot commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #36

Typing and presence in @zooid/transport-matrix are now the room aggregate of an agent's in-flight turns (ZOD091). One turn ending no longer clears another's indicator, a turn waiting on a human shows as not typing, and no refresh can outlive its turn. The Matrix typing lease is the backstop if the daemon dies mid-turn.

Client half (test-only): zooid-ai/clients#53

What changed

  • typing-registry.ts (new): per-room registry of in-flight turns; typing is on while any turn is active and not waiting on a human.
  • transport.ts: routes typing through the registry instead of per-turn set/clear.
  • .changeset/typing-lifecycle.md: patch for @zooid/transport-matrix.
  • Tests: typing-registry.test.ts, transport.typing.test.ts, and packages/cli/tests/typing-lease.integration.test.ts (real Tuwunel lease expiry).

Ori smoke step (lease test, needs Docker)

gh pr checkout 108 -R zooid-ai/zooid && pnpm install --frozen-lockfile && pnpm build && pnpm -C packages/cli exec vitest run tests/typing-lease.integration.test.ts

It should pass, and it prints the clear time ([typing-lease] measured ...), which should be ≤45s. A failure saying "Tuwunel never expired the typing lease" is a #36 finding, not a flake.

This isn't in CI: zooidbot's token lacks the workflow scope, so the job couldn't be pushed. The job I wrote is below, ready to paste into ci.yml once the scope exists.

typing-lease job for .github/workflows/ci.yml
  # [[ZOD091]] The typing-lease backstop, proved against a real Tuwunel. Its own
  # job: it needs Docker and takes ~30s. Run only this file; don't widen test:ci.
  # With CI set the test fails, not skips, when Docker is missing. The measured
  # clearedAt - raisedAt is in the log ("[typing-lease] measured ...").
  typing-lease:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - name: Install
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm build

      - name: Typing lease (real Tuwunel)
        run: pnpm -C packages/cli exec vitest run tests/typing-lease.integration.test.ts

The lease test's CI guard (fail, don't skip, when Docker is missing) stays in the file; it is inert until that job exists.

Tests

  • pnpm -C packages/transport-matrix exec vitest run — 35 files passed, 3 skipped (Docker-gated); 435 tests passed, 4 skipped.
  • pnpm -C packages/transport-matrix exec tsc --noEmit — clean.

Not verified

  • The lease test (typing-lease.integration.test.ts) needs Docker and Tuwunel, which the sandbox can't reach. It has never run green; the smoke step above is its first run.
  • context-mcp and pi-extension socket integration tests fail here with EPERM on socket binds (sandbox). Not touched by this change; reported rather than written off as pre-existing.

Knowledge impact

ZOD091.

Plan feedback

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@zooidbot zooidbot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Architect review (ZOD091 cycle 1): matches the spec and plan. No blocking findings.

I read the diff against specs/045-ZOD091-typing-lifecycle/PLAN.md and the surrounding transport.ts in the branch.

Correct:

  • Registry: it has the two sets from decision 2. working.size > 0 is the indicator, and live guards resume, so a wait that ends after its turn has ended can't re-raise the indicator or start an orphaned refresh interval. That's the worst reading of #36, and it's closed off.
  • Ordering: each slot has one promise chain, and presence has one per user (decision 5). lower clears the interval before it queues the false, so a refresh that's already in flight always lands before the lower. The "never sends the lower before an in-flight raise has settled" test proves this.
  • One release point: runTurnBody's finally → typing.exit. Failure, interrupt and harness-limit each have a wiring test.
  • Resume in the finally: the elicitation handler's finally resumes, gated on countForSession === 0 (decision 3). "A card that could not be published still resumes typing" covers the fail() path, which the old 'resolved' listener missed.
  • No turn-scoped calls left: transport.ts now calls setTyping/setPresence only for bootstrap presence (:1918), and the three ZOD078 per-turn sites are gone. The completion checklist item holds.
  • Session IDs line up: req.sessionId in the elicitation handler and sessionId in runTurnBody are the same ACP session ID (sessions.set at :1386), so pause and resume hit the same slot entry that enter added.

Non-blocking nits (fine to leave):

  • Startup race: bootstrap setPresence('online') at :1918 bypasses the per-user presence chain. If a turn starts before that call settles, the late online can overwrite unavailable until the next edge. It only happens at startup and only affects presence. Worth a follow-up only if anyone sees it.
  • Unknown-session warning: exit warns for a session it doesn't know. Nothing in production should hit that, so if it shows up in daemon logs, treat it as a signal and don't silence it.

Still open before #36 can be called fixed:

  1. The lease test hasn't run anywhere. CI is green, but it doesn't include the lease test. Ori: please run the smoke step in the body and paste the measured clear time here. If it fails with "Tuwunel never expired the typing lease", that's a second cause of #36 and it gets its own issue.
  2. Manual smoke (plan step 4.5), after a local prerelease: one agent in two threads at once, then an AskUserQuestion round trip.
  3. CI job: once zooidbot has the workflow scope, add the typing-lease job from the <details> block so the lease test runs on every PR.

Keep Refs, not Closes, until (1) and (2) are done.

Run against Tuwunel, the original test always failed: Tuwunel expires the
typing lease (a fresh sync shows it cleared) but never pushes the expiry to
an incremental /sync, so an open client keeps the stale indicator. Split
into a test of the expiry and a test pinning the upstream bug, so the suite
is green today and flags the day Tuwunel fixes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@orivibes
orivibes merged commit a064969 into main Oct 5, 2026
1 check passed
@orivibes
orivibes deleted the fix/typing-lifecycle branch October 5, 2026 22:13
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.

2 participants