@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +26 -5
- package/README.md +49 -7
- package/docs/architecture.md +129 -1
- package/package.json +15 -15
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +68 -4
- package/packages/extension/src/bridge.ts +79 -11
- package/packages/extension/src/command-handler.ts +95 -15
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +74 -63
- package/packages/extension/src/provider-register.ts +16 -9
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/session-sync.ts +10 -1
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +8 -7
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +2 -2
- package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
- package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
- package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
- package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
- package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
- package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
- package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
- package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
- package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
- package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
- package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
- package/packages/server/src/auth-plugin.ts +3 -0
- package/packages/server/src/bootstrap-state.ts +10 -0
- package/packages/server/src/browser-gateway.ts +27 -10
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +62 -82
- package/packages/server/src/config-api.ts +14 -2
- package/packages/server/src/directory-service.ts +106 -4
- package/packages/server/src/event-wiring.ts +90 -6
- package/packages/server/src/headless-pid-registry.ts +344 -37
- package/packages/server/src/legacy-pi-cleanup.ts +151 -0
- package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
- package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
- package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
- package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
- package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
- package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
- package/packages/server/src/model-proxy/api-key-store.ts +87 -0
- package/packages/server/src/model-proxy/auth-gate.ts +116 -0
- package/packages/server/src/model-proxy/concurrency.ts +76 -0
- package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
- package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
- package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
- package/packages/server/src/model-proxy/convert/index.ts +8 -0
- package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
- package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
- package/packages/server/src/model-proxy/convert/types.ts +70 -0
- package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
- package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
- package/packages/server/src/model-proxy/internal-registry.ts +157 -0
- package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
- package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
- package/packages/server/src/model-proxy/request-log.ts +53 -0
- package/packages/server/src/model-proxy/streamer.ts +59 -0
- package/packages/server/src/openspec-group-store.ts +490 -0
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +16 -6
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +4 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +182 -11
- package/packages/server/src/provider-auth-storage.ts +29 -47
- package/packages/server/src/provider-catalogue-cache.ts +24 -18
- package/packages/server/src/restart-helper.ts +17 -16
- package/packages/server/src/routes/bootstrap-routes.ts +37 -0
- package/packages/server/src/routes/jj-routes.ts +3 -0
- package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
- package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
- package/packages/server/src/routes/model-proxy-routes.ts +330 -0
- package/packages/server/src/routes/openspec-group-routes.ts +231 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -1
- package/packages/server/src/routes/provider-routes.ts +28 -5
- package/packages/server/src/routes/system-routes.ts +44 -2
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
- package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
- package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
- package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
- package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
- package/packages/server/src/server.ts +254 -60
- package/packages/server/src/session-api.ts +63 -4
- package/packages/server/src/session-discovery.ts +1 -1
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/spawn-register-watchdog.ts +62 -7
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/tunnel-watchdog.ts +230 -0
- package/packages/server/src/tunnel.ts +5 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
- package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
- package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
- package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +70 -0
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +172 -2
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
- package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
- package/packages/shared/src/platform/binary-lookup.ts +204 -0
- package/packages/shared/src/platform/node-spawn.ts +71 -26
- package/packages/shared/src/protocol.ts +27 -1
- package/packages/shared/src/recommended-extensions.ts +18 -0
- package/packages/shared/src/rest-api.ts +219 -1
- package/packages/shared/src/server-launcher.ts +277 -0
- package/packages/shared/src/skill-block-parser.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/tool-registry/definitions.ts +15 -3
- package/packages/shared/src/types.ts +62 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
- package/packages/shared/src/resolve-jiti.ts +0 -102
|
@@ -18,6 +18,7 @@ import { createPreferencesStore, type PreferencesStore } from "./preferences-sto
|
|
|
18
18
|
import { createMetaPersistence, type MetaPersistence } from "./meta-persistence.js";
|
|
19
19
|
import { createSessionOrderManager, type SessionOrderManager } from "./session-order-manager.js";
|
|
20
20
|
import { createPendingForkRegistry, type PendingForkRegistry } from "./pending-fork-registry.js";
|
|
21
|
+
import { createPendingClientCorrelations } from "./pending-client-correlations.js";
|
|
21
22
|
import { createPendingAttachRegistry } from "./pending-attach-registry.js";
|
|
22
23
|
import { createPendingResumeIntentRegistry } from "./pending-resume-intent-registry.js";
|
|
23
24
|
import { applyReattachPolicy } from "./reattach-placement.js";
|
|
@@ -34,6 +35,7 @@ import { discoverAndBroadcastSessions } from "./session-bootstrap.js";
|
|
|
34
35
|
import { scanAllSessions } from "./session-scanner.js";
|
|
35
36
|
import { needsMigration, runMigration } from "./migrate-persistence.js";
|
|
36
37
|
import { detectZrokBinary, cleanupStaleZrok, createTunnel, deleteTunnel, scavengeOrphanZrokProcesses, getTunnelUrl } from "./tunnel.js";
|
|
38
|
+
import { startTunnelWatchdog, stopTunnelWatchdog } from "./tunnel-watchdog.js";
|
|
37
39
|
import { registerAuthPlugin, validateWsUpgrade } from "./auth-plugin.js";
|
|
38
40
|
import { findBundledExtension, registerBridgeExtension } from "@blackbelt-technology/pi-dashboard-shared/bridge-register.js";
|
|
39
41
|
import { createNetworkGuard, isLoopback, isBypassedHost } from "./localhost-guard.js";
|
|
@@ -44,18 +46,22 @@ import { registerSessionRoutes } from "./routes/session-routes.js";
|
|
|
44
46
|
import { registerGitRoutes } from "./routes/git-routes.js";
|
|
45
47
|
import { registerFileRoutes } from "./routes/file-routes.js";
|
|
46
48
|
import { registerOpenSpecRoutes } from "./routes/openspec-routes.js";
|
|
49
|
+
import { registerOpenSpecGroupRoutes } from "./routes/openspec-group-routes.js";
|
|
50
|
+
import { createOpenSpecGroupStore, joinGroupIdsToOpenSpecData } from "./openspec-group-store.js";
|
|
47
51
|
import { registerSystemRoutes } from "./routes/system-routes.js";
|
|
48
52
|
import { registerDoctorRoutes } from "./routes/doctor-routes.js";
|
|
49
53
|
import { registerProviderAuthRoutes } from "./routes/provider-auth-routes.js";
|
|
50
54
|
import { registerPackageRoutes } from "./routes/package-routes.js";
|
|
51
55
|
import { registerRecommendedRoutes, invalidateRecommendedCache } from "./routes/recommended-routes.js";
|
|
52
56
|
import { registerPiCoreRoutes } from "./routes/pi-core-routes.js";
|
|
57
|
+
import { registerPiChangelogRoutes } from "./routes/pi-changelog-routes.js";
|
|
53
58
|
import { PiCoreChecker } from "./pi-core-checker.js";
|
|
54
59
|
import { PiCoreUpdater } from "./pi-core-updater.js";
|
|
55
60
|
import { registerToolRoutes } from "./routes/tool-routes.js";
|
|
56
61
|
import { registerJjRoutes } from "./routes/jj-routes.js";
|
|
57
62
|
import { registerBootstrapRoutes } from "./routes/bootstrap-routes.js";
|
|
58
63
|
import { createBootstrapState, type BootstrapStateStore } from "./bootstrap-state.js";
|
|
64
|
+
import { detectLegacyPiInstalls } from "./legacy-pi-cleanup.js";
|
|
59
65
|
import { createBootstrapQueue } from "./bootstrap-queue.js";
|
|
60
66
|
import { bootstrapInstall } from "@blackbelt-technology/pi-dashboard-shared/bootstrap-install.js";
|
|
61
67
|
import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
|
|
@@ -66,6 +72,12 @@ import { createEditorPidRegistry } from "./editor-pid-registry.js";
|
|
|
66
72
|
import { registerEditorRoutes } from "./routes/editor-routes.js";
|
|
67
73
|
import { registerKnownServersRoutes } from "./routes/known-servers-routes.js";
|
|
68
74
|
import { registerPluginConfigRoutes } from "./routes/plugin-config-routes.js";
|
|
75
|
+
import { createModelProxyAuthGate } from "./model-proxy/auth-gate.js";
|
|
76
|
+
import { registerModelProxyRoutes } from "./routes/model-proxy-routes.js";
|
|
77
|
+
import { registerModelProxyApiKeyRoutes } from "./routes/model-proxy-api-key-routes.js";
|
|
78
|
+
import { registerModelProxyRefreshRoutes } from "./routes/model-proxy-refresh-routes.js";
|
|
79
|
+
import { getModelRegistry, getStreamSimpleFn } from "./model-proxy/registry-singleton.js";
|
|
80
|
+
import { writeConfigPartial } from "./config-api.js";
|
|
69
81
|
import { loadServerEntries, discoverPlugins, getPluginStatusStore } from "@blackbelt-technology/dashboard-plugin-runtime/server";
|
|
70
82
|
import { createServerPluginContext } from "@blackbelt-technology/dashboard-plugin-runtime/server";
|
|
71
83
|
import { getPluginConfig as getPluginConfigFromFile } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
@@ -81,6 +93,12 @@ export interface ServerConfig {
|
|
|
81
93
|
shutdownIdleSeconds: number;
|
|
82
94
|
tunnel: boolean;
|
|
83
95
|
tunnelReservedToken?: string;
|
|
96
|
+
tunnelWatchdog?: {
|
|
97
|
+
enabled: boolean;
|
|
98
|
+
intervalMs: number;
|
|
99
|
+
failureThreshold: number;
|
|
100
|
+
probeTimeoutMs: number;
|
|
101
|
+
};
|
|
84
102
|
authConfig?: AuthConfig;
|
|
85
103
|
/** Override WS ping interval for pi-gateway (ms). Default 60000. Set 0 to disable. */
|
|
86
104
|
pingInterval?: number;
|
|
@@ -269,6 +287,10 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
269
287
|
const metaPersistence = createMetaPersistence();
|
|
270
288
|
const sessionOrderManager = createSessionOrderManager(preferencesStore);
|
|
271
289
|
const pendingForkRegistry = createPendingForkRegistry();
|
|
290
|
+
// Maps spawnToken → originating browser requestId. Surfaced as
|
|
291
|
+
// session_added.spawnRequestId so the client can auto-select / dismiss
|
|
292
|
+
// its placeholder by exact correlation. See change: spawn-correlation-token.
|
|
293
|
+
const pendingClientCorrelations = createPendingClientCorrelations();
|
|
272
294
|
|
|
273
295
|
// Restore sessions from per-session .meta.json files (scans ~/.pi/agent/sessions/)
|
|
274
296
|
const scanResult = scanAllSessions();
|
|
@@ -472,10 +494,32 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
472
494
|
knownSessionIds.add(s.id);
|
|
473
495
|
}
|
|
474
496
|
|
|
475
|
-
|
|
497
|
+
// Create the OpenSpec change-grouping store BEFORE the directory-service so
|
|
498
|
+
// the latter can join `groupId` into every `OpenSpecChange` it produces.
|
|
499
|
+
// See change: add-openspec-change-grouping (task 4.2).
|
|
500
|
+
const openspecGroupStore = createOpenSpecGroupStore();
|
|
501
|
+
|
|
502
|
+
const directoryService = createDirectoryService(
|
|
503
|
+
preferencesStore,
|
|
504
|
+
sessionManager,
|
|
505
|
+
config.openspec,
|
|
506
|
+
{
|
|
507
|
+
enrichOpenSpecData: async (cwd, data) => {
|
|
508
|
+
try {
|
|
509
|
+
const file = await openspecGroupStore.read(cwd);
|
|
510
|
+
return joinGroupIdsToOpenSpecData(data, file.assignments);
|
|
511
|
+
} catch {
|
|
512
|
+
// Bad file (e.g., unsupported schemaVersion) — fall back to unjoined.
|
|
513
|
+
return data;
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
);
|
|
476
518
|
|
|
477
519
|
// mDNS peer discovery state
|
|
478
520
|
let mdnsBrowser: DashboardBrowser | null = null;
|
|
521
|
+
// Optional second-port Fastify instance for model proxy (/v1/*)
|
|
522
|
+
let secondFastify: Awaited<ReturnType<typeof import("fastify").default>> | null = null;
|
|
479
523
|
const peerServers = new Map<string, DiscoveredServer>();
|
|
480
524
|
|
|
481
525
|
const piGateway = createPiGateway(sessionManager, {
|
|
@@ -521,7 +565,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
521
565
|
},
|
|
522
566
|
});
|
|
523
567
|
|
|
524
|
-
const browserGateway = createBrowserGateway(sessionManager, eventStore, piGateway, undefined, pendingForkRegistry, sessionOrderManager, preferencesStore, directoryService, terminalManager, pendingDashboardSpawns, config.maxWsBufferBytes, pendingAttachRegistry, pendingResumeIntents);
|
|
568
|
+
const browserGateway = createBrowserGateway(sessionManager, eventStore, piGateway, undefined, pendingForkRegistry, sessionOrderManager, preferencesStore, directoryService, terminalManager, pendingDashboardSpawns, config.maxWsBufferBytes, pendingAttachRegistry, pendingResumeIntents, pendingClientCorrelations);
|
|
525
569
|
|
|
526
570
|
// Resolve package version once at startup
|
|
527
571
|
const __require = createRequire(import.meta.url);
|
|
@@ -557,6 +601,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
557
601
|
pendingDashboardSpawns,
|
|
558
602
|
pendingAttachRegistry,
|
|
559
603
|
viewedSessionTracker: browserGateway.viewedSessionTracker,
|
|
604
|
+
pendingClientCorrelations,
|
|
560
605
|
});
|
|
561
606
|
|
|
562
607
|
// Auto-shutdown idle timer
|
|
@@ -649,6 +694,14 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
649
694
|
// routes can gate spawn operations on bootstrap status.
|
|
650
695
|
// See change: unified-bootstrap-install.
|
|
651
696
|
const bootstrapState = createBootstrapState();
|
|
697
|
+
// Scan for legacy `@mariozechner/pi-coding-agent` installs so the UI can
|
|
698
|
+
// offer a one-click cleanup. See: legacy-pi-cleanup.ts. Best-effort —
|
|
699
|
+
// detection failure is non-fatal (returns []).
|
|
700
|
+
try {
|
|
701
|
+
bootstrapState.set({ legacyPiInstalls: detectLegacyPiInstalls() });
|
|
702
|
+
} catch (err: any) {
|
|
703
|
+
console.warn("[legacy-pi] detection failed:", err?.message ?? err);
|
|
704
|
+
}
|
|
652
705
|
const bootstrapQueue = createBootstrapQueue();
|
|
653
706
|
// Centralized post-install repair: full ToolRegistry rescan +
|
|
654
707
|
// OpenSpec / pi-resources force-refresh on every `installing → ready`
|
|
@@ -688,6 +741,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
688
741
|
bootstrapState,
|
|
689
742
|
bootstrapQueue,
|
|
690
743
|
pendingResumeIntents,
|
|
744
|
+
pendingAttachRegistry,
|
|
691
745
|
});
|
|
692
746
|
|
|
693
747
|
// Register route modules
|
|
@@ -708,6 +762,29 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
708
762
|
if (data) browserGateway.broadcastToAll({ type: "openspec_update", cwd, data });
|
|
709
763
|
},
|
|
710
764
|
});
|
|
765
|
+
// OpenSpec change-grouping routes (store created earlier next to
|
|
766
|
+
// directory-service so the join can run during polls).
|
|
767
|
+
// See change: add-openspec-change-grouping.
|
|
768
|
+
openspecGroupStore.subscribe((cwd, payload) => {
|
|
769
|
+
browserGateway.broadcastToAll({
|
|
770
|
+
type: "openspec_groups_update",
|
|
771
|
+
cwd,
|
|
772
|
+
groups: payload.groups,
|
|
773
|
+
assignments: payload.assignments,
|
|
774
|
+
});
|
|
775
|
+
// Refresh OpenSpecData so the joined `groupId` field reflects the new
|
|
776
|
+
// assignments on subscribers that don't consume `openspec_groups_update`
|
|
777
|
+
// directly. Fire-and-forget; failures are logged inside refreshOpenSpec.
|
|
778
|
+
directoryService.refreshOpenSpec(cwd).then((data) => {
|
|
779
|
+
browserGateway.broadcastToAll({ type: "openspec_update", cwd, data });
|
|
780
|
+
}).catch(() => {});
|
|
781
|
+
});
|
|
782
|
+
registerOpenSpecGroupRoutes(fastify, {
|
|
783
|
+
sessionManager,
|
|
784
|
+
preferencesStore,
|
|
785
|
+
networkGuard,
|
|
786
|
+
store: openspecGroupStore,
|
|
787
|
+
});
|
|
711
788
|
registerSystemRoutes(fastify, { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version: pkgVersion, directoryService, piGateway, bootstrapState });
|
|
712
789
|
// GET /api/doctor — see change: doctor-rich-output (task 4.2). Auth-gated identically to /api/config.
|
|
713
790
|
registerDoctorRoutes(fastify);
|
|
@@ -721,11 +798,11 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
721
798
|
bootstrapState,
|
|
722
799
|
networkGuard,
|
|
723
800
|
triggerUpgradePi: async () => {
|
|
724
|
-
const packages = ["@
|
|
801
|
+
const packages = ["@earendil-works/pi-coding-agent"];
|
|
725
802
|
bootstrapState.setLastInstallPackages(packages);
|
|
726
803
|
bootstrapState.set({
|
|
727
804
|
status: "installing",
|
|
728
|
-
progress: { step: "@
|
|
805
|
+
progress: { step: "@earendil-works/pi-coding-agent", output: "starting upgrade…" },
|
|
729
806
|
error: undefined,
|
|
730
807
|
});
|
|
731
808
|
try {
|
|
@@ -779,7 +856,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
779
856
|
const prev = bootstrapState.getLastInstallPackages();
|
|
780
857
|
const packages = prev.length > 0
|
|
781
858
|
? prev
|
|
782
|
-
: ["@
|
|
859
|
+
: ["@earendil-works/pi-coding-agent", "@fission-ai/openspec"];
|
|
783
860
|
bootstrapState.set({
|
|
784
861
|
status: "installing",
|
|
785
862
|
progress: { step: "retry", output: `restarting install (${packages.length} pkg${packages.length === 1 ? "" : "s"})…` },
|
|
@@ -900,6 +977,8 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
900
977
|
message: event.message,
|
|
901
978
|
});
|
|
902
979
|
});
|
|
980
|
+
registerPiChangelogRoutes(fastify, { bootstrapState });
|
|
981
|
+
|
|
903
982
|
registerPiCoreRoutes(fastify, {
|
|
904
983
|
piCoreChecker,
|
|
905
984
|
piCoreUpdater,
|
|
@@ -938,7 +1017,51 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
938
1017
|
networkGuard,
|
|
939
1018
|
broadcast: (msg) => browserGateway.broadcast(msg),
|
|
940
1019
|
});
|
|
941
|
-
registerProviderRoutes(fastify, { networkGuard, piGateway, browserGateway });
|
|
1020
|
+
registerProviderRoutes(fastify, { networkGuard, piGateway, browserGateway, port: config.port });
|
|
1021
|
+
|
|
1022
|
+
// ── Model Proxy ───────────────────────────────────────────────────
|
|
1023
|
+
{
|
|
1024
|
+
const fullCfg = loadConfig();
|
|
1025
|
+
if (fullCfg.modelProxy.enabled) {
|
|
1026
|
+
// Register proxy auth gate (runs BEFORE JWT hook for /v1/* routes)
|
|
1027
|
+
const proxyAuthGate = createModelProxyAuthGate({
|
|
1028
|
+
getConfig: () => loadConfig().modelProxy,
|
|
1029
|
+
persistKeyUsage: (apiKeys) => {
|
|
1030
|
+
writeConfigPartial({ modelProxy: { apiKeys } });
|
|
1031
|
+
},
|
|
1032
|
+
});
|
|
1033
|
+
fastify.addHook("onRequest", proxyAuthGate);
|
|
1034
|
+
|
|
1035
|
+
// Register /v1/* routes
|
|
1036
|
+
registerModelProxyRoutes(fastify, {
|
|
1037
|
+
getConfig: () => loadConfig().modelProxy,
|
|
1038
|
+
getRegistry: async () => {
|
|
1039
|
+
try {
|
|
1040
|
+
return await getModelRegistry();
|
|
1041
|
+
} catch {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
streamSimple: (opts: any) => {
|
|
1046
|
+
const fn = getStreamSimpleFn();
|
|
1047
|
+
if (!fn) throw new Error("streamSimple not available");
|
|
1048
|
+
return fn(opts.model, { messages: opts.messages, system: opts.system, tools: opts.tools }, opts);
|
|
1049
|
+
},
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
// Register API key management routes (JWT-gated)
|
|
1053
|
+
registerModelProxyApiKeyRoutes(fastify, {
|
|
1054
|
+
networkGuard,
|
|
1055
|
+
getModelProxyConfig: () => loadConfig().modelProxy,
|
|
1056
|
+
writeModelProxyApiKeys: async (apiKeys) => {
|
|
1057
|
+
writeConfigPartial({ modelProxy: { apiKeys } });
|
|
1058
|
+
},
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// Register refresh route (JWT-gated)
|
|
1062
|
+
registerModelProxyRefreshRoutes(fastify);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
942
1065
|
|
|
943
1066
|
// Serve static files / SPA fallback.
|
|
944
1067
|
//
|
|
@@ -1071,12 +1194,82 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
1071
1194
|
// Clean up orphan headless processes from a previous server instance
|
|
1072
1195
|
browserGateway.headlessPidRegistry.cleanupOrphans();
|
|
1073
1196
|
|
|
1197
|
+
// Wire the singleton KeeperManager into the headless-pid registry so
|
|
1198
|
+
// `writeRpc` can forward `dispatch_extension_command` lines to the
|
|
1199
|
+
// session's keeper UDS, and so `cleanupKeeperOrphans` can reattach
|
|
1200
|
+
// surviving keepers after a server restart. Same instance the spawn
|
|
1201
|
+
// path uses. See change: add-rpc-stdin-dispatch-with-keeper-sidecar.
|
|
1202
|
+
try {
|
|
1203
|
+
const { getKeeperManager } = await import("./process-manager.js");
|
|
1204
|
+
browserGateway.headlessPidRegistry.setKeeperWriter(getKeeperManager());
|
|
1205
|
+
await browserGateway.headlessPidRegistry.cleanupKeeperOrphans();
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
console.warn("[dashboard] keeper-manager wire-up failed (RPC dispatch disabled):", err);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1074
1210
|
// Clean up orphan code-server processes from a previous server instance.
|
|
1075
1211
|
// Runs before fastify.listen, so no editor start request can race with the sweep.
|
|
1076
1212
|
await editorPidRegistry.cleanupOrphans();
|
|
1077
1213
|
|
|
1078
1214
|
piGateway.start(config.piPort);
|
|
1079
1215
|
|
|
1216
|
+
// Load plugin server entries BEFORE fastify.listen() so plugins can
|
|
1217
|
+
// register routes. Fastify rejects route registration after listen().
|
|
1218
|
+
// Failure-isolated per-plugin via loader; awaited so all routes are
|
|
1219
|
+
// mounted before requests can arrive.
|
|
1220
|
+
try {
|
|
1221
|
+
await loadServerEntries({
|
|
1222
|
+
isEnabled: (pluginId) => {
|
|
1223
|
+
const cfg = loadConfig();
|
|
1224
|
+
const pluginCfg = getPluginConfigFromFile(cfg, pluginId) as Record<string, unknown>;
|
|
1225
|
+
return pluginCfg.enabled !== false;
|
|
1226
|
+
},
|
|
1227
|
+
createContext: (plugin) => createServerPluginContext(
|
|
1228
|
+
{
|
|
1229
|
+
fastify,
|
|
1230
|
+
sessionManager: {
|
|
1231
|
+
listActive: () => sessionManager.listActive(),
|
|
1232
|
+
listAll: () => sessionManager.listAll(),
|
|
1233
|
+
getSession: (id: string) => sessionManager.get(id),
|
|
1234
|
+
},
|
|
1235
|
+
eventStore: {
|
|
1236
|
+
getEvents: (sessionId) => eventStore.getEvents(sessionId, 0),
|
|
1237
|
+
getLatestEvent: (sessionId) => {
|
|
1238
|
+
const events = eventStore.getEvents(sessionId, 0);
|
|
1239
|
+
return events.length > 0 ? events[events.length - 1] : undefined;
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
broadcastToSubscribers: (msg) => browserGateway.broadcast(msg as any),
|
|
1243
|
+
registerPiHandler: (_type, _handler) => {},
|
|
1244
|
+
registerBrowserHandler: (_type, _handler) => {},
|
|
1245
|
+
getPluginConfig: (id) => {
|
|
1246
|
+
const cfg = loadConfig();
|
|
1247
|
+
return getPluginConfigFromFile(cfg, id);
|
|
1248
|
+
},
|
|
1249
|
+
updatePluginConfig: async (id, partial) => {
|
|
1250
|
+
const cfg = loadConfig();
|
|
1251
|
+
const current = getPluginConfigFromFile(cfg, id);
|
|
1252
|
+
const merged = { ...current, ...partial };
|
|
1253
|
+
let rawConfig: Record<string, unknown> = {};
|
|
1254
|
+
try {
|
|
1255
|
+
const raw = (await import('node:fs')).default.readFileSync(CONFIG_FILE, 'utf-8');
|
|
1256
|
+
rawConfig = JSON.parse(raw);
|
|
1257
|
+
} catch { /* start fresh */ }
|
|
1258
|
+
rawConfig.plugins = { ...(rawConfig.plugins as Record<string, unknown> ?? {}), [id]: merged };
|
|
1259
|
+
const fs = (await import('node:fs')).default;
|
|
1260
|
+
const tmpFile = CONFIG_FILE + '.tmp.' + process.pid;
|
|
1261
|
+
fs.writeFileSync(tmpFile, JSON.stringify(rawConfig, null, 2) + '\n');
|
|
1262
|
+
fs.renameSync(tmpFile, CONFIG_FILE);
|
|
1263
|
+
browserGateway.broadcast({ type: 'plugin_config_update', id, config: merged } as any);
|
|
1264
|
+
},
|
|
1265
|
+
},
|
|
1266
|
+
plugin.manifest.id,
|
|
1267
|
+
),
|
|
1268
|
+
});
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
console.error('[plugin-loader] Unexpected error during pre-listen load:', err);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1080
1273
|
fastify.server.on("upgrade", (request, socket, head) => {
|
|
1081
1274
|
// Access check for WebSocket upgrades
|
|
1082
1275
|
const remoteAddress = request.socket.remoteAddress || "";
|
|
@@ -1112,6 +1305,40 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
1112
1305
|
console.log(`Dashboard server running at http://localhost:${config.port}`);
|
|
1113
1306
|
console.log(`Pi gateway listening on port ${config.piPort}`);
|
|
1114
1307
|
|
|
1308
|
+
// ── Optional second port for model proxy (/v1/*) ──────────────
|
|
1309
|
+
{
|
|
1310
|
+
const proxyCfg = loadConfig().modelProxy;
|
|
1311
|
+
if (proxyCfg.enabled && proxyCfg.secondPort) {
|
|
1312
|
+
try {
|
|
1313
|
+
const F = (await import("fastify")).default;
|
|
1314
|
+
const sf = F({ logger: false });
|
|
1315
|
+
const proxyAuthGate = createModelProxyAuthGate({
|
|
1316
|
+
getConfig: () => loadConfig().modelProxy,
|
|
1317
|
+
persistKeyUsage: (apiKeys) => {
|
|
1318
|
+
writeConfigPartial({ modelProxy: { apiKeys } });
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
sf.addHook("onRequest", proxyAuthGate);
|
|
1322
|
+
registerModelProxyRoutes(sf, {
|
|
1323
|
+
getConfig: () => loadConfig().modelProxy,
|
|
1324
|
+
getRegistry: async () => {
|
|
1325
|
+
try { return await getModelRegistry(); } catch { return null; }
|
|
1326
|
+
},
|
|
1327
|
+
streamSimple: (opts: any) => {
|
|
1328
|
+
const fn = getStreamSimpleFn();
|
|
1329
|
+
if (!fn) throw new Error("streamSimple not available");
|
|
1330
|
+
return fn(opts.model, { messages: opts.messages, system: opts.system, tools: opts.tools }, opts);
|
|
1331
|
+
},
|
|
1332
|
+
});
|
|
1333
|
+
await sf.listen({ port: proxyCfg.secondPort, host: "127.0.0.1" });
|
|
1334
|
+
secondFastify = sf as any;
|
|
1335
|
+
console.log(`Model proxy second port listening at http://127.0.0.1:${proxyCfg.secondPort}`);
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
console.warn(`Model proxy second port bind failed (continuing without):`, err);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1115
1342
|
// Advertise via mDNS
|
|
1116
1343
|
try {
|
|
1117
1344
|
advertiseDashboard(config.port, config.piPort);
|
|
@@ -1153,6 +1380,21 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
1153
1380
|
const tunnelUrl = await createTunnel(config.port, config.tunnelReservedToken);
|
|
1154
1381
|
if (tunnelUrl) {
|
|
1155
1382
|
console.log(`🌐 Tunnel: ${tunnelUrl}`);
|
|
1383
|
+
// Start the watchdog so a stale zrok edge connection is detected
|
|
1384
|
+
// and recycled automatically (preserves reserved token / URL).
|
|
1385
|
+
const wd = config.tunnelWatchdog;
|
|
1386
|
+
if (wd?.enabled !== false) {
|
|
1387
|
+
startTunnelWatchdog(
|
|
1388
|
+
{
|
|
1389
|
+
getUrl: getTunnelUrl,
|
|
1390
|
+
recycle: async () => {
|
|
1391
|
+
await deleteTunnel(config.port);
|
|
1392
|
+
return await createTunnel(config.port, config.tunnelReservedToken);
|
|
1393
|
+
},
|
|
1394
|
+
},
|
|
1395
|
+
wd,
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1156
1398
|
}
|
|
1157
1399
|
}
|
|
1158
1400
|
}
|
|
@@ -1160,60 +1402,6 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
1160
1402
|
// Discover sessions and start OpenSpec polling (async, non-blocking)
|
|
1161
1403
|
discoverAndBroadcastSessions({ sessionManager, browserGateway, directoryService });
|
|
1162
1404
|
|
|
1163
|
-
// Load plugin server entries (non-blocking; failures isolated per plugin)
|
|
1164
|
-
loadServerEntries({
|
|
1165
|
-
isEnabled: (pluginId) => {
|
|
1166
|
-
const cfg = loadConfig();
|
|
1167
|
-
const pluginCfg = getPluginConfigFromFile(cfg, pluginId) as Record<string, unknown>;
|
|
1168
|
-
return pluginCfg.enabled !== false; // default: enabled
|
|
1169
|
-
},
|
|
1170
|
-
createContext: (plugin) => createServerPluginContext(
|
|
1171
|
-
{
|
|
1172
|
-
fastify,
|
|
1173
|
-
sessionManager: {
|
|
1174
|
-
listActive: () => sessionManager.listActive(),
|
|
1175
|
-
listAll: () => sessionManager.listAll(),
|
|
1176
|
-
getSession: (id: string) => sessionManager.get(id),
|
|
1177
|
-
},
|
|
1178
|
-
eventStore: {
|
|
1179
|
-
getEvents: (sessionId) => eventStore.getEvents(sessionId, 0),
|
|
1180
|
-
getLatestEvent: (sessionId) => {
|
|
1181
|
-
const events = eventStore.getEvents(sessionId, 0);
|
|
1182
|
-
return events.length > 0 ? events[events.length - 1] : undefined;
|
|
1183
|
-
},
|
|
1184
|
-
},
|
|
1185
|
-
broadcastToSubscribers: (msg) => browserGateway.broadcast(msg as any),
|
|
1186
|
-
// Plugin pi/browser handler registration — stub for now;
|
|
1187
|
-
// full dynamic handler registration requires a registry refactor
|
|
1188
|
-
// tracked in extract-*-as-plugin changes.
|
|
1189
|
-
registerPiHandler: (_type, _handler) => {},
|
|
1190
|
-
registerBrowserHandler: (_type, _handler) => {},
|
|
1191
|
-
getPluginConfig: (id) => {
|
|
1192
|
-
const cfg = loadConfig();
|
|
1193
|
-
return getPluginConfigFromFile(cfg, id);
|
|
1194
|
-
},
|
|
1195
|
-
updatePluginConfig: async (id, partial) => {
|
|
1196
|
-
// Inline partial write (reuses CONFIG_FILE path from shared config)
|
|
1197
|
-
const cfg = loadConfig();
|
|
1198
|
-
const current = getPluginConfigFromFile(cfg, id);
|
|
1199
|
-
const merged = { ...current, ...partial };
|
|
1200
|
-
let rawConfig: Record<string, unknown> = {};
|
|
1201
|
-
try {
|
|
1202
|
-
const raw = (await import('node:fs')).default.readFileSync(CONFIG_FILE, 'utf-8');
|
|
1203
|
-
rawConfig = JSON.parse(raw);
|
|
1204
|
-
} catch { /* start fresh */ }
|
|
1205
|
-
rawConfig.plugins = { ...(rawConfig.plugins as Record<string, unknown> ?? {}), [id]: merged };
|
|
1206
|
-
const fs = (await import('node:fs')).default;
|
|
1207
|
-
const tmpFile = CONFIG_FILE + '.tmp.' + process.pid;
|
|
1208
|
-
fs.writeFileSync(tmpFile, JSON.stringify(rawConfig, null, 2) + '\n');
|
|
1209
|
-
fs.renameSync(tmpFile, CONFIG_FILE);
|
|
1210
|
-
browserGateway.broadcast({ type: 'plugin_config_update', id, config: merged } as any);
|
|
1211
|
-
},
|
|
1212
|
-
},
|
|
1213
|
-
plugin.manifest.id,
|
|
1214
|
-
),
|
|
1215
|
-
}).catch((err) => console.error('[plugin-loader] Unexpected error:', err));
|
|
1216
|
-
|
|
1217
1405
|
// Auto-register plugin bridge entries
|
|
1218
1406
|
const discoveredPlugins = discoverPlugins();
|
|
1219
1407
|
const pluginsWithBridges = discoveredPlugins
|
|
@@ -1259,6 +1447,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
1259
1447
|
unsubscribeQueueComplete();
|
|
1260
1448
|
bootstrapState.dispose();
|
|
1261
1449
|
bootstrapQueue.clear("server shutting down");
|
|
1450
|
+
stopTunnelWatchdog();
|
|
1262
1451
|
await deleteTunnel(config.port);
|
|
1263
1452
|
piGateway.stop();
|
|
1264
1453
|
for (const client of browserGateway.wss.clients) {
|
|
@@ -1274,6 +1463,11 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
1274
1463
|
editorManager.stopAll();
|
|
1275
1464
|
// Close any pending OAuth callback servers
|
|
1276
1465
|
try { const { closeAllCallbackServers } = await import("./oauth-callback-server.js"); await closeAllCallbackServers(); } catch {}
|
|
1466
|
+
// Close second port before main server
|
|
1467
|
+
if (secondFastify) {
|
|
1468
|
+
try { await secondFastify.close(); } catch { /* ignore */ }
|
|
1469
|
+
secondFastify = null;
|
|
1470
|
+
}
|
|
1277
1471
|
await fastify.close();
|
|
1278
1472
|
},
|
|
1279
1473
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* These expose WebSocket-only operations as HTTP endpoints
|
|
4
4
|
* for use by skills, scripts, and external tooling.
|
|
5
5
|
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
6
7
|
import type { FastifyInstance } from "fastify";
|
|
7
8
|
import type { SessionManager } from "./memory-session-manager.js";
|
|
8
9
|
import type { PiGateway } from "./pi-gateway.js";
|
|
@@ -15,6 +16,8 @@ import type { PendingResumeIntentRegistry } from "./pending-resume-intent-regist
|
|
|
15
16
|
import type { BootstrapStateStore } from "./bootstrap-state.js";
|
|
16
17
|
import type { BootstrapQueue } from "./bootstrap-queue.js";
|
|
17
18
|
import { attachRenameTarget, detachShouldClearName } from "./proposal-attach-naming.js";
|
|
19
|
+
import { FORK_DEGRADED_TO_NEW_MESSAGE, FORK_DEGRADED_TO_NEW_CODE } from "./browser-handlers/session-action-handler.js";
|
|
20
|
+
import { keeperOptsFromSpawnResult } from "./headless-pid-registry.js";
|
|
18
21
|
|
|
19
22
|
export interface SessionApiDeps {
|
|
20
23
|
sessionManager: SessionManager;
|
|
@@ -36,6 +39,13 @@ export interface SessionApiDeps {
|
|
|
36
39
|
* See change: preserve-session-order-on-reboot.
|
|
37
40
|
*/
|
|
38
41
|
pendingResumeIntents?: PendingResumeIntentRegistry;
|
|
42
|
+
/**
|
|
43
|
+
* Optional pending-attach registry. When provided, the resume endpoint's
|
|
44
|
+
* fork-empty-session degradation path inherits the parent's
|
|
45
|
+
* `attachedProposal` for the new spawn.
|
|
46
|
+
* See change: fix-fork-empty-session-silent-timeout.
|
|
47
|
+
*/
|
|
48
|
+
pendingAttachRegistry?: import("./pending-attach-registry.js").PendingAttachRegistry;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
type IdParams = { Params: { id: string } };
|
|
@@ -48,7 +58,7 @@ function getSessionOrFail(sessionManager: SessionManager, id: string): { session
|
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDeps) {
|
|
51
|
-
const { sessionManager, piGateway, browserGateway, pendingForkRegistry, pendingDashboardSpawns, bootstrapState, bootstrapQueue, pendingResumeIntents } = deps;
|
|
61
|
+
const { sessionManager, piGateway, browserGateway, pendingForkRegistry, pendingDashboardSpawns, bootstrapState, bootstrapQueue, pendingResumeIntents, pendingAttachRegistry } = deps;
|
|
52
62
|
|
|
53
63
|
/**
|
|
54
64
|
* Gate pi-dependent operations on bootstrap status. Returns:
|
|
@@ -230,7 +240,13 @@ export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDep
|
|
|
230
240
|
const config = loadConfig();
|
|
231
241
|
const spawnResult = await spawnPiSession(cwd, { strategy: config.spawnStrategy });
|
|
232
242
|
if (spawnResult.process && spawnResult.pid) {
|
|
233
|
-
browserGateway.headlessPidRegistry.register(
|
|
243
|
+
browserGateway.headlessPidRegistry.register(
|
|
244
|
+
spawnResult.pid,
|
|
245
|
+
cwd,
|
|
246
|
+
spawnResult.process,
|
|
247
|
+
spawnResult.spawnToken,
|
|
248
|
+
keeperOptsFromSpawnResult(spawnResult),
|
|
249
|
+
);
|
|
234
250
|
}
|
|
235
251
|
if (spawnResult.dashboardSpawned && spawnResult.success) {
|
|
236
252
|
pendingDashboardSpawns?.set(cwd, (pendingDashboardSpawns?.get(cwd) ?? 0) + 1);
|
|
@@ -282,8 +298,45 @@ export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDep
|
|
|
282
298
|
reply.code(409);
|
|
283
299
|
return { success: false, error: "session is already being resumed" } satisfies ApiResponse;
|
|
284
300
|
}
|
|
285
|
-
|
|
286
|
-
|
|
301
|
+
// Fork preflight: silent-degrade when the source has no on-disk JSONL.
|
|
302
|
+
// Mirrors the WS-handler logic. See change:
|
|
303
|
+
// fix-fork-empty-session-silent-timeout.
|
|
304
|
+
if (mode === "fork" && !existsSync(session.sessionFile)) {
|
|
305
|
+
// Inherit attachedProposal from parent.
|
|
306
|
+
if (session.attachedProposal && pendingAttachRegistry) {
|
|
307
|
+
pendingAttachRegistry.enqueue(session.cwd, session.attachedProposal);
|
|
308
|
+
}
|
|
309
|
+
const degradeConfig = loadConfig();
|
|
310
|
+
const degradeResult = await spawnPiSession(session.cwd, {
|
|
311
|
+
strategy: degradeConfig.spawnStrategy,
|
|
312
|
+
});
|
|
313
|
+
if (degradeResult.process && degradeResult.pid) {
|
|
314
|
+
browserGateway.headlessPidRegistry.register(
|
|
315
|
+
degradeResult.pid,
|
|
316
|
+
session.cwd,
|
|
317
|
+
degradeResult.process,
|
|
318
|
+
degradeResult.spawnToken,
|
|
319
|
+
keeperOptsFromSpawnResult(degradeResult),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (degradeResult.dashboardSpawned && degradeResult.success) {
|
|
323
|
+
pendingDashboardSpawns?.set(
|
|
324
|
+
session.cwd,
|
|
325
|
+
(pendingDashboardSpawns?.get(session.cwd) ?? 0) + 1,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (!degradeResult.success) {
|
|
329
|
+
reply.code(500);
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
error: degradeResult.message,
|
|
333
|
+
} satisfies ApiResponse;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
success: true,
|
|
337
|
+
data: { message: FORK_DEGRADED_TO_NEW_MESSAGE },
|
|
338
|
+
code: FORK_DEGRADED_TO_NEW_CODE,
|
|
339
|
+
} satisfies ApiResponse<{ message: string }>;
|
|
287
340
|
}
|
|
288
341
|
// Tag the user-resume intent BEFORE spawning. REST resume always
|
|
289
342
|
// uses "front" placement — the only "keep" path is drag-to-resume
|
|
@@ -297,6 +350,12 @@ export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDep
|
|
|
297
350
|
mode,
|
|
298
351
|
strategy: config.spawnStrategy,
|
|
299
352
|
});
|
|
353
|
+
// Fork bookkeeping uses the spawn token (not cwd) so two concurrent
|
|
354
|
+
// forks in the same cwd correlate correctly. See change:
|
|
355
|
+
// spawn-correlation-token.
|
|
356
|
+
if (mode === "fork" && pendingForkRegistry && spawnResult.spawnToken) {
|
|
357
|
+
pendingForkRegistry.recordFork(spawnResult.spawnToken, id);
|
|
358
|
+
}
|
|
300
359
|
if (spawnResult.dashboardSpawned && spawnResult.success) {
|
|
301
360
|
pendingDashboardSpawns?.set(session.cwd, (pendingDashboardSpawns?.get(session.cwd) ?? 0) + 1);
|
|
302
361
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Standalone session discovery — lists pi sessions for a given cwd
|
|
3
|
-
* without requiring @
|
|
3
|
+
* without requiring @earendil-works/pi-coding-agent.
|
|
4
4
|
* Reads session JSONL files from ~/.pi/agent/sessions/<encoded-cwd>/.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Standalone JSONL session file reader.
|
|
3
|
-
* Reads pi session files without requiring @
|
|
3
|
+
* Reads pi session files without requiring @earendil-works/pi-coding-agent.
|
|
4
4
|
* Falls back to linear entry order (no tree branching support).
|
|
5
5
|
*/
|
|
6
6
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|