Skip to content

Commit 89fbeb2

Browse files
juliusmarmingecodex
andcommitted
Merge origin/main into mobile remote connect
Co-authored-by: codex <codex@users.noreply.github.com>
2 parents fd93d7f + 34ec8a8 commit 89fbeb2

59 files changed

Lines changed: 1993 additions & 405 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,19 +272,25 @@ jobs:
272272
exit 0
273273
}
274274
275-
Install-PackageProvider `
276-
-Name NuGet `
277-
-MinimumVersion 2.8.5.201 `
278-
-Force `
279-
-Scope CurrentUser
275+
try {
276+
Install-PackageProvider `
277+
-Name NuGet `
278+
-MinimumVersion 2.8.5.201 `
279+
-Force `
280+
-Scope CurrentUser `
281+
-ErrorAction Stop
282+
} catch {
283+
Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)"
284+
}
280285
281286
Install-Module `
282287
-Name TrustedSigning `
283288
-MinimumVersion 0.5.0 `
284289
-Force `
285290
-AllowClobber `
286291
-Repository PSGallery `
287-
-Scope CurrentUser
292+
-Scope CurrentUser `
293+
-ErrorAction Stop
288294
289295
Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force
290296
Get-Command Invoke-TrustedSigning -ErrorAction Stop

REMOTE.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,43 @@ After setup, the renderer connects to a local forwarded HTTP/WebSocket endpoint.
115115

116116
SSH launch is a desktop feature because it needs local process and SSH access. Once the environment is paired and saved, it uses the same environment list and connection model as direct LAN, Tailscale, HTTPS, or future tunnel-backed environments.
117117

118+
#### SSH Launch Troubleshooting
119+
120+
The desktop SSH launcher connects with a non-interactive `sh` session, writes a small launcher script under `~/.t3/ssh-launch/<host-key>/`, starts or reuses a remote T3 server, and forwards the remote loopback port back to your desktop.
121+
122+
The remote host must have a compatible Node.js runtime. T3 Code uses the server package's `engines.node` requirement:
123+
124+
```text
125+
^22.16 || ^23.11 || >=24.10
126+
```
127+
128+
During SSH launch, T3 Code first checks whether `node` is already available on `PATH`. If it is missing, the launcher tries common non-interactive shell locations and version-manager shims/activation hooks:
129+
130+
- `~/.local/bin`, `~/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
131+
- Volta via `~/.volta/bin`
132+
- asdf via `~/.asdf/shims`, `~/.asdf/bin`, or `~/.asdf/asdf.sh`
133+
- mise via `~/.local/share/mise/shims`, `~/.mise/shims`, or `mise activate sh`
134+
- fnm via `fnm env --use-on-cd --shell sh` or `fnm env --shell sh`
135+
- nodenv via `~/.nodenv/bin`, `~/.nodenv/shims`, or `nodenv init -`
136+
- nvm via `$NVM_DIR/nvm.sh`, then `nvm use default`, `nvm use node`, or `nvm use --lts`
137+
- installed nvm versions under `$NVM_DIR/versions/node/*/bin`
138+
139+
If launch fails with `node: command not found`, a port-scan failure, or a message that the remote Node version does not satisfy the required range, SSH into the host and check the same non-interactive shell path T3 Code uses:
140+
141+
```bash
142+
ssh user@example.com 'sh -lc "command -v node && node --version"'
143+
```
144+
145+
If that does not print a compatible Node version, configure your version manager for non-interactive shells or install a compatible Node binary in one of the searched locations. For example, with nvm you may need a default alias:
146+
147+
```bash
148+
nvm alias default 24
149+
```
150+
151+
With mise/asdf/fnm/nodenv, make sure the tool's shim directory is installed and points at a Node version satisfying the range above.
152+
153+
If reconnecting after an app update fails, retry the SSH launch once. The launcher now compares its generated runner script, stops stale launcher-managed remote servers, clears the SSH launch PID/port state, and starts a fresh remote server. You should not normally need to delete `~/.t3/ssh-launch` or kill `t3` processes manually.
154+
118155
## How Pairing Works
119156

120157
The remote device does not need a long-lived secret up front.

apps/desktop/src/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as Electron from "electron";
1111
import * as NetService from "@t3tools/shared/Net";
1212
import { resolveRemoteT3CliPackageSpec } from "@t3tools/ssh/command";
1313
import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel";
14+
import serverPackageJson from "../../server/package.json" with { type: "json" };
1415

1516
import type { DesktopSettings as DesktopSettingsValue } from "./settings/DesktopAppSettings.ts";
1617
import * as DesktopIpc from "./ipc/DesktopIpc.ts";
@@ -65,14 +66,18 @@ const resolveDesktopSshCliRunner = (
6566
): RemoteT3RunnerOptions => {
6667
const devRemoteEntryPath = Option.getOrUndefined(environment.devRemoteT3ServerEntryPath);
6768
if (environment.isDevelopment && devRemoteEntryPath !== undefined) {
68-
return { nodeScriptPath: devRemoteEntryPath };
69+
return {
70+
nodeScriptPath: devRemoteEntryPath,
71+
nodeEngineRange: serverPackageJson.engines.node,
72+
};
6973
}
7074
return {
7175
packageSpec: resolveRemoteT3CliPackageSpec({
7276
appVersion: environment.appVersion,
7377
updateChannel: settings.updateChannel,
7478
isDevelopment: environment.isDevelopment,
7579
}),
80+
nodeEngineRange: serverPackageJson.engines.node,
7681
};
7782
};
7883

apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ describe("CheckpointDiffQueryLive", () => {
9494
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
9595
getShellSnapshot: () =>
9696
Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
97+
getArchivedShellSnapshot: () =>
98+
Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
9799
getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
98100
getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
99101
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
@@ -174,6 +176,8 @@ describe("CheckpointDiffQueryLive", () => {
174176
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
175177
getShellSnapshot: () =>
176178
Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
179+
getArchivedShellSnapshot: () =>
180+
Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
177181
getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
178182
getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
179183
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
@@ -222,6 +226,8 @@ describe("CheckpointDiffQueryLive", () => {
222226
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
223227
getShellSnapshot: () =>
224228
Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
229+
getArchivedShellSnapshot: () =>
230+
Effect.die("CheckpointDiffQuery should not request archived shell snapshots"),
225231
getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
226232
getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
227233
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),

apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,13 @@ describe("OrchestrationEngine", () => {
183183
threads: [],
184184
updatedAt: projectionSnapshot.updatedAt,
185185
}),
186+
getArchivedShellSnapshot: () =>
187+
Effect.succeed({
188+
snapshotSequence: projectionSnapshot.snapshotSequence,
189+
projects: [],
190+
threads: [],
191+
updatedAt: projectionSnapshot.updatedAt,
192+
}),
186193
getSnapshotSequence: () =>
187194
Effect.succeed({ snapshotSequence: projectionSnapshot.snapshotSequence }),
188195
getCounts: () => Effect.succeed({ projectCount: 1, threadCount: 1 }),

apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,126 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
441441
}),
442442
);
443443

444+
it.effect("keeps archived threads out of the main shell snapshot", () =>
445+
Effect.gen(function* () {
446+
const snapshotQuery = yield* ProjectionSnapshotQuery;
447+
const sql = yield* SqlClient.SqlClient;
448+
449+
yield* sql`DELETE FROM projection_projects`;
450+
yield* sql`DELETE FROM projection_threads`;
451+
yield* sql`DELETE FROM projection_state`;
452+
453+
yield* sql`
454+
INSERT INTO projection_projects (
455+
project_id,
456+
title,
457+
workspace_root,
458+
default_model_selection_json,
459+
scripts_json,
460+
created_at,
461+
updated_at,
462+
deleted_at
463+
)
464+
VALUES (
465+
'project-archive-test',
466+
'Archive Test',
467+
'/tmp/archive-test',
468+
'{"provider":"codex","model":"gpt-5-codex"}',
469+
'[]',
470+
'2026-04-06T00:00:00.000Z',
471+
'2026-04-06T00:00:01.000Z',
472+
NULL
473+
)
474+
`;
475+
476+
yield* sql`
477+
INSERT INTO projection_threads (
478+
thread_id,
479+
project_id,
480+
title,
481+
model_selection_json,
482+
runtime_mode,
483+
interaction_mode,
484+
branch,
485+
worktree_path,
486+
latest_turn_id,
487+
latest_user_message_at,
488+
pending_approval_count,
489+
pending_user_input_count,
490+
has_actionable_proposed_plan,
491+
created_at,
492+
updated_at,
493+
archived_at,
494+
deleted_at
495+
)
496+
VALUES
497+
(
498+
'thread-active',
499+
'project-archive-test',
500+
'Active Thread',
501+
'{"provider":"codex","model":"gpt-5-codex"}',
502+
'full-access',
503+
'default',
504+
NULL,
505+
NULL,
506+
NULL,
507+
NULL,
508+
0,
509+
0,
510+
0,
511+
'2026-04-06T00:00:02.000Z',
512+
'2026-04-06T00:00:03.000Z',
513+
NULL,
514+
NULL
515+
),
516+
(
517+
'thread-archived',
518+
'project-archive-test',
519+
'Archived Thread',
520+
'{"provider":"codex","model":"gpt-5-codex"}',
521+
'full-access',
522+
'default',
523+
NULL,
524+
NULL,
525+
NULL,
526+
NULL,
527+
0,
528+
0,
529+
0,
530+
'2026-04-06T00:00:04.000Z',
531+
'2026-04-06T00:00:05.000Z',
532+
'2026-04-06T00:00:06.000Z',
533+
NULL
534+
)
535+
`;
536+
537+
yield* sql`
538+
INSERT INTO projection_state (projector, last_applied_sequence, updated_at)
539+
VALUES
540+
(${ORCHESTRATION_PROJECTOR_NAMES.projects}, 4, '2026-04-06T00:00:07.000Z'),
541+
(${ORCHESTRATION_PROJECTOR_NAMES.threads}, 4, '2026-04-06T00:00:07.000Z'),
542+
(${ORCHESTRATION_PROJECTOR_NAMES.threadMessages}, 4, '2026-04-06T00:00:07.000Z'),
543+
(${ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans}, 4, '2026-04-06T00:00:07.000Z'),
544+
(${ORCHESTRATION_PROJECTOR_NAMES.threadActivities}, 4, '2026-04-06T00:00:07.000Z'),
545+
(${ORCHESTRATION_PROJECTOR_NAMES.threadSessions}, 4, '2026-04-06T00:00:07.000Z'),
546+
(${ORCHESTRATION_PROJECTOR_NAMES.checkpoints}, 4, '2026-04-06T00:00:07.000Z')
547+
`;
548+
549+
const shellSnapshot = yield* snapshotQuery.getShellSnapshot();
550+
assert.deepEqual(
551+
shellSnapshot.threads.map((thread) => thread.id),
552+
[ThreadId.make("thread-active")],
553+
);
554+
555+
const archivedShellSnapshot = yield* snapshotQuery.getArchivedShellSnapshot();
556+
assert.deepEqual(
557+
archivedShellSnapshot.threads.map((thread) => thread.id),
558+
[ThreadId.make("thread-archived")],
559+
);
560+
assert.equal(archivedShellSnapshot.threads[0]?.archivedAt, "2026-04-06T00:00:06.000Z");
561+
}),
562+
);
563+
444564
it.effect(
445565
"reads targeted project, thread, and count queries without hydrating the full snapshot",
446566
() =>

0 commit comments

Comments
 (0)