Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1345ed2
FIX: harden claude test isolation in desktop tests
aponamarev Mar 31, 2026
1397167
FEAT: persist symphony loops on reboot
aponamarev Mar 31, 2026
d658666
Updated package version
aponamarev Mar 31, 2026
12e322e
Merge remote-tracking branch 'origin/main' into feat/persist-loops-on…
aponamarev Mar 31, 2026
047bd5b
FEAT: restore recovered loop auth and diagnostics
aponamarev Mar 31, 2026
8490471
FEAT: drop unused runner-token refresh from boot recovery
aponamarev Mar 31, 2026
5d00207
FEAT: slim reboot recovery completion path
aponamarev Mar 31, 2026
91fffda
FEAT-PERSIST-LOOPS: add encrypted LoopTokenStore
aponamarev Mar 31, 2026
eef5e5a
FEAT-PERSIST-LOOPS: handle recovery finalization by terminal status
aponamarev Mar 31, 2026
10fc409
FEAT-PERSIST-LOOPS: remove loop token fallback paths
aponamarev Mar 31, 2026
da1da3a
FEAT-PERSIST-LOOPS: remove job path backfill
aponamarev Mar 31, 2026
774ab97
Updated comments
aponamarev Mar 31, 2026
a667099
Made loop token store require
aponamarev Mar 31, 2026
050dbb6
Fixed the order of loop registration in re-attachment process
aponamarev Mar 31, 2026
251d824
refactored finalizeLoopFromRuntime
aponamarev Mar 31, 2026
afa7538
FEAT-PERSIST-LOOPS: fix cancel-pending finalization
aponamarev Mar 31, 2026
0edea51
PERSIST-LOOPS: fix output tailer replay-safe offsets
aponamarev Mar 31, 2026
c1d0e24
FEAT-PERSIST-LOOPS: defer boot dead-job finalization
aponamarev Mar 31, 2026
ca667a2
- Remove artifact/event failure guard that kept tokens for a boot ret…
aponamarev Mar 31, 2026
2e555f0
Refactored createTestLoopTokenSafeStorage
aponamarev Mar 31, 2026
59d37ba
PERSIST-LOOPS: configurable boot-recovery watcher poll
aponamarev Mar 31, 2026
9676e07
PERSIST-LOOPS: retry dead-job cloud finalization across boots
aponamarev Apr 1, 2026
7ea2bbb
PERSIST-LOOPS: retry output tailer on auth failures
aponamarev Apr 1, 2026
dff4af4
PERSIST-LOOPS: clean up loop tokens on all exit paths
aponamarev Apr 1, 2026
b04aeca
PERSIST-LOOPS: restore finalizer completed-event metadata
aponamarev Apr 1, 2026
e17e859
Merge remote-tracking branch 'origin/main' into feat/persist-loops-on…
aponamarev Apr 1, 2026
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 apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Override the default service URLs. Useful for local development or staging envir
|---|---|
| `CLOSEDLOOP_TAILER_POLL_MS` | Poll interval in milliseconds for the output tailer (default determined at runtime). |
| `CLOSEDLOOP_TAILER_THROTTLE_MS` | Throttle interval in milliseconds for the output tailer (default determined at runtime). |
| `CLOSEDLOOP_WATCHER_POLL_MS` | Poll interval in milliseconds for the boot-recovery live-loop PID watcher (default 3000). |

