Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
154 commits
Select commit Hold shift + click to select a range
a03f718
perf(web): PDF Find jumps to the first match in long PDFs 3.1x faster…
Yuruzuu Nov 12, 2026
96aa40e
fix(mobile): Keep a tall question's Submit button above Android's nav…
mwolson Nov 12, 2026
4f555c7
fix(web): choosing a project icon again re-reads it from disk (#28584)
AlexSandilands Nov 12, 2026
d9a97e7
fix(web): sidebar keeps the open thread active while Settings or Usag…
Mnigos Nov 12, 2026
9c7618b
fix(web): gitignore fences in the rich-text composer highlight as ini…
juliusmarminge Nov 12, 2026
451e375
fix(web): keep later navigation when thread deletion completes (#33953)
Edge358 Nov 12, 2026
0bc993a
fix(server): Git status finds the default branch when the only remote…
only21mil Nov 12, 2026
f255158
fix(pi): settle prompts consumed by input handlers (#28798)
Chinoman10 Nov 12, 2026
39ab6a2
fix(cursor): honor HTTP/1.1 transport for proxy networks (#25338)
only21mil Nov 12, 2026
5f43681
fix(client): hide Resume while a pending request blocks continuing th…
only21mil Nov 12, 2026
2adda57
fix(server): t3 doctor holds off server launches and names the holder…
only21mil Nov 12, 2026
cbf9e26
fix(server): preserve filename whitespace when reading and saving fil…
voltcrash Nov 12, 2026
70833b2
fix(server): tool calls a released session cuts off no longer keep th…
NotReliable Nov 12, 2026
85b1606
fix(server): keep thread tool responses small by default (#29268)
voltcrash Nov 12, 2026
6d53584
fix(web): a chunk that fails after boot no longer reloads the page in…
grodriguez-fr Nov 12, 2026
fd5cb99
fix(opencode): keep ACP question choices and fold the custom answer (…
QRpeachKZ Nov 12, 2026
b63d3c5
fix(web): sent messages honor their theme foreground (#26888)
fancyboi999 Nov 12, 2026
a3cde98
perf(ci): run server test files in parallel on each shard (#35618)
juliusmarminge Nov 12, 2026
aa58dc6
chore(deps): bump the npm_and_yarn group across 2 directories with 1 …
dependabot[bot] Nov 12, 2026
8753c74
fix(server): keep worktrees containing nested repositories (#25241)
maria-rcks Nov 12, 2026
f41e8b3
fix(shared): Windows-style paths stay out of the slash menu (#27851)
samkelly880 Nov 12, 2026
ac7c7f6
fix(acp): let agents that advertise /compact compact their own contex…
ParasN Nov 12, 2026
2f9d930
test(acp-registry): subscribe to snapshot changes before triggering t…
juliusmarminge Nov 12, 2026
fbd77b9
fix(web): diff colors follow appearance contrast (#28947)
voltcrash Nov 12, 2026
dfded9d
fix(web): keep the child thread open when its remote parent was delet…
ScottN-PV Nov 12, 2026
731291e
fix(desktop): poll for updates every 4 hours, not 4 minutes (#28481)
tao12345666333 Nov 12, 2026
9a614e3
fix(usage): bill Codex requests whose cumulative counters rewind to a…
jokaperes Nov 12, 2026
65716ba
fix(server): persist terminal output without rewriting all history (#…
nateEc Nov 12, 2026
8bfb723
perf(server): stop serializing terminal chunks just to measure them (…
bompus Nov 12, 2026
4cd7a3e
fix(web): stop opening a menu when a terminal selection ends (#24112)
fixfon Nov 12, 2026
30a26db
fix(server): agents keep named browser profiles after a server restar…
only21mil Nov 12, 2026
9b242e4
fix(web): reveal full composer command descriptions (#27414)
voltcrash Nov 12, 2026
44ee2b9
fix(server): restart the service after T3 home ownership clears (#25237)
maria-rcks Nov 12, 2026
a344276
fix(web): Enter on an empty prompt resumes an interrupted thread (#27…
toni92-beep Nov 12, 2026
9ac4535
fix(shared): token counts just below a unit boundary use the next uni…
juliusmarminge Nov 12, 2026
0685635
fix(desktop): long annotation strokes stay cheap to draw (#24573)
only21mil Nov 12, 2026
058d953
fix(ssh): load remote profiles with the account login shell (#23934)
nabettu Nov 12, 2026
da345d2
fix(server): terminals recreate a worktree removed by storage cleanup…
TonybynMp4 Nov 12, 2026
95f164d
test(web): Mod+E bold-word test no longer drops its first arrow key (…
juliusmarminge Nov 12, 2026
3595b9e
fix(server): access list keeps changes made while its stream opens (#…
juliusmarminge Nov 12, 2026
c544c27
fix(server): a failed Git version probe no longer advises installing …
juliusmarminge Nov 12, 2026
14978e3
fix(server): PR worktrees keep local commits on an existing branch (#…
only21mil Nov 12, 2026
c699562
fix(web): branch labels keep the space before a middle-truncated name…
guryanov-a Nov 12, 2026
13034bf
fix(server): stale provider update results clear once the provider is…
fixfon Nov 12, 2026
a0844ea
perf(server): merged-PR threads no longer re-read git state every min…
Batuhan1999 Nov 12, 2026
d9bed93
fix(server): pending request reads keep question option previews (#35…
juliusmarminge Nov 12, 2026
a135972
fix(opencode): preserve owned turns during server recovery (#25596)
maria-rcks Nov 12, 2026
072254d
fix(server): baseline existing pull request checks when watching (#23…
vkpdeveloper Nov 12, 2026
2ff2770
fix(server): saved preview snapshot failures name the screenshot dest…
juliusmarminge Nov 12, 2026
beaa647
fix(ssh): distinguish incompatible Node from a missing executable (#3…
MasonNight Nov 12, 2026
817b92f
fix(pi): stop offering extension-only models for text generation (#24…
only21mil Nov 12, 2026
18aeb0f
fix(server): new worktrees show submodule downloads instead of a stuc…
GuardedAirplane Nov 12, 2026
0508906
fix(web): restore composer focus when returning to a thread (#29179)
voltcrash Nov 12, 2026
bd731da
fix(server): linked calls fail fast when a peer stalls the MCP handsh…
only21mil Nov 12, 2026
c6b18c4
fix(web,mobile): keep line breaks in pending question text (#33353)
StiensWout Nov 12, 2026
ad2d22c
fix(mobile): opening a search result clears Home's search on iPhone (…
only21mil Nov 12, 2026
a6b8f71
fix(desktop): Escape during IME composition no longer cancels a previ…
juliusmarminge Nov 12, 2026
b1c1e07
fix(web): a click no longer lands on the sidebar row that just slid u…
mkantautas Nov 12, 2026
613d1ac
fix(mcp): background wait fails when none of its task IDs exist (#29773)
onlydole Nov 12, 2026
e1d59d1
fix(web): the collapsed mobile composer shows Stop while a turn runs …
TanJeeSchuan Nov 12, 2026
bce2bc0
test(web): split the cloud voice cancel flow from the failed-then-sav…
juliusmarminge Nov 12, 2026
4cc6beb
fix(desktop): nightly updates no longer depend on GitHub's tag order …
t3dotgg Nov 12, 2026
ff04519
fix(web): PDF preview prerenders pages two viewports below the visibl…
juliusmarminge Nov 12, 2026
e253b88
perf(server): uploading a large document with seven notebooks finishe…
Yuruzuu Nov 12, 2026
c4e74fe
Typeset equations that fail only because of an unescaped percent (#29…
meutex Nov 12, 2026
dbaef80
fix(server): wake PR watches when the merge queue drops a pull reques…
only21mil Nov 12, 2026
db470a3
fix(web): legacy collapsed project folders stay collapsed after a sav…
juliusmarminge Nov 12, 2026
c60aa1a
fix(server): explain how to close unclaimed browser tabs (#29205)
voltcrash Nov 12, 2026
9ccc0f8
fix(editors): JetBrains IDEs open file links in the thread's project …
only21mil Nov 12, 2026
e895a56
fix(mobile): select part of a source file's text on iOS (#33218)
binjamshid Nov 12, 2026
bc0befe
fix(mobile): show loading until existing threads are known (#28663)
shivamhwp Nov 12, 2026
aa37097
fix(web): streamed device toolbar starts at the panel size (#35664)
juliusmarminge Nov 12, 2026
1e6a251
fix(acp): preserve OMP single-choice questions and custom answers (#2…
nateEc Nov 12, 2026
3de49ba
fix(server): workspace search leaves out files Git ignores outside .g…
juliusmarminge Nov 12, 2026
12a62ed
fix(server): worktree setup no longer hangs when the shell is Nushell…
KenDJohnson Nov 12, 2026
6641231
fix(secrets): notify when a delegated agent asks for a secret (#33585)
zmievsa Nov 12, 2026
a98bc69
fix(web): Cursor threads keep a stored model the catalog doesn't list…
only21mil Nov 12, 2026
1e20361
fix(server): workspace search keeps backslashes in POSIX file names (…
juliusmarminge Nov 12, 2026
de77e61
fix(provider): make readiness probe timeouts configurable (#33275)
johanjohnthomas Nov 12, 2026
cc97303
fix(cursor): mcp.json servers using ${workspaceFolder} now start (#24…
only21mil Nov 12, 2026
7d51718
fix(server): workspace search ignore filter follows the host path sep…
juliusmarminge Nov 12, 2026
6dfb1a8
fix(server): t3 uninstall names another home's service it leaves inst…
juliusmarminge Nov 12, 2026
cfad310
fix(client-runtime): long MCP app download names no longer split an e…
juliusmarminge Nov 12, 2026
e179cef
fix(web): collapsed reasoning previews no longer split an emoji (#35677)
juliusmarminge Nov 12, 2026
4cfcfaf
fix(web): a question answer restored with undo is no longer sent twic…
ixmxvii-hash Nov 12, 2026
f59aa4e
fix(server): reject PR watches when hosting dependencies are unavaila…
complynx Nov 12, 2026
9fb4492
fix(chat): requeued steers no longer look delivered (#23420)
Alb11747 Nov 12, 2026
f6787e8
fix(web): reuse timeline rows while thinking and plans stream (#24121)
Batuhan1999 Nov 12, 2026
532bf2e
fix(mobile): Back from a PR opened in a chat returns to the chat on A…
bitmvk Nov 12, 2026
0ca50a6
fix(mobile): tall iOS code blocks with long lines show their code (#3…
Muzammil-Bit Nov 12, 2026
56b6c2a
fix(mobile): refreshing a review diff keeps unfolded files open (#35682)
juliusmarminge Nov 12, 2026
d76e066
fix(server): snooze no longer waits two minutes after a steered queue…
juliusmarminge Nov 12, 2026
f70e349
test(server): count leaked PR cache caller data without WeakRefs (#35…
juliusmarminge Nov 12, 2026
bb26e56
fix(cli): t3 pair's protocol mismatch hint keeps --base-dir (#35690)
juliusmarminge Nov 12, 2026
141de5e
fix(web): a dismissed provider banner stays dismissed when you switch…
torztomasz Nov 12, 2026
c19b8f0
fix(cli): t3 status no longer suggests --no-lan under a T3CODE_HOST o…
juliusmarminge Nov 12, 2026
2e741fc
fix(desktop): wait for the server's database upgrade instead of repor…
juliusmarminge Nov 13, 2026
39f465a
fix(server): a C# dictionary term no longer rewrites standalone lette…
juliusmarminge Nov 13, 2026
c1b42c1
fix(client-runtime): last user message sort ignores agent-posted mess…
juliusmarminge Nov 13, 2026
f8372d1
fix(web): long screen-reader announcements keep emoji whole at the cu…
juliusmarminge Nov 13, 2026
82839de
fix(server): Claude context meter honours CLAUDE_CODE_AUTO_COMPACT_WI…
juliusmarminge Nov 13, 2026
e0d6aad
feat(server): watched GitHub pull requests react when a check suite c…
juliusmarminge Nov 13, 2026
9b0387d
fix(web): triple-click selects the whole line around links and file c…
juliusmarminge Nov 13, 2026
56d2278
fix(projects): preserve grouped renames to repository names (#25244)
maria-rcks Nov 13, 2026
5959787
fix(acp): agents get T3's runtime notes once per session, not on ever…
Radu028 Nov 13, 2026
587deab
feat(server): keep the computer awake while agents work (#34128)
juliusmarminge Nov 13, 2026
254e985
fix(web): find scheduled tasks in Settings search (#35715)
juliusmarminge Nov 13, 2026
fec3b3a
fix(preview): bound stalled screenshot captures (#25862)
cabamarcos Nov 13, 2026
48a56fa
test(server): idle-resume replays wait for the session release before…
juliusmarminge Nov 13, 2026
ac1cb92
fix(server): archive refuses a thread with running delegated tasks (#…
SouthCarpet Nov 13, 2026
c99201c
fix(web): minimap Next turn no longer sticks on adjacent user message…
juliusmarminge Nov 13, 2026
d4b77d2
fix(server): Claude background commands show how long they have run (…
SunkenInTime Nov 13, 2026
c2931a6
fix(server): honor worktree prefixes when automatic naming fails (#35…
juliusmarminge Nov 13, 2026
7bf2a43
fix(git): refuse remote switches that would silently detach HEAD (#35…
Kirat0201 Nov 13, 2026
0465d4a
fix(mobile): retain inline image zoom on iOS (#33852)
saphid Nov 13, 2026
b54dad1
fix(server): Claude's auto-compact setting now wins over CLAUDE_CODE_…
juliusmarminge Nov 13, 2026
79daf43
fix(desktop): an SSH reconnect during a desktop update no longer lock…
ajbarryiii Nov 13, 2026
0bb6eaa
fix(web): auto-balanced drafts recheck machine load before sending (#…
juliusmarminge Nov 13, 2026
4137412
fix(web): screen readers at Everything hear context compaction start …
juliusmarminge Nov 13, 2026
59e64ee
fix(server): t3_thread_read returns the newest items and fits in agen…
darjss Nov 13, 2026
a119af0
fix(server): remote status keeps a local upstream named without a sla…
juliusmarminge Nov 13, 2026
36254ab
fix(server): a watched PR stops holding its thread once its checks fi…
juliusmarminge Nov 13, 2026
ce26864
fix(server): a slow database write no longer freezes the server (#35728)
tnfssc Nov 13, 2026
62647f4
fix(mobile): Android sheets scroll without per-feature platform check…
juliusmarminge Nov 13, 2026
6b292d9
docs: drop the GitCafe mirror runbook for a generic forge mirror (#35…
juliusmarminge Nov 13, 2026
12421da
fix(ssh): timed-out SSH commands no longer leave a proxy helper runni…
juliusmarminge Nov 13, 2026
63426c4
fix(dev): long-running dev servers no longer leak memory when other w…
t3dotgg Nov 13, 2026
f5d0be6
fix(server): Changes totals no longer take index.lock (#35770)
juliusmarminge Nov 13, 2026
ad1c46b
fix(server): keep repository identity for roots whose name ends in a …
juliusmarminge Nov 13, 2026
adf3151
fix(server): restored worktrees find pushed thread work on a non-orig…
juliusmarminge Nov 13, 2026
25feac2
fix(ssh): an alias no longer hides known_hosts entries on other ports…
juliusmarminge Nov 13, 2026
368b4fe
fix(server): IPv4-mapped loopback SSH device targets count as this ma…
juliusmarminge Nov 13, 2026
c0b08fa
fix(ssh): host discovery follows Include globs in directory component…
juliusmarminge Nov 13, 2026
8b9f8ca
fix(server): Commit finishes a merge resolved to the current branch's…
juliusmarminge Nov 13, 2026
b8c74f5
fix(server): gh default remote lookup ignores an inherited GIT_DIR (#…
juliusmarminge Nov 13, 2026
7bc3684
docs: FAQ says the Claude 5-hour limit drain is fixed (#31473)
linushohne Nov 13, 2026
336a8c1
fix(server): fork PR worktrees no longer fail when a local branch own…
juliusmarminge Nov 13, 2026
05cc444
fix(web): a failed commit keeps the commit message you typed (#35781)
juliusmarminge Nov 13, 2026
8a1811e
fix(server): branches whose names start with a parenthesis are no lon…
juliusmarminge Nov 13, 2026
49440b2
test(ssh): launcher harness no longer races its background nohup (#35…
juliusmarminge Nov 13, 2026
442b1db
fix(web): Resume replaces Stop without a lingering Stopped button (#3…
juliusmarminge Nov 13, 2026
d86ee1e
test(web): typecheck the project file refresh test (#34061)
maschwenk Nov 13, 2026
6f21b0f
docs(codex): explain native image generation setup (#31426)
dpreil-ydlp Nov 13, 2026
bb9658d
perf(shared): command output failure scan rejects clean output in one…
hieu-lee Nov 13, 2026
5ccc65b
test(web): sidebar drag test opens its folder past the shift guard (#…
juliusmarminge Nov 13, 2026
7fb54eb
feat(mobile): secret field gets an eye toggle to show what you typed …
t3dotgg Nov 13, 2026
5ceabb3
fix(mobile): a cold launch shows the cached thread rows with the firs…
t3dotgg Nov 13, 2026
45b0fe4
fix(server): ACP terminal MCP calls answer interleaved server request…
juliusmarminge Nov 13, 2026
2755a95
fix(web): "Try again" recovers from a failed page chunk load (#35806)
juliusmarminge Nov 13, 2026
36371de
fix(shared): read MCP event streams whatever the case of the content …
juliusmarminge Nov 13, 2026
9318eb2
Merge remote-tracking branch 'upstream/main' into yordis/chore-sync-u…
yordis Nov 13, 2026
8b4db39
docs(fork): record the pull request watch instructions in the ledger
yordis Nov 13, 2026
97190ef
fix(server): record the merged agent context budget
yordis Nov 13, 2026
1741e4c
test: align watch tests with the merged upstream behavior
yordis Nov 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
45 changes: 45 additions & 0 deletions .github/scripts/check-nightly-release.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,54 @@ async function resolveLatestNightlyCommit({ github, context, core }) {
return { tag, sha: commit.sha, version };
}

// The two places a nightly install looks for an update: the `update-feed` pointer, and
// releases.atom, which holds the 10 newest git tags of any kind. Older installs take the first
// nightly in the feed. Reports what each names beside the newest published nightly.
async function readNightlyUpdateFeed({ github, context, fetch = globalThis.fetch }) {
const latest = await findLatestNightly({ github, context });
if (!latest) return {};

const base = `https://github.com/${context.repo.owner}/${context.repo.repo}`;
// Only a missing or malformed pointer is stale. Rewriting deletes the old file first, so a
// GitHub outage must not look like one.
// Node waits five minutes for a stalled response, which is this job's whole time limit.
const signal = () => AbortSignal.timeout(30_000);
const pointer = await fetch(`${base}/releases/download/update-feed/nightly.json`, {
signal: signal(),
});
if (!pointer.ok && pointer.status !== 404) {
throw new Error(`The update-feed pointer answered HTTP ${pointer.status}.`);
}
let pointerTag;
if (pointer.ok) {
// A body that fails to arrive throws here; only one that is not JSON counts as malformed.
const body = await pointer.text();
try {
pointerTag = JSON.parse(body)?.tag_name;
} catch {}
}

// An unreadable feed leaves `feedTags` unset, so the pointer is still repaired. An error page
// can answer HTTP 200, so only an Atom document counts as read.
let feedTags;
try {
const feed = await fetch(`${base}/releases.atom`, { signal: signal() });
const body = feed.ok ? await feed.text() : "";
if (/<feed[\s>]/.test(body)) {
feedTags = Array.from(body.matchAll(/href="[^"]*\/releases\/tag\/([^"]+)"/g), (match) =>
decodeURIComponent(match[1]),
);
}
} catch {}
const feedNightly = feedTags?.find((tag) => /^v?\d+\.\d+\.\d+-nightly\./.test(tag));

return { tag: latest.tagName, pointerTag, feedNightly, feedTags };
}

module.exports = {
assertCommitOnDefaultBranch,
assertReleaseSource,
shouldReleaseNightly,
resolveLatestNightlyCommit,
readNightlyUpdateFeed,
};
78 changes: 78 additions & 0 deletions .github/scripts/check-nightly-release.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,81 @@ test("stable releases fail when no nightly is published", async () => {
const { options } = nightlyCommitFixture({ releases: [nightly(1, { tagName: "v1.0.0" })] });
await assert.rejects(resolveLatestNightlyCommit(options), /No published nightly found/);
});

const { readNightlyUpdateFeed } = require("./check-nightly-release.cjs");

// GitHub percent-encodes tag names in the feed, and any tag in the repository takes a slot.
const atomEntry = (tag) =>
`<entry><link rel="alternate" type="text/html" href="https://github.com/example/app/releases/tag/${encodeURIComponent(tag)}"/></entry>`;
const atomFeed = (tags) => new Response(`<feed>${tags.map(atomEntry).join("")}</feed>`);
const NEWEST = "v1.0.1-nightly.20260905.123";
const OLDER = "v1.0.1-nightly.20260905.100";

function updateFeedFixture({ feed, pointer }) {
const { options } = fixture({
releases: [nightly(1), nightly(0, { tagName: "v1.0.2-preview.1" })],
});
options.fetch = async (url) =>
url === "https://github.com/example/app/releases.atom"
? feed
: url === "https://github.com/example/app/releases/download/update-feed/nightly.json"
? pointer
: assert.fail(`unexpected fetch ${url}`);
return options;
}

const missing = () => new Response("Not Found", { status: 404 });
// HTTP 200, then the connection drops before the body arrives.
const brokenBody = () =>
new Response(new ReadableStream({ start: (controller) => controller.error(new Error("reset")) }));
for (const [name, { feed, pointer }, expected] of [
[
"a nightly crowded out of the feed and a missing pointer",
{ feed: atomFeed(["v1.0.2-preview.1", "mobile-android-build/2.3.0/abc"]), pointer: missing() },
{ pointerTag: undefined, feedNightly: undefined },
],
[
"the first nightly in the feed, which is the one older installs take",
{ feed: atomFeed(["update-feed", OLDER, NEWEST]), pointer: Response.json({ tag_name: OLDER }) },
{ pointerTag: OLDER, feedNightly: OLDER },
],
[
"a malformed pointer as stale and an unreadable feed as naming nothing",
{ feed: new Response("", { status: 503 }), pointer: Response.json(null) },
{ pointerTag: undefined, feedNightly: undefined, unreadable: true },
],
[
"an HTML page with release links as an unreadable feed",
{
feed: new Response(
`<html><feedback-form></feedback-form><a href="/example/app/releases/tag/${OLDER}">x</a></html>`,
),
pointer: Response.json({ tag_name: NEWEST }),
},
{ pointerTag: NEWEST, feedNightly: undefined, unreadable: true },
],
[
"the pointer when the feed's body fails to arrive",
{ feed: brokenBody(), pointer: Response.json({ tag_name: NEWEST }) },
{ pointerTag: NEWEST, feedNightly: undefined, unreadable: true },
],
]) {
test(`update feed check reports ${name}`, async () => {
const { tag, pointerTag, feedNightly, feedTags } = await readNightlyUpdateFeed(
updateFeedFixture({ feed, pointer }),
);
const { unreadable = false, ...named } = expected;
assert.deepEqual({ tag, pointerTag, feedNightly }, { tag: NEWEST, ...named });
assert.equal(feedTags === undefined, unreadable);
});
}

for (const [name, pointer, error] of [
["answers with an error", () => new Response("", { status: 503 }), /pointer answered HTTP 503/],
["drops the connection", brokenBody, /reset/],
]) {
test(`update feed check does not call a pointer stale when GitHub ${name}`, async () => {
const options = updateFeedFixture({ feed: atomFeed([NEWEST]), pointer: pointer() });
await assert.rejects(readNightlyUpdateFeed(options), error);
});
}
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -504,8 +504,9 @@ jobs:
T3CODE_TRANSFER_BUDGET_REPORT_PATH: ${{ runner.temp }}/t3code-transfer-budget.md
T3CODE_TRANSFER_BUDGET_RESULT_PATH: ${{ runner.temp }}/thread-transfer-result.json
T3CODE_TRANSFER_BUDGET_SCENARIOS_PATH: ${{ runner.temp }}/thread-transfer-scenarios.json
# One process per file spends ~2s starting before its tests run; one file per vCPU overlaps that.
run: >-
vp run test:cached -- --shard ${{ matrix.shard }}/${{ strategy.job-total }}
vp run test:cached -- --shard ${{ matrix.shard }}/${{ strategy.job-total }} --concurrency 4
--uncached apps/server/integration/transferBudgetV2.integration.test.ts
--verify-hits 1 apps/server

Expand Down
95 changes: 82 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ jobs:
# published nightly, so stable only ever ships a build that nightly users
# have already run. Scheduled runs also decide here whether a nightly is due.
# The PAUSE_NIGHTLY repository variable skips scheduled runs only; every later
# job needs this one, so a skip here skips the whole run.
# job needs this one, so a skip here skips the whole run. `update_feed` runs
# without it and checks the variable itself.
resolve_commit:
name: Resolve release commit
if: github.repository == 'pingdotgg/t3code' && (github.event_name != 'schedule' || vars.PAUSE_NIGHTLY != 'true')
Expand Down Expand Up @@ -165,14 +166,12 @@ jobs:
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
echo "make_latest=false" >> "$GITHUB_OUTPUT"
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-preview}" == "preview" ]]; then
# Manual-only test train: exercises the whole release flow for a
# commit end users must never receive. Never scheduled.
# Manual-only test train: exercises the release flow up to a draft
# release for a commit end users must never receive. Never scheduled.
# Same versioning as nightly under its own prerelease identifier.
# A preview release is reachable only by asking for it: npm gets it
# under the `preview` dist-tag, which nothing resolves by default,
# its desktop builds carry no update feed, and no updater manifest
# is attached to the release, so neither stable nor nightly
# installs can ever be offered one.
# A preview release stays a draft (see "Publish prerelease"), its
# desktop builds carry no update feed, no updater manifest is
# attached, and npm gets nothing, so no install can be offered one.
nightly_date="$(date -u -d "$NIGHTLY_DATE" +%Y%m%d)"

node scripts/resolve-nightly-release.ts \
Expand All @@ -183,7 +182,6 @@ jobs:
--github-output

echo "release_channel=preview" >> "$GITHUB_OUTPUT"
echo "cli_dist_tag=preview" >> "$GITHUB_OUTPUT"
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
echo "make_latest=false" >> "$GITHUB_OUTPUT"
else
Expand Down Expand Up @@ -1038,22 +1036,93 @@ jobs:
# The action keeps a stable release a draft until its assets finish uploading, but publishes a
# prerelease first. Desktop clients update connected servers from the CLI archives as soon as
# they see the release, so a prerelease also goes public only once every asset is attached.
# Previews stay drafts, because a draft has no git tag. releases.atom lists the 10 newest tags,
# a day's `preview` tags can sort above its `nightly` ones, and older updaters find nightlies only there.
- name: Publish prerelease
if: needs.preflight.outputs.is_prerelease == 'true'
if: needs.preflight.outputs.is_prerelease == 'true' && needs.preflight.outputs.release_channel != 'preview'
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.preflight.outputs.tag }}
run: gh release edit "$RELEASE_TAG" --draft=false

# A nightly install finds updates in releases.atom, which holds the 10 newest git tags of any
# kind, so a burst of other tags hides every nightly. The `update-feed` release is the fixed URL
# newer installs also read. Every unpaused scheduled tick and every nightly repairs it, then fails when
# the feed's first nightly is not the newest one. See docs/operations/release.md#update-feed.
update_feed:
name: Check the update feed
needs: [preflight, release]
if: ${{ !cancelled() && github.repository == 'pingdotgg/t3code' && ((github.event_name == 'schedule' && vars.PAUSE_NIGHTLY != 'true') || needs.preflight.outputs.release_channel == 'nightly') }}
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 5
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
sparse-checkout: .github/scripts

- id: feed
name: Read the update feed
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
retries: 3
script: |
const { readNightlyUpdateFeed } = require('./.github/scripts/check-nightly-release.cjs');
const { tag, pointerTag, feedNightly, feedTags } = await readNightlyUpdateFeed({ github, context });
if (!tag) return;
core.info(`Newest nightly: ${tag}. Pointer: ${pointerTag}. Feed: ${feedTags?.join(', ')}.`);
core.setOutput('tag', tag);
core.setOutput('pointer_stale', pointerTag !== tag);
core.setOutput('feed', feedTags === undefined ? 'unreadable' : feedNightly === tag ? 'ok' : 'hidden');

- name: Point the update feed at the newest nightly
if: steps.feed.outputs.pointer_stale == 'true'
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ steps.feed.outputs.tag }}
run: |
gh api "repos/$GH_REPO/releases/tags/$RELEASE_TAG" --jq '{tag_name, name, body}' > nightly.json
gh release view update-feed > /dev/null 2>&1 ||
gh release create update-feed --prerelease --latest=false --title "Update feed" \
--notes "The desktop updater reads this release to find the newest nightly. Do not delete it. See docs/operations/release.md."
gh release upload update-feed nightly.json --clobber

# Every run, because the pointer can be current while the release's flags are wrong. A draft's
# files are not public, and a release marked "latest" is the one stable installs update to.
- name: Keep the update feed release public and never latest
if: steps.feed.outputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh release edit update-feed --draft=false --prerelease --latest=false

# An unreadable feed says nothing about its tags and the next tick reads it again, so only a
# feed that was read and names another nightly first fails the run.
- name: Fail when the feed hides the newest nightly
if: steps.feed.outputs.tag != '' && steps.feed.outputs.feed != 'ok'
env:
RELEASE_TAG: ${{ steps.feed.outputs.tag }}
FEED: ${{ steps.feed.outputs.feed }}
run: |
if [[ "$FEED" == "unreadable" ]]; then
echo "::warning::releases.atom could not be read, so this run did not check it."
exit 0
fi
echo "::error::The first nightly in releases.atom is not $RELEASE_TAG. Nightly installs older than 0.1.10-nightly.20261112.4603 cannot reach it, and neither can later builds that do not read the pointer when the feed names an older nightly. A preview from a branch not rebased onto main is the usual cause. See docs/operations/release.md#update-feed."
exit 1

# The `t3` npm package is a launcher that downloads this version's CLI
# archive from the GitHub Release, so it publishes only once that release
# exists. Preview publishes too, under the `preview` dist-tag, which
# nothing resolves unless asked for by name.
# exists. A preview release stays a draft, whose files are not public, so
# a preview never publishes the launcher.
publish_cli:
name: Publish CLI to npm
needs: [preflight, build_bundle, release]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }}
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel != 'preview' }}
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 15
permissions:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ node_modules/
# pnpm content-addressable store; never belongs in the repo.
.pnpm-store/
.vitest/

/packages/ssh/.ssh-launcher-test-*/
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ The most common defect in this repo is a change that works on the path you teste
- Ports derive from the worktree path and are stable across restarts, but read the real ones from the `[dev-runner]` line since occupied ports shift.
- Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, then give that full URL to an unpaired browser. Do not wire up `tailscale serve` by hand, open the URL yourself, or consume the user's pairing link. A browser with the reusable dev cookie can use the bare origin. If a normal one-time token was consumed, mint a fresh one with `node apps/server/src/bin.ts pair`. It carries standard scopes, while the startup URL carries admin scopes needed for Connections settings.
- To reuse web dev auth across worktrees, configure one fixed `T3CODE_DEV_AUTH_TOKEN` in the main checkout's gitignored `.env`. The `t3.json` setup links that file into worktrees. Never commit or publish the token or a startup URL. See [Reusable dev credential](docs/operations/development.md#reusable-dev-credential).
- Stop what you started, by the PID you tracked. See rule 1.
- Stop what you started, by the PID you tracked. See rule 1. `vp` does not pass SIGTERM on, so signalling the `vp run dev` PID alone leaves Vite and the server running. Stop the background task with your harness's own stop, which ends the whole tree.
- Run dev servers, Metro, and builds from your own shell so they end with your session. Do not detach them with `systemd-run`, `nohup`, `setsid`, or `tmux` unless the user asks for one that outlives the session. Detached servers ran for weeks and filled RAM and swap. The dev runner stops itself above `--memory-limit-gb` (half of RAM, at most 16 GiB) on Linux.
- Need a second checkout? `git worktree add` it next to this worktree, not in `/tmp`. Remove it when done.

## Test data
Expand Down Expand Up @@ -150,7 +151,7 @@ For mobile verification, a missing or outdated native client is an install step,

- Never make a PR unless the developer explicitly asks you to do so.
- PRs target this fork (`TrogonStack/t3code`), never `pingdotgg/t3code`, unless the developer asks for the change to go upstream.
- If `origin` is git.cafe, follow [GitCafe mirror](docs/operations/development.md#gitcafe-mirror-optional): push to `origin`, open and track PRs with `cafe` and their git.cafe URLs, and keep `gh` for merging.
- `origin` is not always GitHub. Check `git remote get-url origin` before your first forge command and use that forge's CLI and skill. See [Forge mirror](docs/operations/development.md#forge-mirror-optional).
- Conventional commit titles, plain language: `fix(web): new threads no longer spike CPU`.
- Body: the problem in a sentence or two, then how you fixed it.
- UI changes need before/after images. Motion or timing needs a short video.
Expand Down
Loading
Loading