Skip to content

Add dev pairing and seed utilities#4557

Open
t3dotgg wants to merge 17 commits into
agent/dev-server-sharingfrom
agent/dev-utilities
Open

Add dev pairing and seed utilities#4557
t3dotgg wants to merge 17 commits into
agent/dev-server-sharingfrom
agent/dev-utilities

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 26, 2026

Copy link
Copy Markdown
Member

What

  • Add dev:pair to find a live dev server’s recorded state and issue a fresh pairing URL with the correct web origin.
  • Add dev:seed to copy recent projection data into an isolated dev database while refusing to write to the shared home.
  • Record the dev URL in runtime state and ignore stale server records.
  • Share the projection-table list with the mobile showcase tooling and document the workflows.

Why

Isolated worktree databases are intentionally empty, and replacing a consumed pairing URL previously required reconstructing internal ports, state paths, and origins by hand. These are useful developer conveniences, but they are not required for the hosting fix itself.

Impact

This is PR 3 of 3 and is stacked on #4556. Keeping these optional utilities last lets the core isolation and sharing changes land without the larger data-copy implementation.

Verification

  • vp test run scripts/lib/dev-seed.test.ts apps/server/src/cli/auth.test.ts apps/server/src/serverRuntimeState.test.ts (24 tests)
  • targeted lint
  • server and scripts typechecks

Note

Add dev:pair and dev:seed CLI utilities for dev environment setup

  • Adds bun run dev:pair (t3 auth pairing url) to print a ready-to-open pairing URL for a running dev server, auto-detecting the live server state from dev/ or userdata/ runtime files.
  • Adds bun run dev:seed (scripts/dev-seed.ts) to copy recent threads and projects from a source T3 data directory into a target dev database, with configurable thread/activity limits, schema-drift tolerance, and attachment file copying.
  • The seeder normalizes runtime-dependent state (interrupts running turns, clears streaming flags, resets projector cursors to 0) and refuses to run if a server is actively using the target database.
  • Adds isPersistedServerRuntimeStateLive to distinguish live from stale server runtime state files, and extends persisted state to include an optional devUrl field used for dev server preference.
  • Risk: seedDevDatabase is destructive — it clears all projection tables in the target before copying; a concurrent server check just before commit reduces but does not eliminate the race window.

Macroscope summarized cb1da9c.


Note

Medium Risk
dev:seed destructively replaces target projections and clears orchestration history; guards reduce but do not eliminate races if a server starts during the copy. Pairing URL discovery touches auth credential issuance against the correct dev database.

Overview
Adds bun run dev:pair (auth pairing url) so a running dev server can mint a fresh pairing link from server-runtime.json without manually matching port, dev vs userdata, or web origin. The command probes both state dirs, ignores stale PIDs, prefers a live instance with devUrl, defaults to the worktree .t3 when run from a worktree, and issues credentials against the same state directory the server actually uses.

Runtime state now optionally records devUrl, requires a positive PID, and adds isPersistedServerRuntimeStateLive so tooling does not trust crash-leftover files. deriveServerPaths accepts an explicit stateDir for callers that already located the live server.

Adds bun run dev:seed to copy recent projection data from ~/.t3 (or --from) into an isolated worktree database: caps threads/activities, tolerates schema drift, copies attachment files, clears the target event log and resets projector cursors, and normalizes agent-dependent state (stopped sessions, interrupted turns, no streaming flags). It refuses the shared home, same source/target, a missing DB, or seeding while a server holds the target open (with a pre-commit re-check).

Docs and agent skills now describe these workflows; projection-tables.ts is shared with mobile showcase seeding. Covered by new tests for auth CLI discovery, runtime liveness, server probes, and the seeder.

Reviewed by Cursor Bugbot for commit cb1da9c. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added commands to share local development servers, generate pairing URLs, and seed isolated development data.
    • Development servers now support worktree-specific ports and improved browser proxying.
    • Shared development environments can use HTTPS tailnet URLs with configurable allowed origins.
    • Session cookies are isolated by server port, preventing conflicts between concurrent instances.
    • Tailscale errors now include concise diagnostic details.
  • Documentation

    • Expanded development setup guidance, browser workflow instructions, pairing recovery, port handling, and database seeding safety notes.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and matches the main addition of dev pairing and seeding utilities.