### Passed to Child Processes

Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "desktop",
"version": "0.9.11",
"version": "0.10.0",
"description": "ClosedLoop Desktop",
"author": "ClosedLoop AI <support@closedloop.ai>",
"private": true,
Expand All @@ -9,7 +9,7 @@
"scripts": {
"dev": "pnpm build && ELECTRON_BIN=$(bash scripts/patch-electron-plist.sh | tail -1) && \"$ELECTRON_BIN\" dist/main/index.js",
"start": "ELECTRON_BIN=$(bash scripts/patch-electron-plist.sh | tail -1) && \"$ELECTRON_BIN\" dist/main/index.js",
"prebuild": "node -e \"const{execSync:e}=require('child_process'),{writeFileSync:w}=require('fs');const h=e('git rev-parse HEAD').toString().trim();w('src/shared/build-info.ts','// AUTO-GENERATED do not edit\\nexport const BUILD_COMMIT_HASH = \\\"'+h+'\\\";\\n');\"",
"prebuild": "node -e \"const{execSync:e}=require('child_process'),{writeFileSync:w}=require('fs');const h=e('git rev-parse HEAD').toString().trim();w('src/shared/build-info.ts','// AUTO-GENERATED \u2014 do not edit\\nexport const BUILD_COMMIT_HASH = \\\"'+h+'\\\";\\n');\"",
"build": "pnpm prebuild && tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "eslint src/",
Expand Down
32 changes: 28 additions & 4 deletions apps/desktop/src/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { shouldAutoApprove, OPERATION_RISK_TIERS } from "./approval-policy.js";
import { gatewayLog, isNetworkError } from "./gateway-logger.js";
import { ActivityLogStore } from "./activity-log-store.js";
import { ApprovalStore } from "./approval-store.js";
import { JobStore, isTerminalJobStatus } from "./job-store.js";
import { JobStore, isTerminalJobStatus, type LocalJob } from "./job-store.js";
import { Observability } from "./observability.js";
import type {
GatewayApprovalRequest,
Expand All @@ -65,6 +65,8 @@ import type { RetrySpawnDeps } from "./spawn-retry.js";
import pkg from "electron-updater";
const { autoUpdater } = pkg;
import { BUILD_COMMIT_HASH } from "../shared/build-info.js";
import { BootRecoveryService } from "./boot-recovery.js";
import { LoopTokenStore } from "./loop-token-store.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const execFileAsync = promisify(execFile);
Expand All @@ -73,6 +75,7 @@ const UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
export class DesktopApplication {
private readonly settingsStore: SettingsStore;
private readonly apiKeyStore: ApiKeyStore;
private readonly loopTokenStore: LoopTokenStore;
private readonly tray: DesktopTray;
private readonly desktopWindow: DesktopWindow;
private readonly server: DesktopGatewayServer;
Expand All @@ -82,6 +85,7 @@ export class DesktopApplication {
private readonly approvalStore: ApprovalStore;
private readonly jobStore: JobStore;
private readonly recovery: GatewayRecoveryManager;
private readonly bootRecovery: BootRecoveryService;
private readonly gatewayAuthToken: string;
private readonly sessionStore: LocalSessionStore;
private shuttingDown = false;
Expand All @@ -106,6 +110,7 @@ export class DesktopApplication {
this.cloudConnectionEnabled =
this.settingsStore.getCloudConnectionEnabled();
this.apiKeyStore = new ApiKeyStore();
this.loopTokenStore = new LoopTokenStore();
this.tray = new DesktopTray();
this.desktopWindow = new DesktopWindow();
this.activityLog = new ActivityLogStore();
Expand Down Expand Up @@ -151,6 +156,7 @@ export class DesktopApplication {
this.isProdOriginsOnly(),
this.jobStore,
() => this.recovery.onUnexpectedClose(),
this.loopTokenStore,
retrySpawnDeps,
);
this.commandExecutor = new CloudCommandExecutor({
Expand Down Expand Up @@ -260,6 +266,13 @@ export class DesktopApplication {
isShuttingDown: () => this.shuttingDown,
isPaused: () => this.cloudCommandsPaused,
});
this.bootRecovery = new BootRecoveryService({
jobStore: this.jobStore,
telemetry: Observability.getTelemetryEmitter(),
getApiKey: () => this.apiKeyStore.getApiKey(),
getApiOrigin: () => this.settingsStore.getApiOrigin(),
loopTokenStore: this.loopTokenStore,
});
this.registerIpcHandlers();
}

Expand All @@ -284,7 +297,8 @@ export class DesktopApplication {

gatewayLog.setVerbose(this.settingsStore.getAll().verboseLogging);
this.migrateLegacyData();
this.reconcileJobStore();
const deadJobs = this.reconcileJobStore();
await this.bootRecovery.reattachLiveJobs();

const bootSandbox = this.settingsStore.getSandboxBaseDirectory();
if (bootSandbox?.trim()) {
Expand All @@ -301,6 +315,14 @@ export class DesktopApplication {
this.refreshTrayState(
`Serving on localhost:${this.server.getActivePort()} | relay=${configuredOrigins.relayOrigin} api=${configuredOrigins.apiOrigin} web=${configuredOrigins.webAppOrigin}`,
);
void this.bootRecovery
.startDeadJobFinalization(deadJobs)
.catch((err: unknown) => {
gatewayLog.warn(
"boot-recovery",
`Background dead-loop finalization failed: ${err instanceof Error ? err.message : String(err)}`,
);
});

if (this.cloudConnectionEnabled) {
void this.cloudSocket.start();
Expand Down Expand Up @@ -385,6 +407,8 @@ export class DesktopApplication {
}

this.shuttingDown = true;
this.bootRecovery.dispose();
await this.bootRecovery.quiesce(1_000);
await Observability.shutdown();
return runShutdownSequence({
updateCheckTimer: this.updateCheckTimer,
Expand Down Expand Up @@ -860,8 +884,8 @@ export class DesktopApplication {
app.exit(0);
}

private reconcileJobStore(): void {
this.jobStore.reconcile((job) => {
private reconcileJobStore(): LocalJob[] {
return this.jobStore.reconcile((job) => {
const now = new Date().toISOString();

// If no PID, we cannot verify liveness
Expand Down
Loading
Loading