Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/durable-facts.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
{"id":"2026-08-30-prime-correlated-prompt-lifecycle","recorded_at":"2026-08-30T01:34:28Z","scope":"provider/prime-agent","fact":"Prime foreground prompt ownership has two explicit modes and is never inferred from text or generic busy state. The optional correlated lifecycle is enabled only by explicit capability negotiation; its correlation stays provider-private and generation-bound, and recovery requires complete attributed event continuity without synthesizing snapshot output. Stock daemons use the conservative observed-activity busy path.","source_commits":["058193588f66d8b455cd49e9b014bc9fde4b0447","9a4de4c08ee62dea2b5ad68ff8bf022addb67d61","486af3ea52180c3dcda51b6411ffaeecf9987d08"],"source_paths":["docs/internals/prime-agent-daemon-parity.md","docs/user/providers-prime-agent.md","apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts","apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts","apps/server/src/provider/prime/PrimeAgentDaemonSessionRuntime.ts"],"supersedes":[]}
{"id":"2026-08-30-runtime-home-default","recorded_at":"2026-08-31T14:45:36Z","scope":"server/runtime-home","fact":"Every Pylon server launch path defaults its runtime home to ~/.pylon-code: the t3 CLI's resolveBaseDir, the desktop's SSH remote launcher scripts, the WSL secondary backend, and the dev runner's implicit home. ~/.t3 is T3 Code's install and is never adopted as a fallback, because its database carries upstream migration numbering. --base-dir and T3CODE_HOME remain the unchanged, T3-compatible overrides. When a launch resolves the default base dir, finds no state there, and sees an older ~/.t3/userdata, warnAboutLegacyRuntimeHome writes one hint to stderr and never to the logger: auth, connect, and project resolve their base dir through the same helper and emit JSON payloads on the logger's stream, and their quietLogs guard is installed too late to suppress it. Migration moves the userdata state directory, not its parent: settings.json and secrets are derived from stateDir and live inside userdata, while the sibling caches and worktrees directories are disposable or referenced by absolute path and must stay. The SSH launch script names ~/.t3 only to retire a previous launcher's state directory once, and signals a recorded pid only after confirming its process arguments still carry serve --host 127.0.0.1 and --base-dir $HOME/.t3; where ps cannot report arguments it leaves the process running and still retires the directory. The maintainer database scripts keep their source home and their destructive-write guard separate: both read or seed from ~/.pylon-code but refuse to write to either runtime home, and migrate-dev-db applies that refusal before it checks whether a source exists.","source_commits":["a5cfa5da266c3e10ff5b7b049aea8035c222282a","8ec50a068a278b385428b5c131bf093ecaf7079c","ec31498eb69ebe7c6f15985ecb8320205a0ad872","5814f1a62a91d852b752562c39ad722323f999cc","fc7bda6e1fe7f058891d741ebea5116ce0ba4e37","1ace841de68d5e4563647182621d633a44025a81","8c298a75b153dba825114fe5601d26e2339c3f01","20bae41009f8cbb43ed7cb044ba024f5c6971ea5","a4ddf3266e6175de0cb76ffc5f8cc3ec3d180bac","66401f073eb502fd00e9719a18d6e8343cb500d9"],"source_paths":["apps/server/src/os-jank.ts","apps/server/src/os-jank.test.ts","apps/server/src/cli/config.ts","apps/server/src/cli/config.test.ts","apps/server/src/cli/triage.ts","packages/ssh/src/tunnel.ts","packages/ssh/src/tunnel.test.ts","apps/server/scripts/migrate-dev-db.ts","apps/server/scripts/t3-sqlite-state.ts","scripts/dev-runner.ts","docs/user/install.md","docs/user/remote-access.md","docs/operations/observability.md","docs/internals/scripts.md"],"supersedes":[]}
{"id": "2026-09-09-client-settings-default-flip-needs-fresh-key", "recorded_at": "2026-09-09T07:25:11Z", "scope": "contracts/client-settings", "fact": "Changing a ClientSettings decoding default does not reach existing installs. DesktopClientSettings encodes the whole ClientSettingsSchema and writes it to <runtime home>/userdata/client-settings.json, so a saved document carries every schema field with an explicit value, including keys the user never touched; observed live, one such document held all 44 schema fields. A withDecodingDefault change therefore only affects installs whose stored document predates the key, and retired keys linger in older files. To move a default for everyone, retire the key and add a fresh one, because decoding drops unknown keys: contextWindowIndicatorEnabled replaced contextWindowMeterEnabled, and legacySidebarEnabled replaced sidebarV2Enabled plus sidebarV2ConfiguredByUser.", "source_commits": ["8d5acb2dfd3e3e899e977d5c964eb99d2f3c3ee1", "75afe0ae0e42a815af01dbdb89b55b317bbc2418"], "source_paths": ["packages/contracts/src/settings.ts", "packages/contracts/src/settings.test.ts", "apps/desktop/src/settings/DesktopClientSettings.ts"], "supersedes": []}
{"id": "2026-09-10-desktop-update-track-is-a-build-property", "recorded_at": "2026-09-10T09:05:00Z", "scope": "desktop/updates", "fact": "A desktop build's update track is fixed by the build and cannot be changed at runtime. Stable and nightly are separate applications: appId com.pylon.code vs com.pylon.code.nightly, productName Pylon (Alpha) vs Pylon (Nightly), runtime home ~/.pylon-code vs ~/.pylon-code-nightly. On macOS an in-place cross-track update is impossible, not merely undesirable: Squirrel.Mac validates the downloaded bundle against the running app's designated requirement, which pins the bundle identifier (verified on installed builds via `codesign -d --requirements -`), so the nightly bundle can never satisfy the stable app's requirement. A refused install is also silent \u2014 electron-updater's MacUpdater.quitAndInstall registers a listener for a native update-downloaded event that never fires, after DesktopUpdates has already stopped every backend, which presents as the app hanging on restart. Squirrel only ever replaces the running bundle, so no second app is ever produced. DesktopUpdates therefore derives its channel from resolveDefaultDesktopUpdateChannel(appVersion); there is no setChannel on the service, IPC, preload bridge, or contract, and DesktopSettings carries no updateChannel. Documents written by the retired Update track selector still decode (excess keys are ignored) and shed updateChannel and updateChannelConfiguredByUser on the next write, so a stable install that was switched to nightly returns to the stable feed on upgrade. Moving between tracks means installing the other application; both run at once.", "source_commits": ["ece3d1c6d342cc85c1ec36b53605d3a00e55ee79"], "source_paths": ["apps/desktop/src/updates/DesktopUpdates.ts", "apps/desktop/src/updates/updateChannels.ts", "apps/desktop/src/settings/DesktopAppSettings.ts", "apps/web/src/components/settings/SettingsPanels.tsx", "scripts/build-desktop-artifact.ts", "docs/user/updating.md"], "supersedes": []}
2 changes: 1 addition & 1 deletion apps/desktop/src/app/DesktopEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ const make = Effect.fn("desktop.environment.make")(function* (
appImagePath: config.appImagePath,
userDataDirName,
legacyUserDataDirName,
defaultDesktopSettings: DesktopAppSettings.resolveDefaultDesktopSettings(input.appVersion),
defaultDesktopSettings: DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
runtimeInfo: resolveDesktopRuntimeInfo({
platform: input.platform,
processArch: input.processArch,
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/backend/DesktopServerExposure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ describe("DesktopServerExposure", () => {
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
setServerExposureMode: () => Effect.fail(settingsFailure),
setTailscaleServe: () => Effect.fail(settingsFailure),
setUpdateChannel: () => Effect.die("unexpected update channel change"),
setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"),
setWslDistro: () => Effect.die("unexpected WSL distro change"),
setWslOnly: () => Effect.die("unexpected WSL-only toggle"),
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
downloadUpdate,
getUpdateState,
installUpdate,
setUpdateChannel,
} from "./methods/updates.ts";
import {
getAppBranding,
Expand Down Expand Up @@ -98,7 +97,6 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(openSystemSettings);
yield* ipc.handle(probeRemoteEditors);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
yield* ipc.handle(installUpdate);
yield* ipc.handle(checkForUpdate);
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export const DESKTOP_APP_ACTIVATION_COMPLETE_CHANNEL = "desktop:app-activation-c
export const DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL = "desktop:app-activation-request";
export const UPDATE_STATE_CHANNEL = "desktop:update-state";
export const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
export const UPDATE_SET_CHANNEL_CHANNEL = "desktop:update-set-channel";
export const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
export const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
export const UPDATE_CHECK_CHANNEL = "desktop:update-check";
Expand Down
11 changes: 0 additions & 11 deletions apps/desktop/src/ipc/methods/updates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
DesktopUpdateActionResultSchema,
DesktopUpdateChannelSchema,
DesktopUpdateCheckResultSchema,
DesktopUpdateStateSchema,
} from "@t3tools/contracts";
Expand All @@ -21,16 +20,6 @@ export const getUpdateState = DesktopIpc.makeIpcMethod({
}),
});

export const setUpdateChannel = DesktopIpc.makeIpcMethod({
channel: IpcChannels.UPDATE_SET_CHANNEL_CHANNEL,
payload: DesktopUpdateChannelSchema,
result: DesktopUpdateStateSchema,
handler: Effect.fn("desktop.ipc.updates.setChannel")(function* (channel) {
const updates = yield* DesktopUpdates.DesktopUpdates;
return yield* updates.setChannel(channel);
}),
});

export const downloadUpdate = DesktopIpc.makeIpcMethod({
channel: IpcChannels.UPDATE_DOWNLOAD_CHANNEL,
payload: Schema.Void,
Expand Down
9 changes: 3 additions & 6 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import * as DesktopServerExposure from "./backend/DesktopServerExposure.ts";
import * as DesktopClientSettings from "./settings/DesktopClientSettings.ts";
import * as DesktopSavedEnvironments from "./settings/DesktopSavedEnvironments.ts";
import * as DesktopAppSettings from "./settings/DesktopAppSettings.ts";
import { resolveDefaultDesktopUpdateChannel } from "./updates/updateChannels.ts";
import * as DesktopPreReadyPlatform from "./app/DesktopPreReadyPlatform.ts";
import * as DesktopShellEnvironment from "./shell/DesktopShellEnvironment.ts";
import * as DesktopSshEnvironment from "./ssh/DesktopSshEnvironment.ts";
Expand Down Expand Up @@ -86,7 +87,6 @@ const desktopEnvironmentLayer = Layer.unwrap(

const resolveDesktopSshCliRunner = (
environment: DesktopEnvironment.DesktopEnvironment["Service"],
settings: DesktopAppSettings.DesktopSettings,
): RemoteT3RunnerOptions => {
const devRemoteEntryPath = Option.getOrUndefined(environment.devRemoteT3ServerEntryPath);
if (environment.isDevelopment && devRemoteEntryPath !== undefined) {
Expand All @@ -98,7 +98,7 @@ const resolveDesktopSshCliRunner = (
return {
packageSpec: resolveRemoteT3CliPackageSpec({
appVersion: environment.appVersion,
updateChannel: settings.updateChannel,
updateChannel: resolveDefaultDesktopUpdateChannel(environment.appVersion),
isDevelopment: environment.isDevelopment,
}),
nodeEngineRange: serverPackageJson.engines.node,
Expand All @@ -108,11 +108,8 @@ const resolveDesktopSshCliRunner = (
const desktopSshEnvironmentLayer = Layer.unwrap(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
return DesktopSshEnvironment.layer({
resolveCliRunner: settings.get.pipe(
Effect.map((currentSettings) => resolveDesktopSshCliRunner(environment, currentSettings)),
),
resolveCliRunner: Effect.sync(() => resolveDesktopSshCliRunner(environment)),
});
}),
);
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,6 @@ contextBridge.exposeInMainWorld("desktopBridge", {
};
},
getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL),
setUpdateChannel: (channel) =>
ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel),
checkForUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_CHECK_CHANNEL),
downloadUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_DOWNLOAD_CHANNEL),
installUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_INSTALL_CHANNEL),
Expand Down
77 changes: 33 additions & 44 deletions apps/desktop/src/settings/DesktopAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const DesktopSettingsPatch = Schema.Struct({
serverExposureMode: Schema.optionalKey(Schema.Literals(["local-only", "network-accessible"])),
tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),
tailscaleServePort: Schema.optionalKey(Schema.Number),
// Retired keys, kept here so tests can still write the legacy documents that
// real installs have on disk.
updateChannel: Schema.optionalKey(Schema.Literals(["latest", "nightly"])),
updateChannelConfiguredByUser: Schema.optionalKey(Schema.Boolean),
wslBackendEnabled: Schema.optionalKey(Schema.Boolean),
Expand Down Expand Up @@ -101,25 +103,6 @@ describe("DesktopSettings", () => {
),
);

it("defaults packaged nightly builds to the nightly update channel", () => {
assert.deepEqual(
DesktopAppSettings.resolveDefaultDesktopSettings("0.0.17-nightly.20260415.1"),
{
linuxPasswordStore: "auto",
mainWindowBounds: null,
mainWindowMaximized: false,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
updateChannel: "nightly",
updateChannelConfiguredByUser: false,
wslBackendEnabled: false,
wslOnly: false,
wslDistro: null,
} satisfies DesktopAppSettings.DesktopSettings,
);
});

it.effect("loads persisted settings and applies semantic updates", () =>
withSettings(
Effect.gen(function* () {
Expand All @@ -129,8 +112,6 @@ describe("DesktopSettings", () => {
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
updateChannel: "latest",
updateChannelConfiguredByUser: true,
});

assert.deepEqual(yield* settings.load, {
Expand All @@ -140,8 +121,6 @@ describe("DesktopSettings", () => {
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
updateChannel: "latest",
updateChannelConfiguredByUser: true,
wslBackendEnabled: false,
wslOnly: false,
wslDistro: null,
Expand All @@ -157,11 +136,38 @@ describe("DesktopSettings", () => {
});
assert.isTrue(tailscale.changed);
assert.equal(tailscale.settings.tailscaleServePort, 9443);
}),
),
);

const updateChannel = yield* settings.setUpdateChannel("nightly");
assert.isTrue(updateChannel.changed);
assert.equal(updateChannel.settings.updateChannel, "nightly");
assert.equal(updateChannel.settings.updateChannelConfiguredByUser, true);
// Anyone who used the old Update track selector has `updateChannel` on disk.
// Honouring it would leave a stable install polling the nightly feed for an
// update it can never install, so the key has to stay ignored.
it.effect("ignores a retired update channel preference and drops it on write", () =>
withSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* writeSettingsPatch({
tailscaleServePort: 8443,
updateChannel: "nightly",
updateChannelConfiguredByUser: true,
});

const loaded = yield* settings.load;
assert.deepEqual(loaded, {
...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
tailscaleServePort: 8443,
} satisfies DesktopAppSettings.DesktopSettings);
assert.notProperty(loaded, "updateChannel");

yield* settings.setServerExposureMode("network-accessible");
const persisted = yield* decodeDesktopSettingsPatch(
yield* fileSystem.readFileString(environment.desktopSettingsPath),
);
assert.isUndefined(persisted.updateChannel);
assert.isUndefined(persisted.updateChannelConfiguredByUser);
}),
),
);
Expand Down Expand Up @@ -200,10 +206,6 @@ describe("DesktopSettings", () => {
port: Option.none(),
});
assert.isFalse(tailscale.changed);

const updateChannel = yield* settings.setUpdateChannel("latest");
assert.isFalse(updateChannel.changed);
assert.equal(updateChannel.settings.updateChannelConfiguredByUser, false);
}),
),
);
Expand Down Expand Up @@ -247,8 +249,6 @@ describe("DesktopSettings", () => {
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
updateChannel: "latest",
updateChannelConfiguredByUser: false,
wslBackendEnabled: false,
wslOnly: false,
wslDistro: null,
Expand Down Expand Up @@ -303,8 +303,6 @@ describe("DesktopSettings", () => {
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
updateChannel: "nightly",
updateChannelConfiguredByUser: true,
wslBackendEnabled: false,
wslOnly: false,
wslDistro: null,
Expand Down Expand Up @@ -341,7 +339,6 @@ describe("DesktopSettings", () => {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* writeSettingsPatch({
serverExposureMode: "local-only",
updateChannel: "latest",
});

assert.deepEqual(yield* settings.load, {
Expand All @@ -351,8 +348,6 @@ describe("DesktopSettings", () => {
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
updateChannel: "nightly",
updateChannelConfiguredByUser: false,
wslBackendEnabled: false,
wslOnly: false,
wslDistro: null,
Expand All @@ -368,8 +363,6 @@ describe("DesktopSettings", () => {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* writeSettingsPatch({
serverExposureMode: "local-only",
updateChannel: "latest",
updateChannelConfiguredByUser: true,
});

assert.deepEqual(yield* settings.load, {
Expand All @@ -379,8 +372,6 @@ describe("DesktopSettings", () => {
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
updateChannel: "latest",
updateChannelConfiguredByUser: true,
wslBackendEnabled: false,
wslOnly: false,
wslDistro: null,
Expand All @@ -406,8 +397,6 @@ describe("DesktopSettings", () => {
serverExposureMode: "local-only",
tailscaleServeEnabled: true,
tailscaleServePort: 443,
updateChannel: "latest",
updateChannelConfiguredByUser: false,
wslBackendEnabled: false,
wslOnly: false,
wslDistro: null,
Expand Down
Loading
Loading