Description check ✅ Passed It covers what changed, why, and verification, though the template headings and checklist are not followed exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/dev-utilities

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 26, 2026
@t3dotgg
t3dotgg marked this pull request as ready for review July 26, 2026 05:21
Comment thread scripts/dev-seed.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces substantial new dev tooling capabilities (~2000+ lines) including a new CLI command for pairing URLs and a database seeding script with destructive operations. While well-tested and dev-focused, the scope of new functionality warrants human review.

No code changes detected at cb1da9c. Prior analysis still applies.

You can customize Macroscope's approvability policy. Learn more.

@t3dotgg

t3dotgg commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Picked this up, reviewed it, and pushed fixes in 331fc71.

Review found five ways a seeded database could describe something the copy cannot back up. All are in the dev:seed path; dev:pair reviewed clean.

dev:seed ran against a live server. The docs said "stop the server first" but nothing enforced it. SQLite locking isn't sufficient on its own — the seed's transaction can take the write lock between the running server's own writes and commit cleanly. What it can't do is tell that server the projections it holds in memory, and the event log it appends to, were both replaced underneath it. Now refused via a pid probe over both state directories, with stale files from a SIGKILLed server ignored so seeding can't get permanently blocked.

Turns copied mid-flight stayed running. The status: "stopped" session override didn't cover this: the turn is read independently, and deriveUnsettledTurnId keeps the thread unsettled and its timeline unfoldable as long as the turn is running — with no agent left to finish it. Now settled to interrupted, matching settledTurnStateForSessionStatus for the stopped status the sessions are copied with. Streaming messages are finished for the same reason.

Attachment metadata was copied without the files. attachments_json came along but the bytes live on disk beside the database, so a seeded thread rendered an image whose request 404s. The seeder now reports referenced ids and the CLI copies the matching files. Matched by directory listing rather than reconstructing the extension, since the server infers that at upload time.

Orphaned provider bindings survived. provider_session_runtime was neither copied nor cleared, so rows naming just-deleted threads outlived them. ProviderSessionReaper sweeps every non-stopped binding, finds no thread behind it, so never reaches its active-turn guard — it just tries to stop a session for a thread that doesn't exist, against a provider instance the worktree may not define.

--threads had no upper bound. Each selected thread is one bound parameter in the copy's IN (...); past SQLite's 32766 ceiling the seed dies with "too many SQL variables" after the target has been emptied.

Also corrected a comment: it claimed sequence restarts at 1 after the log is emptied. orchestration_events.sequence is AUTOINCREMENT, so DELETE doesn't reset the high-water mark. Zero is still the right cursor — it's below every future sequence either way — but for a different reason than the one written down.

Verification: 33 tests pass (9 new covering the guard, turn settling, streaming, attachment ids, and binding cleanup), scripts typecheck and lint clean.

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Jul 26, 2026
@t3dotgg

t3dotgg commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Second round pushed in 73669b3, covering the Macroscope comment and findings from a follow-up review of the pairing path.

isPersistedServerRuntimeStateLive reported non-positive pids as live. The highest-severity item this round. process.kill reads 0 and -1 as signal-group targets rather than process lookups — 0 signals the caller's own group, -1 every permitted process — so neither throws, and both landed in the "didn't throw, so it's running" branch. Verified directly. A truncated or hand-edited state file recording either would be believed indefinitely, and since a dev candidate wins the devUrl preference it also outranks a genuinely live server elsewhere, minting tokens into a database nothing reads. Rejected in the schema and re-checked at the call site. The errno cast is now a guard, since an out-of-range pid throws a TypeError with no code. Regression tests confirmed failing without the fix.

pairing url --json emitted warnings on stdout ahead of the payload. The probe reads both state directories, so a corrupt file in the one not in use broke JSON for anything piping it — the quiet-logs layer was only applied around the issuing step that runs afterward. Reproduced with a crafted fixture; stdout is now valid JSON on the success path.

Turns copied in the pending state were left unsettled — same defect class as the running case from the first round, one state short. That's a user message accepted before its provider turn started, which only that thread's next message would ever clear.

The source was read across a dozen implicit transactions while the installed app may be writing to it, so a project could vanish between the query that selected it and the copy that read its row — leaving an orphaned thread, with no foreign keys to catch it. Now one deferred read transaction pinning every query to a single snapshot.

--from and --to resolving to the same directory is refused. Details in the inline reply — the wipe described in the review comment doesn't reproduce (the source reads a pre-transaction snapshot, so the rows come back; verified in both journal modes), but the event log, cursors, and anything outside the caps genuinely don't, so it's guarded on that basis.

