@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.
Files changed (201) hide show
  1. package/AGENTS.md +26 -5
  2. package/README.md +49 -7
  3. package/docs/architecture.md +129 -1
  4. package/package.json +15 -15
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
  13. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  14. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  15. package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
  16. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  17. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  18. package/packages/extension/src/ask-user-tool.ts +1 -1
  19. package/packages/extension/src/bridge-context.ts +68 -4
  20. package/packages/extension/src/bridge.ts +79 -11
  21. package/packages/extension/src/command-handler.ts +95 -15
  22. package/packages/extension/src/flow-event-wiring.ts +1 -1
  23. package/packages/extension/src/multiselect-list.ts +1 -1
  24. package/packages/extension/src/pi-env.d.ts +16 -9
  25. package/packages/extension/src/prompt-expander.ts +74 -63
  26. package/packages/extension/src/provider-register.ts +16 -9
  27. package/packages/extension/src/retry-tracker.ts +123 -0
  28. package/packages/extension/src/server-launcher.ts +31 -70
  29. package/packages/extension/src/session-sync.ts +10 -1
  30. package/packages/extension/src/slash-dispatch.ts +123 -0
  31. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  32. package/packages/server/bin/pi-dashboard.mjs +84 -0
  33. package/packages/server/package.json +8 -7
  34. package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
  35. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  36. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  37. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  38. package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
  39. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
  40. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  41. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  42. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  43. package/packages/server/src/__tests__/directory-service.test.ts +2 -2
  44. package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
  45. package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
  46. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
  47. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  48. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  49. package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
  50. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  51. package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
  52. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
  53. package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
  54. package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
  55. package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
  56. package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
  57. package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
  58. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
  59. package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
  60. package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
  61. package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
  62. package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
  63. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  64. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  65. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  66. package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
  67. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  68. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  69. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  70. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
  71. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  72. package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
  73. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  74. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  75. package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
  76. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  77. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
  78. package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
  79. package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
  80. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  81. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
  82. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  83. package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
  84. package/packages/server/src/auth-plugin.ts +3 -0
  85. package/packages/server/src/bootstrap-state.ts +10 -0
  86. package/packages/server/src/browser-gateway.ts +27 -10
  87. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  88. package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
  89. package/packages/server/src/changelog-fs.ts +167 -0
  90. package/packages/server/src/changelog-parser.ts +321 -0
  91. package/packages/server/src/changelog-remote.ts +134 -0
  92. package/packages/server/src/cli.ts +62 -82
  93. package/packages/server/src/config-api.ts +14 -2
  94. package/packages/server/src/directory-service.ts +106 -4
  95. package/packages/server/src/event-wiring.ts +90 -6
  96. package/packages/server/src/headless-pid-registry.ts +344 -37
  97. package/packages/server/src/legacy-pi-cleanup.ts +151 -0
  98. package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
  99. package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
  100. package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
  101. package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
  102. package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
  103. package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
  104. package/packages/server/src/model-proxy/api-key-store.ts +87 -0
  105. package/packages/server/src/model-proxy/auth-gate.ts +116 -0
  106. package/packages/server/src/model-proxy/concurrency.ts +76 -0
  107. package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
  108. package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
  109. package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
  110. package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
  111. package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
  112. package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
  113. package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
  114. package/packages/server/src/model-proxy/convert/index.ts +8 -0
  115. package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
  116. package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
  117. package/packages/server/src/model-proxy/convert/types.ts +70 -0
  118. package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
  119. package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
  120. package/packages/server/src/model-proxy/internal-registry.ts +157 -0
  121. package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
  122. package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
  123. package/packages/server/src/model-proxy/request-log.ts +53 -0
  124. package/packages/server/src/model-proxy/streamer.ts +59 -0
  125. package/packages/server/src/openspec-group-store.ts +490 -0
  126. package/packages/server/src/pending-client-correlations.ts +73 -0
  127. package/packages/server/src/pending-fork-registry.ts +24 -12
  128. package/packages/server/src/pi-core-checker.ts +77 -17
  129. package/packages/server/src/pi-core-updater.ts +16 -6
  130. package/packages/server/src/pi-dev-version-check.ts +145 -0
  131. package/packages/server/src/pi-gateway.ts +4 -0
  132. package/packages/server/src/pi-version-skew.ts +12 -4
  133. package/packages/server/src/process-manager.ts +182 -11
  134. package/packages/server/src/provider-auth-storage.ts +29 -47
  135. package/packages/server/src/provider-catalogue-cache.ts +24 -18
  136. package/packages/server/src/restart-helper.ts +17 -16
  137. package/packages/server/src/routes/bootstrap-routes.ts +37 -0
  138. package/packages/server/src/routes/jj-routes.ts +3 -0
  139. package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
  140. package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
  141. package/packages/server/src/routes/model-proxy-routes.ts +330 -0
  142. package/packages/server/src/routes/openspec-group-routes.ts +231 -0
  143. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  144. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  145. package/packages/server/src/routes/provider-auth-routes.ts +8 -1
  146. package/packages/server/src/routes/provider-routes.ts +28 -5
  147. package/packages/server/src/routes/system-routes.ts +44 -2
  148. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
  149. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
  150. package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
  151. package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
  152. package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
  153. package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
  154. package/packages/server/src/server.ts +254 -60
  155. package/packages/server/src/session-api.ts +63 -4
  156. package/packages/server/src/session-discovery.ts +1 -1
  157. package/packages/server/src/session-file-reader.ts +1 -1
  158. package/packages/server/src/spawn-register-watchdog.ts +62 -7
  159. package/packages/server/src/spawn-token.ts +20 -0
  160. package/packages/server/src/tunnel-watchdog.ts +230 -0
  161. package/packages/server/src/tunnel.ts +5 -1
  162. package/packages/shared/package.json +1 -1
  163. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
  164. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
  165. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  166. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  167. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  168. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  169. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  170. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  172. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  173. package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
  174. package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
  175. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
  176. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  177. package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
  178. package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
  179. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
  180. package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
  181. package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
  182. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  183. package/packages/shared/src/bootstrap-install.ts +1 -1
  184. package/packages/shared/src/browser-protocol.ts +70 -0
  185. package/packages/shared/src/changelog-types.ts +111 -0
  186. package/packages/shared/src/config.ts +172 -2
  187. package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
  188. package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
  189. package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
  190. package/packages/shared/src/platform/binary-lookup.ts +204 -0
  191. package/packages/shared/src/platform/node-spawn.ts +71 -26
  192. package/packages/shared/src/protocol.ts +27 -1
  193. package/packages/shared/src/recommended-extensions.ts +18 -0
  194. package/packages/shared/src/rest-api.ts +219 -1
  195. package/packages/shared/src/server-launcher.ts +277 -0
  196. package/packages/shared/src/skill-block-parser.ts +1 -1
  197. package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
  198. package/packages/shared/src/tool-registry/definitions.ts +15 -3
  199. package/packages/shared/src/types.ts +62 -0
  200. package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
  201. 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
- const directoryService = createDirectoryService(preferencesStore, sessionManager, config.openspec);
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 = ["@mariozechner/pi-coding-agent"];
801
+ const packages = ["@earendil-works/pi-coding-agent"];
725
802
  bootstrapState.setLastInstallPackages(packages);
726
803
  bootstrapState.set({
727
804
  status: "installing",
728
- progress: { step: "@mariozechner/pi-coding-agent", output: "starting upgrade…" },
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
- : ["@mariozechner/pi-coding-agent", "@fission-ai/openspec", "tsx"];
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(spawnResult.pid, cwd, spawnResult.process);
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
- if (mode === "fork" && pendingForkRegistry) {
286
- pendingForkRegistry.recordFork(session.cwd, id);
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 @mariozechner/pi-coding-agent.
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 @mariozechner/pi-coding-agent.
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";