Verification: 38 tests pass, full-repo typecheck reports 0 errors, lint clean on changed files.

Comment thread scripts/lib/dev-seed.ts Outdated
Comment thread scripts/lib/dev-seed.ts
Comment thread scripts/lib/dev-seed.ts Outdated
Comment thread scripts/dev-seed.ts Outdated
Comment thread scripts/lib/dev-seed.ts
Comment thread scripts/lib/dev-seed.ts
Comment thread scripts/dev-seed.ts Outdated
Comment thread scripts/dev-seed.ts Outdated
@t3dotgg
t3dotgg force-pushed the agent/dev-server-sharing branch from edc8e4f to dbad3bc Compare July 26, 2026 06:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
scripts/mobile-showcase-environment.ts (1)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Finish the dedup: PROJECTION_TABLES_IN_DEPENDENCY_ORDER is also exported now.

seedDatabase still inlines the same nine-table delete list that the new module exports, so the two can drift the moment a projection table is added.

♻️ Import the shared list too
-import { PROJECTOR_NAMES } from "./lib/projection-tables.ts";
+import {
+  PROJECTION_TABLES_IN_DEPENDENCY_ORDER,
+  PROJECTOR_NAMES,
+} from "./lib/projection-tables.ts";

Then in seedDatabase:

for (const table of PROJECTION_TABLES_IN_DEPENDENCY_ORDER) {
  database.exec(`DELETE FROM ${table}`);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/mobile-showcase-environment.ts` at line 2, Update the imports in
scripts/mobile-showcase-environment.ts to include
PROJECTION_TABLES_IN_DEPENDENCY_ORDER, then replace seedDatabase’s inline
projection-table delete list with iteration over that shared exported list while
preserving the existing DELETE execution.
scripts/dev-seed.ts (1)

266-276: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

catch: (cause) => cause as DevSeedError is an unchecked cast.

seedDevDatabase wraps its own failures today, so this holds in practice, but any future throw that isn't a DevSeedError (or isn't an Error at all) reaches the tapError at Line 312-315 and dereferences .message on it. Normalizing here keeps the error channel type honest.

♻️ Normalize unexpected causes
-        catch: (cause) => cause as DevSeedError,
+        catch: (cause) =>
+          cause instanceof DevSeedError
+            ? cause
+            : new DevSeedError(`could not seed the dev database: ${String(cause)}`),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/dev-seed.ts` around lines 266 - 276, Replace the unchecked cast in
the Effect.try catch handler around seedDevDatabase with normalization that
preserves existing DevSeedError instances and converts unexpected thrown values
into a DevSeedError-compatible error. Ensure non-Error causes are also safely
represented so the downstream tapError handling can access message without
throwing.
scripts/lib/dev-seed.test.ts (1)

545-572: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider marking the 32k-thread fixture as a slow test.

Building 32,766 thread and turn rows on every run of this file is the one materially slow case here. A it.skipIf/tag or a longer explicit timeout keeps the focused-test workflow snappy without losing the ceiling guarantee in CI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/dev-seed.test.ts` around lines 545 - 572, Mark the
maximum-thread-limit test identified by its “seeds at the maximum thread limit
without exhausting SQL variables” description as slow, using the project’s
existing skip/tag mechanism or an explicit extended timeout. Preserve its CI
execution and the existing ceiling assertions while keeping focused local test
runs fast.
packages/tailscale/src/tailscale.ts (1)

219-228: 📐 Maintainability & Code Quality | 🔵 Trivial

Minor: stderrPreviewOf(stderr) computed twice per call site.

Both readTailscaleStatus and runTailscaleCommand call stderrPreviewOf(stderr) once in the ternary condition and again in the object body. Hoisting into a local avoids the redundant (if cheap) call and reads slightly cleaner.

♻️ Proposed dedup
-        ...(stderrPreviewOf(stderr) !== undefined
-          ? { stderrPreview: stderrPreviewOf(stderr) }
-          : {}),
+        ...(preview !== undefined ? { stderrPreview: preview } : {}),

(with const preview = stderrPreviewOf(stderr); added above each call site)

Also applies to: 286-295

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tailscale/src/tailscale.ts` around lines 219 - 228, Deduplicate
stderrPreviewOf(stderr) in both readTailscaleStatus and runTailscaleCommand by
assigning its result to a local preview variable before constructing
TailscaleCommandExitError, then reuse that variable for the conditional property
and stderrPreview value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.agents/skills/test-t3-app/SKILL.md:
- Around line 51-58: Update the later troubleshooting guidance that recommends
manually minting replacement tokens to direct agents to run the existing `bun
run dev:pair` workflow instead, preserving the documented `--base-dir` usage
when applicable and removing the obsolete manual-token recovery instructions.

In `@docs/reference/scripts.md`:
- Around line 54-55: Update the runtime-state documentation to describe probing
both dev and userdata for the directory containing the active database,
resolving dev/userdata ties by modification time. In docs/reference/scripts.md
at lines 54-55, replace the userdata-first wording; in
.agents/skills/test-t3-app/SKILL.md at line 58, explain that pairing checks both
directories and applies the same active-state selection rule.
- Around line 4-6: Standardize the documented development workflow on the
repository’s bun wrappers: in docs/reference/scripts.md lines 4-6, replace the
vp run dev, vp run dev:share, vp run dev:pair, and vp run dev:seed examples with
their bun run equivalents while preserving the existing options and
descriptions; in .agents/skills/test-t3-app/SKILL.md line 16, replace vp run dev
with bun run dev and keep the sharing variant as bun run dev:share.

---

Nitpick comments:
In `@packages/tailscale/src/tailscale.ts`:
- Around line 219-228: Deduplicate stderrPreviewOf(stderr) in both
readTailscaleStatus and runTailscaleCommand by assigning its result to a local
preview variable before constructing TailscaleCommandExitError, then reuse that
variable for the conditional property and stderrPreview value.

In `@scripts/dev-seed.ts`:
- Around line 266-276: Replace the unchecked cast in the Effect.try catch
handler around seedDevDatabase with normalization that preserves existing
DevSeedError instances and converts unexpected thrown values into a
DevSeedError-compatible error. Ensure non-Error causes are also safely
represented so the downstream tapError handling can access message without
throwing.

In `@scripts/lib/dev-seed.test.ts`:
- Around line 545-572: Mark the maximum-thread-limit test identified by its
“seeds at the maximum thread limit without exhausting SQL variables” description
as slow, using the project’s existing skip/tag mechanism or an explicit extended
timeout. Preserve its CI execution and the existing ceiling assertions while
keeping focused local test runs fast.

In `@scripts/mobile-showcase-environment.ts`:
- Line 2: Update the imports in scripts/mobile-showcase-environment.ts to
include PROJECTION_TABLES_IN_DEPENDENCY_ORDER, then replace seedDatabase’s
inline projection-table delete list with iteration over that shared exported
list while preserving the existing DELETE execution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1139449d-5adc-49f6-a961-eb622b3e4674

📥 Commits

Reviewing files that changed from the base of the PR and between dbad3bc and 3fa6578.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (34)
  • .agents/skills/test-t3-app/SKILL.md
  • .agents/skills/test-t3-app/references/sqlite-fixtures.md
  • AGENTS.md
  • apps/server/src/auth/EnvironmentAuth.test.ts
  • apps/server/src/auth/EnvironmentAuth.ts
  • apps/server/src/auth/EnvironmentAuthPolicy.test.ts
  • apps/server/src/auth/EnvironmentAuthPolicy.ts
  • apps/server/src/auth/PairingGrantStore.ts
  • apps/server/src/auth/SessionStore.ts
  • apps/server/src/auth/utils.ts
  • apps/server/src/cli/auth.test.ts
  • apps/server/src/cli/auth.ts
  • apps/server/src/config.ts
  • apps/server/src/http.ts
  • apps/server/src/serverRuntimeState.test.ts
  • apps/server/src/serverRuntimeState.ts
  • apps/web/vite.config.ts
  • docs/reference/scripts.md
  • package.json
  • packages/shared/package.json
  • packages/shared/src/devProxy.ts
  • packages/tailscale/src/tailscale.ts
  • scripts/dev-runner.test.ts
  • scripts/dev-runner.ts
  • scripts/dev-seed.ts
  • scripts/lib/dev-seed.test.ts
  • scripts/lib/dev-seed.ts
  • scripts/lib/dev-share.test.ts
  • scripts/lib/dev-share.ts
  • scripts/lib/projection-tables.ts
  • scripts/lib/server-runtime-probe.test.ts
  • scripts/lib/server-runtime-probe.ts
  • scripts/mobile-showcase-environment.ts
  • scripts/package.json

Comment on lines +51 to +58
Ask the running server for a fresh pairing URL:

```bash
T3CODE_PORT=<server-port> node apps/server/src/bin.ts auth pairing create \
--base-dir <base-dir> \
--dev-url <web-url> \
--base-url <web-url> \
--ttl 15m \
--label agent-ui-test
bun run dev:pair
bun run dev:pair -- --base-dir <base-dir>
```

Use the `Pair URL` from this command once. Derive `<server-port>` and `<web-url>` from the current dev-runner output, including any automatically selected port offset. Setting `T3CODE_PORT` keeps the administrative CLI from probing for an unrelated free port.

Always pass `--dev-url` for a dev-runner environment so the generated pairing URL uses the current web origin. An explicit base directory stores runtime state in `<base-dir>/userdata`; the `<base-dir>/dev` fallback is only used by an implicit dev home. Use `auth pairing list` to inspect active token metadata; it intentionally cannot reveal token secrets.
It finds the live server state under either the `dev` or `userdata` state directory and reuses the recorded web origin, including a tailnet URL. Pass `--base-dir` only when the server was started with `--home-dir`. Use the printed URL once.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the obsolete manual-token recovery path.

The new dev:pair workflow conflicts with the later troubleshooting guidance at Lines 86-88, which still tells agents to mint replacement tokens manually. Update that later guidance to call bun run dev:pair instead.

🧰 Tools
🪛 SkillSpector (2.3.11)

[warning] 36: [RA2] Session Persistence: Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Remediation: Remove any persistence mechanisms (cron jobs, startup scripts, state files). Skills should not maintain state across sessions without explicit user consent.

(Rogue Agent (RA2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/test-t3-app/SKILL.md around lines 51 - 58, Update the later
troubleshooting guidance that recommends manually minting replacement tokens to
direct agents to run the existing `bun run dev:pair` workflow instead,
preserving the documented `--base-dir` usage when applicable and removing the
obsolete manual-token recovery instructions.

Comment thread docs/reference/scripts.md Outdated
Comment on lines +4 to +6
- `vp run dev --share` (or `vp run dev:share`) — Also publishes the web port over HTTPS on this machine's tailnet. The startup pairing URL is built against the shared origin, and the mapping is removed on exit.
- `vp run dev:pair` — Prints a fresh pairing URL for the running dev server, using its recorded state directory, port, and web origin. Add `--base-dir <path>` only when the server was started with `--home-dir`.
- `vp run dev:seed` — Copies recent projects and threads from the shared home into the isolated dev database. Stop the server before running it, then restart. Tune the copy with `--threads` and `--activities`; it refuses to write to the shared home.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use one supported command wrapper in operational documentation.

  • docs/reference/scripts.md#L4-L6: standardize the new workflow examples on the repository's bun run wrappers.
  • .agents/skills/test-t3-app/SKILL.md#L16-L16: replace vp run dev with bun run dev and keep the sharing variant aligned.

As per coding guidelines, start the web stack with bun run dev; use bun run dev:share when access from another tailnet device is required.

📍 Affects 2 files
  • docs/reference/scripts.md#L4-L6 (this comment)
  • .agents/skills/test-t3-app/SKILL.md#L16-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/scripts.md` around lines 4 - 6, Standardize the documented
development workflow on the repository’s bun wrappers: in
docs/reference/scripts.md lines 4-6, replace the vp run dev, vp run dev:share,
vp run dev:pair, and vp run dev:seed examples with their bun run equivalents
while preserving the existing options and descriptions; in
.agents/skills/test-t3-app/SKILL.md line 16, replace vp run dev with bun run dev
and keep the sharing variant as bun run dev:share.

Source: Coding guidelines

Comment thread docs/reference/scripts.md Outdated
Comment on lines +54 to +55
Both source and target follow whichever state directory actually holds a database — `userdata` when present, otherwise `dev` — so a main-checkout `bun run dev`, whose state lives in `~/.t3/dev`, is seedable without naming the directory.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document active state-directory selection consistently.

  • docs/reference/scripts.md#L54-L55: replace the userdata-first wording with the active-database probe and dev/userdata tie-resolution rule.
  • .agents/skills/test-t3-app/SKILL.md#L58-L58: explain that pairing checks both directories and follows the same active-state rule.

The PR objectives state that runtime-state selection follows the active database directory and uses modification time to resolve dev/userdata ties.

📍 Affects 2 files
  • docs/reference/scripts.md#L54-L55 (this comment)
  • .agents/skills/test-t3-app/SKILL.md#L58-L58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/scripts.md` around lines 54 - 55, Update the runtime-state
documentation to describe probing both dev and userdata for the directory
containing the active database, resolving dev/userdata ties by modification
time. In docs/reference/scripts.md at lines 54-55, replace the userdata-first
wording; in .agents/skills/test-t3-app/SKILL.md at line 58, explain that pairing
checks both directories and applies the same active-state selection rule.

t3dotgg and others added 10 commits July 26, 2026 01:01
Review of the seeding path found five ways a seeded database could
describe something the copy cannot back up:

- Nothing stopped `dev:seed` from running against a live server. SQLite
  locking lets the seed take the write lock between the server's own
  writes and commit cleanly; what it cannot do is tell that server its
  projections and event log were replaced underneath it. Refuse instead,
  via a pid probe over both state directories.
- A thread copied mid-turn kept `state = 'running'`. The session override
  did not cover it — the turn is read independently, and an unsettled one
  leaves the thread spinning and its timeline unfoldable forever. Settle
  those to "interrupted", matching what the server does when a session
  leaves "running". Streaming messages are finished for the same reason.
- Attachment metadata was copied without the files behind it, so a seeded
  thread rendered an image whose request 404s. Report the referenced ids
  and copy the matching files alongside the rows.
- Orphaned `provider_session_runtime` rows survived the seed naming
  threads it had just deleted, which the session reaper then tried to
  stop against a provider instance the worktree may not define.
- `--threads` had no upper bound, so a large enough value died with "too
  many SQL variables" after the target was already emptied.

Also corrects the projector-cursor comment: `orchestration_events.sequence`
is AUTOINCREMENT, so emptying the log does not reset its high-water mark
and sequence does not restart at 1. Zero remains the right cursor, for the
reason now stated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up review of the pairing path and a second pass over seeding:

- `isPersistedServerRuntimeStateLive` reported non-positive pids as live.
  `process.kill` reads 0 and -1 as signal-group targets rather than
  process lookups, so neither throws and both were caught by the "it
  didn't throw, so it's running" branch. A truncated or hand-edited file
  recording either would be believed forever — the dev candidate wins the
  devUrl preference, so it also outranks a genuinely live server
  elsewhere and mints tokens into a database nothing reads. Rejected in
  the schema and re-checked at the call site; the errno cast is now a
  guard, since an out-of-range pid throws a TypeError with no `code`.
- `pairing url --json` emitted warnings on stdout ahead of the payload.
  The probe reads both state directories, so a corrupt file in the one
  *not* in use broke JSON for any consumer piping it; the quiet-logs
  layer was only applied around the issuing step that runs after. Verified
  with a crafted fixture: stdout is now valid JSON.
- Turns copied in the `pending` state were left unsettled. Same defect as
  the `running` case fixed previously — a user message accepted before its
  provider turn started, which only that thread's next message would ever
  clear.
- The source was read across a dozen implicit transactions while the
  installed app may be writing to it, so a project could vanish between
  the query that selected it and the copy that read its row. One deferred
  read transaction pins every query to a single snapshot.
- `--from` and `--to` resolving to the same directory is now refused. Not
  for the data loss it appears to be (the source reads a pre-transaction
  snapshot, so the rows come back — verified in both journal modes), but
  because the event log, the cursors, and everything outside the caps do
  not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adding "pending" to the settled states bound them as parameters, costing
two bindings on top of one per thread. `--threads` is capped at 32766 —
SQLite's ceiling — so the documented maximum landed exactly two over and
threw "too many SQL variables", after the target had already been
emptied. Inline the literals; they are internal constants, never input.

The regression test seeds at the cap for real rather than asserting the
statement text, since the property is that no code path adds a binding.
Fixture rows are batched into one transaction and the scale case builds
thread rows only, keeping it near a second.

Caught by Macroscope review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cap was declared in the CLI while the constraint belongs to the
seeder's statements, and the number appeared in three places. That split
is the mechanism behind the last regression: a change to a statement in
the library silently invalidated a bound stated in the CLI, and the
mismatch only surfaced at the maximum — after the target was emptied.

Move it to `MAX_SEED_THREAD_LIMIT` beside the statements that have to
satisfy it, have the flag derive its bound from there, and check it in
`seedDevDatabase` before anything is opened so a caller bypassing the
flag fails while the target is still intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three findings from Macroscope review:

- Settling `pending` rows to `interrupted` was the wrong verb. The server
  deletes them (`clearPendingProjectionTurnsByThread`) once the turn
  starts or the thread stops, and never settles one in place. They also
  cannot be settled coherently: `turn_id` is NULL, so an "interrupted"
  pending row is a terminal turn with no id, a shape no real turn has.
  Now deleted, matching the server's predicate exactly so a checkpoint
  row is never caught by it.
- `COALESCE(completed_at, requested_at)` preserved a stale completion on
  a running turn. One can carry a completion from an earlier settle that
  a later checkpoint reverted to "running" — the `...existingTurn` spread
  in ProjectionPipeline keeps the old value — and dating the interruption
  to that checkpoint gives a wrong duration. Overwritten unconditionally.
- The CLI hard-coded `<base>/userdata/state.sqlite` while a dev server
  under an implicit dev home stores state in `<base>/dev`. A main-checkout
  `bun run dev` was therefore unseedable, and said so by claiming no
  database existed and suggesting the user start the server that was
  already running. Source and target now follow whichever directory holds
  a database, `userdata` breaking the tie; attachments follow the same
  directory. Verified both a dev-only target and the precedence order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review of the settle path, which has been the source of every
regression in this branch. The server filters `turnId !== null &&
state === "running"` before settling; this filtered only on state.

No row is both null-id and running today — the sole null-id insert
writes 'pending' — so this changes no behavior. It is the invariant the
UPDATE already depends on, written down where it is relied upon, and it
keeps the two predicates directly comparable if a null-id running row
ever becomes possible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two more from Macroscope review:

- `ORDER BY COALESCE(...)` throws on a source down to one recency
  column. SQLite requires at least two arguments, so the pre-017 schema
  the column filter exists to support was exactly the case that aborted.
  Emit the column directly when only one survives the filter.
- `seedDevDatabase` accepted non-positive limits. Both are bound into
  `LIMIT ?`, where SQLite reads a negative as "no limit" — a caller
  bypassing the CLI flags silently got a full-table copy instead of the
  bounded one the options contract promises. Rejected before the target
  is touched, alongside the existing upper-bound check.

Both regression tests confirmed failing without their fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preferring `userdata` whenever both directories exist was wrong for the
case that motivated resolving at all: an implicit `bun run dev` stores
state in `dev`, so a base directory that had also been used with an
explicit `--home-dir` would seed the database the running server does
not open — looking like the seed did nothing.

Neither directory is categorically right, so the tie now goes to
whichever was written most recently: the one the last server actually
used. `userdata` still wins an equal or unavailable mtime. The chosen
path is already printed, so a wrong guess is visible rather than silent.

Verified both directions against a target holding both databases.

Caught by Cursor Bugbot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rebasing onto the updated #4556 turned up two doc drifts: the base moved
the script list from `vp run` to `bun run`, and the state-directory
paragraph still described the fixed `userdata` preference that the mtime
tiebreak replaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@t3dotgg
t3dotgg force-pushed the agent/dev-utilities branch from 3fa6578 to 97401b1 Compare July 26, 2026 08:02
Comment thread scripts/dev-seed.ts
CodeRabbit flagged docstring coverage. Added docs where a caller would
actually need them — that `seedDevDatabase` is destructive and not
incremental, that it validates before opening anything so a rejected run
leaves the target intact, that the caller still owns the attachment
files, and that `DevSeedError.hint` is the actionable half the CLI prints
separately.

Left the self-describing internals (`placeholders`, `hasTable`,
`columnsOf`, `stateDbPath`) alone: the surrounding scripts/ code documents
about 2% of its declarations, so padding to an 80% tool default would
read as noise rather than convention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

On the docstring coverage warning: added docs to the exported surface in 33fcf80 — the parts a caller genuinely needs to know. Specifically that seedDevDatabase is destructive and not incremental, that it validates before opening anything so a rejected run leaves the target untouched, that the caller still owns copying the attachment files, and that DevSeedError.hint is the actionable half the CLI prints on its own line.

I stopped short of the 80% threshold deliberately. The remaining undocumented declarations are things like placeholders, hasTable, columnsOf, and stateDbPath — one-line helpers whose names and signatures say everything a comment would. For context on convention: the rest of scripts/ documents roughly 2% of its top-level declarations, and these files were already at ~47% before this commit. Padding those four to satisfy a generic threshold would add noise without adding information, and would read as inconsistent with the surrounding code.

Happy to revisit if the 80% target is a deliberate repo standard rather than a CodeRabbit default — that's a maintainer call, not one I want to make unilaterally on someone else's codebase.

t3dotgg and others added 6 commits July 26, 2026 01:09
Two findings from Macroscope review:

- The running-server guard was check-then-act. A copy takes seconds, and
  nothing stopped a server starting after the check returned None — the
  seed would then replace the projections and event log underneath it,
  which is exactly what the guard exists to prevent. `seedDevDatabase`
  now takes an optional probe and re-runs it immediately before COMMIT,
  where a rollback still costs nothing. This narrows the window to the
  commit itself rather than closing it: a filesystem read and a SQLite
  transaction share no lock, so a server starting in that last instant is
  still possible. Documented as such rather than claimed as airtight.
- The state-dir tiebreak read `state.sqlite` mtime, but the server runs
  SQLite in WAL mode, where writes land in `state.sqlite-wal` and the
  main file can stay untouched until a checkpoint. Verified: 50 writes
  left the main mtime unchanged while the sidecar advanced. So a busy
  directory lost the tie to a dormant one, and the seed overwrote the
  wrong database. Recency is now the newer of the two files.

Both verified end-to-end: a backdated main file with a fresh -wal now
wins the tie, and a probe returning a pid mid-copy leaves the target's
rows and event log intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sync probe added for the pre-commit re-check duplicates the Effect
one's rules by hand — positive pid, live process, unparseable means no.
Two implementations of one rule drift apart silently, and this branch has
already produced several bugs of exactly that shape.

Asserts every case against both probes rather than trusting them to
agree: live in either state dir, dead pid, pid 0 and -1, malformed JSON,
and JSON with no pid. Confirmed the parity test fails when the sync
probe's positive-pid guard is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test schema omitted `projection_turns.checkpoint_turn_count`, which
is the column `dropPendingTurnStarts` keys its "spare checkpoint rows"
guard on. Every existing test therefore ran the degraded path where the
clause is dropped for schema drift, so the guard's real behavior was
never exercised.

Added the column (migration 005 has it) and a case covering what the
clause is for: a pending row carrying a checkpoint survives the delete.
Confirmed the case fails when the clause is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Auditing the fixture against the real migrations (the method that found
the missing checkpoint column) turned up a second gap: the test schema
omitted `projection_thread_activities.sequence`, and the copy's per-thread
cap ordered on `created_at` alone.

The app reads these back ordered by `sequence` first, then `created_at`
(ProjectionSnapshotQuery). A burst of tool activity lands many rows on
one timestamp, so capping on the timestamp alone cuts arbitrarily among
them — demonstrated with four tied rows where "keep the newest two" kept
the two the timeline treats as oldest.

Ordered to match, falling back to the timestamp when the source predates
migration 008. Fixture gained the column, and the regression test fails
against the old ordering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completing the fixture audit: `orchestration_command_receipts` is one of
the three tables the seed clears, but the fixture never created it, so
that delete only ever ran its table-missing skip path.

Added the table (migration 002) with a receipt row, and extended the
event-log test to assert receipts go too — a receipt left behind records
a command whose events the seed deleted, so re-issuing that command would
be treated as already applied and produce nothing. Confirmed the
assertion fails when the delete is removed.

The audit is now complete: every table the seed writes or clears exists
in the fixture, and every column it keys on is present. The remaining
schema gaps are columns copied generically by column intersection, where
no seeder behavior depends on them.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cb1da9c. Configure here.

Comment thread scripts/lib/dev-seed.ts
orderBy: hasColumn(source, "projection_thread_activities", "sequence")
? `"created_at" DESC, "sequence"`
: `"created_at"`,
limit: options.activityLimit,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Activity cap wrong sort keys

Medium Severity

The activity cap for projection_thread_activities sorts by created_at then sequence, but the app timeline ranks activities by sequence then created_at. This mismatch can cause seeded threads to display older activities and drop newer, high-sequence ones, making the timeline inconsistent with the source.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cb1da9c. Configure here.

Comment thread scripts/dev-seed.ts
return yield* new DevSeedTargetError({
reason: "server-running",
detail: String(runningPid.value),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seed guard scans wrong directory

Low Severity

dev:seed resolves a specific target state directory for state.sqlite, but findRunningServerPid treats a live server in either dev or userdata under the same base as blocking. A process using only the other database can incorrectly refuse the seed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cb1da9c. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant