@blackbelt-technology/pi-agent-dashboard 0.4.6 → 0.5.1
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 +339 -190
- package/README.md +50 -7
- package/docs/architecture.md +238 -23
- package/package.json +23 -13
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
- 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.test.ts +24 -1
- 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 +1 -1
- package/packages/extension/src/bridge.ts +169 -4
- package/packages/extension/src/command-handler.ts +65 -2
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/markdown-image-inliner.ts +268 -0
- 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 +50 -2
- package/packages/extension/src/provider-register.ts +132 -8
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +18 -1
- package/packages/extension/src/session-sync.ts +15 -1
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/package.json +6 -6
- package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
- package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
- package/packages/server/src/__tests__/build-auth-status.test.ts +190 -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 +22 -4
- package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -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 +1 -1
- package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
- 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__/has-openspec-dir.test.ts +64 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
- package/packages/server/src/__tests__/health-shape.test.ts +43 -0
- package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -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-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 +236 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- 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-codes.test.ts +80 -0
- package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
- package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
- package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
- package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
- package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
- package/packages/server/src/bootstrap-install-from-list.ts +232 -0
- package/packages/server/src/bootstrap-state.ts +18 -0
- package/packages/server/src/browser-gateway.ts +70 -24
- package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
- package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
- 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 +23 -2
- package/packages/server/src/directory-service.ts +31 -0
- package/packages/server/src/event-wiring.ts +105 -5
- package/packages/server/src/headless-pid-registry.ts +54 -5
- package/packages/server/src/home-lock.d.ts +124 -0
- package/packages/server/src/home-lock.js +330 -0
- package/packages/server/src/home-lock.js.map +1 -0
- package/packages/server/src/idle-timer.ts +15 -1
- 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 +81 -15
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +10 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +115 -21
- package/packages/server/src/provider-auth-handlers.ts +9 -0
- package/packages/server/src/provider-auth-storage.ts +83 -51
- package/packages/server/src/provider-catalogue-cache.ts +47 -0
- package/packages/server/src/routes/doctor-routes.ts +140 -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 +14 -1
- package/packages/server/src/routes/provider-routes.ts +4 -4
- package/packages/server/src/routes/system-routes.ts +38 -1
- package/packages/server/src/server.ts +85 -66
- package/packages/server/src/session-api.ts +54 -3
- package/packages/server/src/session-bootstrap.ts +27 -12
- package/packages/server/src/session-discovery.ts +11 -4
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/session-scanner.ts +4 -2
- package/packages/server/src/spawn-failure-log.ts +130 -0
- package/packages/server/src/spawn-preflight.ts +82 -0
- package/packages/server/src/spawn-register-watchdog.ts +291 -0
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/terminal-manager.ts +12 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -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 +6 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config.test.ts +48 -0
- package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
- package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
- package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
- package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
- package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
- package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
- package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
- package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
- package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +197 -3
- package/packages/shared/src/browser-protocol.ts +155 -1
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +15 -0
- package/packages/shared/src/dashboard-starter.ts +33 -0
- package/packages/shared/src/doctor-core.ts +821 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/installable-list.ts +152 -0
- package/packages/shared/src/launch-source-flag.ts +14 -0
- package/packages/shared/src/launch-source-types.ts +18 -0
- package/packages/shared/src/openspec-activity-detector.ts +25 -7
- package/packages/shared/src/platform/detached-spawn.ts +13 -2
- package/packages/shared/src/platform/managed-node-path.ts +77 -0
- package/packages/shared/src/platform/node-spawn.ts +29 -21
- package/packages/shared/src/protocol.ts +54 -2
- package/packages/shared/src/resolve-jiti.ts +62 -9
- package/packages/shared/src/rest-api.ts +4 -0
- package/packages/shared/src/skill-block-parser.ts +115 -0
- package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
- package/packages/shared/src/tool-registry/definitions.ts +33 -8
- package/packages/shared/src/tool-registry/strategies.ts +42 -0
- package/packages/shared/src/types.ts +64 -0
|
@@ -15,7 +15,44 @@ import { createHeadlessPidRegistry, type HeadlessPidRegistry } from "./headless-
|
|
|
15
15
|
import type { PendingForkRegistry } from "./pending-fork-registry.js";
|
|
16
16
|
import type { SessionOrderManager } from "./session-order-manager.js";
|
|
17
17
|
import type { PreferencesStore } from "./preferences-store.js";
|
|
18
|
-
import type
|
|
18
|
+
import { hasOpenSpecDir, type DirectoryService } from "./directory-service.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pure helper: build the per-cwd `openspec_update` messages a freshly
|
|
22
|
+
* connecting browser should receive. One message per known cwd.
|
|
23
|
+
* Disambiguates three states:
|
|
24
|
+
* - cache populated → cached payload
|
|
25
|
+
* - openspec dir but cold → { initialized: false, pending: true }
|
|
26
|
+
* - no openspec dir → { initialized: false, pending: false }
|
|
27
|
+
*
|
|
28
|
+
* Exported so cold-boot snapshot semantics can be unit-tested without
|
|
29
|
+
* spinning up a WS server. See change: fix-cold-boot-openspec-protocol.
|
|
30
|
+
*/
|
|
31
|
+
export function buildOpenSpecConnectSnapshot(
|
|
32
|
+
directoryService: Pick<DirectoryService, "knownDirectories" | "getOpenSpecData">,
|
|
33
|
+
hasDir: (cwd: string) => boolean,
|
|
34
|
+
): Array<ServerToBrowserMessage> {
|
|
35
|
+
const out: Array<ServerToBrowserMessage> = [];
|
|
36
|
+
for (const cwd of directoryService.knownDirectories()) {
|
|
37
|
+
const cached = directoryService.getOpenSpecData(cwd);
|
|
38
|
+
if (cached && cached.initialized) {
|
|
39
|
+
out.push({ type: "openspec_update", cwd, data: cached });
|
|
40
|
+
} else if (hasDir(cwd)) {
|
|
41
|
+
out.push({
|
|
42
|
+
type: "openspec_update",
|
|
43
|
+
cwd,
|
|
44
|
+
data: { initialized: false, pending: true, changes: [] },
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
out.push({
|
|
48
|
+
type: "openspec_update",
|
|
49
|
+
cwd,
|
|
50
|
+
data: { initialized: false, pending: false, changes: [] },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
19
56
|
import { createPendingResumeRegistry, type PendingResumeRegistry } from "./pending-resume-registry.js";
|
|
20
57
|
import { createViewedSessionTracker, type ViewedSessionTracker } from "./viewed-session-tracker.js";
|
|
21
58
|
import type { TerminalManager } from "./terminal-manager.js";
|
|
@@ -31,7 +68,7 @@ import { handlePinDirectory, handleUnpinDirectory, handleReorderPinnedDirs, hand
|
|
|
31
68
|
export interface BrowserGateway {
|
|
32
69
|
wss: WebSocketServer;
|
|
33
70
|
broadcastEvent(sessionId: string, seq: number, event: any): void;
|
|
34
|
-
broadcastSessionAdded(session: any): void;
|
|
71
|
+
broadcastSessionAdded(session: any, opts?: { spawnRequestId?: string }): void;
|
|
35
72
|
broadcastSessionUpdated(sessionId: string, updates: any): void;
|
|
36
73
|
broadcastSessionRemoved(sessionId: string): void;
|
|
37
74
|
sendToSubscribers(sessionId: string, msg: ServerToBrowserMessage): void;
|
|
@@ -82,6 +119,7 @@ export function createBrowserGateway(
|
|
|
82
119
|
maxWsBufferBytes?: number,
|
|
83
120
|
pendingAttachRegistry?: import("./pending-attach-registry.js").PendingAttachRegistry,
|
|
84
121
|
pendingResumeIntents?: import("./pending-resume-intent-registry.js").PendingResumeIntentRegistry,
|
|
122
|
+
pendingClientCorrelations?: import("./pending-client-correlations.js").PendingClientCorrelations,
|
|
85
123
|
): BrowserGateway {
|
|
86
124
|
const wss = new WebSocketServer({ noServer: true });
|
|
87
125
|
|
|
@@ -208,10 +246,21 @@ export function createBrowserGateway(
|
|
|
208
246
|
const subs = new Set<string>();
|
|
209
247
|
subscriptions.set(ws, subs);
|
|
210
248
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
249
|
+
// Atomic snapshot of the full session registry + per-cwd orders.
|
|
250
|
+
// Replaces the legacy per-session `session_added` loop and per-cwd
|
|
251
|
+
// `sessions_reordered` loop. Client REPLACES (not merges) its
|
|
252
|
+
// `sessions` Map and `sessionOrderMap` on receipt so stale ids from a
|
|
253
|
+
// previous server lifetime are dropped atomically.
|
|
254
|
+
// See change: fix-stale-sessions-on-reconnect.
|
|
255
|
+
{
|
|
256
|
+
const sessionsSnapshot = sessionManager.listAll();
|
|
257
|
+
const orders: Record<string, string[]> = {};
|
|
258
|
+
if (sessionOrderManager) {
|
|
259
|
+
for (const [cwd, sessionIds] of Object.entries(sessionOrderManager.getAllOrders())) {
|
|
260
|
+
if (sessionIds.length > 0) orders[cwd] = sessionIds;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
sendTo(ws, { type: "sessions_snapshot", sessions: sessionsSnapshot, orders });
|
|
215
264
|
}
|
|
216
265
|
|
|
217
266
|
// Send pinned directories on connect
|
|
@@ -219,23 +268,12 @@ export function createBrowserGateway(
|
|
|
219
268
|
sendTo(ws, { type: "pinned_dirs_updated", paths: preferencesStore.getPinnedDirectories() });
|
|
220
269
|
}
|
|
221
270
|
|
|
222
|
-
// Send
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
for (const [cwd, sessionIds] of Object.entries(allOrders)) {
|
|
226
|
-
if (sessionIds.length > 0) {
|
|
227
|
-
sendTo(ws, { type: "sessions_reordered", cwd, sessionIds });
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Send cached OpenSpec data for all known directories
|
|
271
|
+
// Send OpenSpec data for every known directory — exactly one
|
|
272
|
+
// `openspec_update` per cwd, never silently omit.
|
|
273
|
+
// See change: fix-cold-boot-openspec-protocol.
|
|
233
274
|
if (directoryService) {
|
|
234
|
-
for (const
|
|
235
|
-
|
|
236
|
-
if (data && data.initialized) {
|
|
237
|
-
sendTo(ws, { type: "openspec_update", cwd, data });
|
|
238
|
-
}
|
|
275
|
+
for (const msg of buildOpenSpecConnectSnapshot(directoryService, hasOpenSpecDir)) {
|
|
276
|
+
sendTo(ws, msg);
|
|
239
277
|
}
|
|
240
278
|
}
|
|
241
279
|
|
|
@@ -270,6 +308,7 @@ export function createBrowserGateway(
|
|
|
270
308
|
headlessPidRegistry, pendingResumeRegistry, pendingDashboardSpawns,
|
|
271
309
|
pendingAttachRegistry,
|
|
272
310
|
pendingResumeIntents,
|
|
311
|
+
pendingClientCorrelations,
|
|
273
312
|
sendTo, broadcast, getSubscribers, replayPendingUiRequests,
|
|
274
313
|
trackUiRequest: trackUiRequest,
|
|
275
314
|
markReplaying(targetWs, sessionId) {
|
|
@@ -537,8 +576,15 @@ export function createBrowserGateway(
|
|
|
537
576
|
}
|
|
538
577
|
},
|
|
539
578
|
|
|
540
|
-
broadcastSessionAdded(session: any) {
|
|
541
|
-
|
|
579
|
+
broadcastSessionAdded(session: any, opts?: { spawnRequestId?: string }) {
|
|
580
|
+
// Carry the originating client `requestId` (when known) so the
|
|
581
|
+
// browser can auto-select / dismiss its placeholder by exact
|
|
582
|
+
// correlation. See change: spawn-correlation-token.
|
|
583
|
+
broadcast({
|
|
584
|
+
type: "session_added",
|
|
585
|
+
session,
|
|
586
|
+
...(opts?.spawnRequestId ? { spawnRequestId: opts.spawnRequestId } : {}),
|
|
587
|
+
});
|
|
542
588
|
},
|
|
543
589
|
|
|
544
590
|
broadcastSessionUpdated(sessionId: string, updates: any) {
|
|
@@ -148,6 +148,10 @@ export function handlePiGatewayForward(
|
|
|
148
148
|
case "request_models":
|
|
149
149
|
piGateway.sendToSession(msg.sessionId, { type: "request_models", sessionId: msg.sessionId });
|
|
150
150
|
break;
|
|
151
|
+
case "request_providers":
|
|
152
|
+
// See change: replace-hardcoded-provider-lists.
|
|
153
|
+
piGateway.sendToSession(msg.sessionId, { type: "request_providers", sessionId: msg.sessionId });
|
|
154
|
+
break;
|
|
151
155
|
case "set_thinking_level":
|
|
152
156
|
piGateway.sendToSession(msg.sessionId, { type: "set_thinking_level", sessionId: msg.sessionId, level: msg.level });
|
|
153
157
|
break;
|
|
@@ -16,6 +16,7 @@ import type { HeadlessPidRegistry } from "../headless-pid-registry.js";
|
|
|
16
16
|
import type { PendingResumeRegistry } from "../pending-resume-registry.js";
|
|
17
17
|
import type { PendingAttachRegistry } from "../pending-attach-registry.js";
|
|
18
18
|
import type { PendingResumeIntentRegistry } from "../pending-resume-intent-registry.js";
|
|
19
|
+
import type { PendingClientCorrelations } from "../pending-client-correlations.js";
|
|
19
20
|
|
|
20
21
|
export interface BrowserHandlerContext {
|
|
21
22
|
ws: WebSocket;
|
|
@@ -43,6 +44,14 @@ export interface BrowserHandlerContext {
|
|
|
43
44
|
* See change: preserve-session-order-on-reboot.
|
|
44
45
|
*/
|
|
45
46
|
pendingResumeIntents?: PendingResumeIntentRegistry;
|
|
47
|
+
/**
|
|
48
|
+
* Optional registry mapping `spawnToken → requestId` for client-side
|
|
49
|
+
* correlation. When set, browser-initiated spawns/resumes that carry a
|
|
50
|
+
* `requestId` are recorded so the eventual `session_added` broadcast
|
|
51
|
+
* carries `spawnRequestId` for auto-select / placeholder dismissal.
|
|
52
|
+
* See change: spawn-correlation-token.
|
|
53
|
+
*/
|
|
54
|
+
pendingClientCorrelations?: PendingClientCorrelations;
|
|
46
55
|
/** Send message to a specific WebSocket */
|
|
47
56
|
sendTo(ws: WebSocket, msg: ServerToBrowserMessage): void;
|
|
48
57
|
/** Broadcast to all connected browsers */
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session action handlers: send_prompt, abort, resume, spawn, shutdown, flow_control.
|
|
3
3
|
*/
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
4
5
|
import type { BrowserToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
5
6
|
import type { BrowserHandlerContext } from "./handler-context.js";
|
|
6
7
|
import { spawnPiSession } from "../process-manager.js";
|
|
8
|
+
import { ToolResolver } from "@blackbelt-technology/pi-dashboard-shared/platform/binary-lookup.js";
|
|
7
9
|
import { loadConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
10
|
+
import { preflightSpawn } from "../spawn-preflight.js";
|
|
11
|
+
import { getSpawnRegisterWatchdog } from "../spawn-register-watchdog.js";
|
|
12
|
+
import { appendSpawnFailure } from "../spawn-failure-log.js";
|
|
8
13
|
import { createBranchedSessionFile } from "../session-file-reader.js";
|
|
9
14
|
import {
|
|
10
15
|
killPidWithGroup,
|
|
@@ -15,6 +20,19 @@ import {
|
|
|
15
20
|
} from "@blackbelt-technology/pi-dashboard-shared/platform/process-identify.js";
|
|
16
21
|
import { shouldInterceptReload } from "./session-action-helpers.js";
|
|
17
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Status message + code emitted when fork is attempted on a session whose
|
|
25
|
+
* `.jsonl` does not exist on disk yet (empty session, no persisted entries).
|
|
26
|
+
* The dashboard silently degrades to a fresh spawn in the same cwd — fork
|
|
27
|
+
* has no history to copy, so the user-meaningful semantic of "fork" and
|
|
28
|
+
* "new" is identical here. The structured code lets the client surface a
|
|
29
|
+
* non-blocking toast.
|
|
30
|
+
* See change: fix-fork-empty-session-silent-timeout.
|
|
31
|
+
*/
|
|
32
|
+
export const FORK_DEGRADED_TO_NEW_MESSAGE =
|
|
33
|
+
"Started a fresh session \u2014 the source had no persisted history to fork from.";
|
|
34
|
+
export const FORK_DEGRADED_TO_NEW_CODE = "FORK_DEGRADED_TO_NEW";
|
|
35
|
+
|
|
18
36
|
/**
|
|
19
37
|
* Find headless pi PIDs associated with a session-id marker and kill them.
|
|
20
38
|
* Delegates platform branching to `platform/process-identify.ts` — Windows
|
|
@@ -236,10 +254,10 @@ export async function handleResumeSession(
|
|
|
236
254
|
msg: Extract<BrowserToServerMessage, { type: "resume_session" }>,
|
|
237
255
|
ctx: BrowserHandlerContext,
|
|
238
256
|
): Promise<void> {
|
|
239
|
-
const { ws, sessionManager, pendingForkRegistry, headlessPidRegistry, pendingDashboardSpawns, pendingResumeIntents, sendTo } = ctx;
|
|
257
|
+
const { ws, sessionManager, pendingForkRegistry, headlessPidRegistry, pendingDashboardSpawns, pendingResumeIntents, pendingClientCorrelations, sendTo } = ctx;
|
|
240
258
|
const session = sessionManager.get(msg.sessionId);
|
|
241
259
|
if (!session) {
|
|
242
|
-
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session not found" });
|
|
260
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session not found", requestId: msg.requestId });
|
|
243
261
|
return;
|
|
244
262
|
}
|
|
245
263
|
// Resolve placement intent. Old browsers omit the field; default to
|
|
@@ -248,28 +266,71 @@ export async function handleResumeSession(
|
|
|
248
266
|
// See change: differentiate-resume-intent-by-trigger.
|
|
249
267
|
const placement: "front" | "keep" = msg.placement ?? "front";
|
|
250
268
|
if (!session.sessionFile) {
|
|
251
|
-
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session file is unknown (pre-migration session)" });
|
|
269
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session file is unknown (pre-migration session)", requestId: msg.requestId });
|
|
252
270
|
return;
|
|
253
271
|
}
|
|
254
272
|
if (msg.mode === "continue" && session.status !== "ended") {
|
|
255
|
-
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session is already active" });
|
|
273
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session is already active", requestId: msg.requestId });
|
|
256
274
|
return;
|
|
257
275
|
}
|
|
258
276
|
if (session.resuming) {
|
|
259
|
-
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session is already being resumed" });
|
|
277
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session is already being resumed", requestId: msg.requestId });
|
|
260
278
|
return;
|
|
261
279
|
}
|
|
262
|
-
|
|
263
|
-
|
|
280
|
+
// Fork preflight: silent-degrade when the source session has no on-disk
|
|
281
|
+
// JSONL yet (empty session, no persisted entries). `pi --fork <missing>`
|
|
282
|
+
// would crash silently and produce a 30s register-timeout; instead we
|
|
283
|
+
// spawn a fresh pi in the same cwd and surface `code: FORK_DEGRADED_TO_NEW`
|
|
284
|
+
// so the client can render a non-blocking toast. The parent's
|
|
285
|
+
// attachedProposal (if any) is inherited via `pendingAttachRegistry`
|
|
286
|
+
// since fork's own inheritance path doesn't run on this branch.
|
|
287
|
+
// See change: fix-fork-empty-session-silent-timeout.
|
|
288
|
+
if (msg.mode === "fork" && session.sessionFile && !existsSync(session.sessionFile)) {
|
|
289
|
+
// Inherit attachedProposal from parent so the new session still
|
|
290
|
+
// tracks the change the user was working on.
|
|
291
|
+
const pendingAttachRegistry = ctx.pendingAttachRegistry;
|
|
292
|
+
if (session.attachedProposal && pendingAttachRegistry) {
|
|
293
|
+
pendingAttachRegistry.enqueue(session.cwd, session.attachedProposal);
|
|
294
|
+
}
|
|
295
|
+
const degradeConfig = loadConfig();
|
|
296
|
+
// Fresh spawn: no sessionFile, no mode — just `pi --mode rpc`.
|
|
297
|
+
const degradeResult = await spawnPiSession(session.cwd, {
|
|
298
|
+
strategy: degradeConfig.spawnStrategy,
|
|
299
|
+
});
|
|
300
|
+
if (degradeResult.process && degradeResult.pid) {
|
|
301
|
+
headlessPidRegistry.register(
|
|
302
|
+
degradeResult.pid,
|
|
303
|
+
session.cwd,
|
|
304
|
+
degradeResult.process,
|
|
305
|
+
degradeResult.spawnToken,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (msg.requestId && degradeResult.spawnToken && pendingClientCorrelations) {
|
|
309
|
+
pendingClientCorrelations.record(degradeResult.spawnToken, msg.requestId);
|
|
310
|
+
}
|
|
311
|
+
if (degradeResult.dashboardSpawned && degradeResult.success) {
|
|
312
|
+
pendingDashboardSpawns?.set(
|
|
313
|
+
session.cwd,
|
|
314
|
+
(pendingDashboardSpawns?.get(session.cwd) ?? 0) + 1,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
sendTo(ws, {
|
|
318
|
+
type: "resume_result",
|
|
319
|
+
sessionId: msg.sessionId,
|
|
320
|
+
success: degradeResult.success,
|
|
321
|
+
message: degradeResult.success ? FORK_DEGRADED_TO_NEW_MESSAGE : degradeResult.message,
|
|
322
|
+
requestId: msg.requestId,
|
|
323
|
+
...(degradeResult.success ? { code: FORK_DEGRADED_TO_NEW_CODE } : {}),
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
264
326
|
}
|
|
265
|
-
|
|
266
327
|
// For fork-from-message: create a pruned session file first
|
|
267
328
|
let forkSessionFile = session.sessionFile;
|
|
268
329
|
if (msg.mode === "fork" && msg.entryId) {
|
|
269
330
|
try {
|
|
270
331
|
forkSessionFile = createBranchedSessionFile(session.sessionFile, msg.entryId);
|
|
271
332
|
} catch (err: any) {
|
|
272
|
-
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: `Fork from entry failed: ${err.message}
|
|
333
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: `Fork from entry failed: ${err.message}`, requestId: msg.requestId });
|
|
273
334
|
return;
|
|
274
335
|
}
|
|
275
336
|
}
|
|
@@ -289,30 +350,61 @@ export async function handleResumeSession(
|
|
|
289
350
|
mode: msg.mode,
|
|
290
351
|
strategy: resumeConfig.spawnStrategy,
|
|
291
352
|
});
|
|
353
|
+
// Record fork parent keyed by spawn token (was: keyed by cwd, racy on
|
|
354
|
+
// multi-fork-in-same-cwd). See change: spawn-correlation-token.
|
|
355
|
+
if (msg.mode === "fork" && pendingForkRegistry && result.spawnToken) {
|
|
356
|
+
pendingForkRegistry.recordFork(result.spawnToken, msg.sessionId);
|
|
357
|
+
}
|
|
358
|
+
// Record client-correlation so the eventual session_added carries
|
|
359
|
+
// spawnRequestId. See change: spawn-correlation-token.
|
|
360
|
+
if (msg.requestId && result.spawnToken && pendingClientCorrelations) {
|
|
361
|
+
pendingClientCorrelations.record(result.spawnToken, msg.requestId);
|
|
362
|
+
}
|
|
292
363
|
if (result.dashboardSpawned && result.success) {
|
|
293
364
|
pendingDashboardSpawns?.set(session.cwd, (pendingDashboardSpawns?.get(session.cwd) ?? 0) + 1);
|
|
294
365
|
}
|
|
295
366
|
if (result.process && result.pid) {
|
|
296
|
-
headlessPidRegistry.register(result.pid, session.cwd, result.process);
|
|
367
|
+
headlessPidRegistry.register(result.pid, session.cwd, result.process, result.spawnToken);
|
|
297
368
|
}
|
|
298
|
-
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: result.success, message: result.message });
|
|
369
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: result.success, message: result.message, requestId: msg.requestId });
|
|
299
370
|
}
|
|
300
371
|
|
|
301
372
|
export async function handleSpawnSession(
|
|
302
373
|
msg: Extract<BrowserToServerMessage, { type: "spawn_session" }>,
|
|
303
374
|
ctx: BrowserHandlerContext,
|
|
304
375
|
): Promise<void> {
|
|
305
|
-
const { ws, headlessPidRegistry, pendingDashboardSpawns, pendingAttachRegistry, sendTo } = ctx;
|
|
376
|
+
const { ws, headlessPidRegistry, pendingDashboardSpawns, pendingAttachRegistry, pendingClientCorrelations, sendTo } = ctx;
|
|
306
377
|
const config = loadConfig();
|
|
307
378
|
const strategy = config.spawnStrategy ?? "tmux";
|
|
308
379
|
|
|
309
380
|
// Queue the optional attach intent BEFORE awaiting the spawn so a fast
|
|
310
381
|
// bridge `session_register` cannot lose the intent. See change:
|
|
311
|
-
// add-folder-task-checker-and-spawn-attach.
|
|
382
|
+
// add-folder-task-checker-and-spawn-attach. NOTE: at this point we don't
|
|
383
|
+
// yet have a spawnToken (spawn hasn't run); we enqueue by cwd-FIFO and
|
|
384
|
+
// re-record by token after spawnPiSession returns. See change:
|
|
385
|
+
// spawn-correlation-token.
|
|
312
386
|
if (typeof msg.attachProposal === "string" && msg.attachProposal.length > 0) {
|
|
313
387
|
pendingAttachRegistry?.enqueue(msg.cwd, msg.attachProposal);
|
|
314
388
|
}
|
|
315
389
|
|
|
390
|
+
// ── Preflight: fast synchronous checks before spawning. See change: spawn-failure-diagnostics.
|
|
391
|
+
const preflightResolver = new ToolResolver({ processExecPath: process.execPath, useLoginShell: false });
|
|
392
|
+
const preflight = preflightSpawn(msg.cwd, { resolver: preflightResolver });
|
|
393
|
+
if (!preflight.ok) {
|
|
394
|
+
const message = preflight.reasons.map((r) => r.message).join("; ");
|
|
395
|
+
sendTo(ws, { type: "spawn_result", cwd: msg.cwd, success: false, message, requestId: msg.requestId });
|
|
396
|
+
sendTo(ws, { type: "spawn_error", cwd: msg.cwd, strategy, message, code: "PREFLIGHT_FAILED", reasons: preflight.reasons });
|
|
397
|
+
appendSpawnFailure({
|
|
398
|
+
ts: new Date().toISOString(),
|
|
399
|
+
cwd: msg.cwd,
|
|
400
|
+
strategy,
|
|
401
|
+
code: "PREFLIGHT_FAILED",
|
|
402
|
+
message,
|
|
403
|
+
reasons: preflight.reasons,
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
316
408
|
// Catch both thrown exceptions and { success: false } results; surface as
|
|
317
409
|
// spawn_error so the UI can render a retryable banner instead of failing
|
|
318
410
|
// silently. Previous behaviour left the user staring at an empty state
|
|
@@ -320,20 +412,69 @@ export async function handleSpawnSession(
|
|
|
320
412
|
try {
|
|
321
413
|
const spawnResult = await spawnPiSession(msg.cwd, { strategy });
|
|
322
414
|
if (spawnResult.process && spawnResult.pid) {
|
|
323
|
-
headlessPidRegistry.register(spawnResult.pid, msg.cwd, spawnResult.process);
|
|
415
|
+
headlessPidRegistry.register(spawnResult.pid, msg.cwd, spawnResult.process, spawnResult.spawnToken);
|
|
416
|
+
}
|
|
417
|
+
// Record client-correlation so the eventual session_added carries
|
|
418
|
+
// spawnRequestId. See change: spawn-correlation-token.
|
|
419
|
+
if (msg.requestId && spawnResult.spawnToken && pendingClientCorrelations) {
|
|
420
|
+
pendingClientCorrelations.record(spawnResult.spawnToken, msg.requestId);
|
|
324
421
|
}
|
|
325
422
|
if (spawnResult.dashboardSpawned && spawnResult.success) {
|
|
326
423
|
pendingDashboardSpawns?.set(msg.cwd, (pendingDashboardSpawns?.get(msg.cwd) ?? 0) + 1);
|
|
327
424
|
}
|
|
328
|
-
sendTo(ws, {
|
|
425
|
+
sendTo(ws, {
|
|
426
|
+
type: "spawn_result",
|
|
427
|
+
cwd: msg.cwd,
|
|
428
|
+
success: spawnResult.success,
|
|
429
|
+
message: spawnResult.message,
|
|
430
|
+
requestId: msg.requestId,
|
|
431
|
+
...(spawnResult.pid ? { pid: spawnResult.pid } : {}),
|
|
432
|
+
});
|
|
329
433
|
if (!spawnResult.success) {
|
|
330
|
-
sendTo(ws, {
|
|
434
|
+
sendTo(ws, {
|
|
435
|
+
type: "spawn_error",
|
|
436
|
+
cwd: msg.cwd,
|
|
437
|
+
strategy,
|
|
438
|
+
message: spawnResult.message,
|
|
439
|
+
...(spawnResult.code ? { code: spawnResult.code } : {}),
|
|
440
|
+
...(spawnResult.stderr ? { stderr: spawnResult.stderr } : {}),
|
|
441
|
+
});
|
|
442
|
+
appendSpawnFailure({
|
|
443
|
+
ts: new Date().toISOString(),
|
|
444
|
+
cwd: msg.cwd,
|
|
445
|
+
strategy,
|
|
446
|
+
code: spawnResult.code ?? "SPAWN_ERRNO",
|
|
447
|
+
message: spawnResult.message,
|
|
448
|
+
...(spawnResult.stderr ? { stderrTail: spawnResult.stderr } : {}),
|
|
449
|
+
});
|
|
450
|
+
} else {
|
|
451
|
+
// Arm watchdog for every successful spawn. See change: spawn-failure-diagnostics.
|
|
452
|
+
const watchdog = getSpawnRegisterWatchdog();
|
|
453
|
+
watchdog.arm({
|
|
454
|
+
pid: spawnResult.pid,
|
|
455
|
+
cwd: msg.cwd,
|
|
456
|
+
mechanism: strategy as import("@blackbelt-technology/pi-dashboard-shared/platform/spawn-mechanism.js").SpawnMechanism,
|
|
457
|
+
logPath: spawnResult.logPath,
|
|
458
|
+
// Read-on-arm: pass current config value so a Settings change takes effect
|
|
459
|
+
// on the next spawn without a server restart. See change: spawn-failure-diagnostics (fix W1).
|
|
460
|
+
timeoutMs: config.spawnRegisterTimeoutMs,
|
|
461
|
+
ws,
|
|
462
|
+
spawnToken: spawnResult.spawnToken,
|
|
463
|
+
});
|
|
331
464
|
}
|
|
332
465
|
} catch (err) {
|
|
333
466
|
const message = err instanceof Error ? err.message : String(err);
|
|
334
467
|
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr).slice(-2048) : undefined;
|
|
335
|
-
sendTo(ws, { type: "spawn_result", cwd: msg.cwd, success: false, message });
|
|
336
|
-
sendTo(ws, { type: "spawn_error", cwd: msg.cwd, strategy, message, stderr });
|
|
468
|
+
sendTo(ws, { type: "spawn_result", cwd: msg.cwd, success: false, message, requestId: msg.requestId });
|
|
469
|
+
sendTo(ws, { type: "spawn_error", cwd: msg.cwd, strategy, message, code: "SPAWN_ERRNO", stderr });
|
|
470
|
+
appendSpawnFailure({
|
|
471
|
+
ts: new Date().toISOString(),
|
|
472
|
+
cwd: msg.cwd,
|
|
473
|
+
strategy,
|
|
474
|
+
code: "SPAWN_ERRNO",
|
|
475
|
+
message,
|
|
476
|
+
...(stderr ? { stderrTail: stderr } : {}),
|
|
477
|
+
});
|
|
337
478
|
}
|
|
338
479
|
}
|
|
339
480
|
|
|
@@ -68,7 +68,7 @@ async function sendEventBatches(
|
|
|
68
68
|
* Called immediately after every `replayPendingUiRequests` site so the full
|
|
69
69
|
* replay ordering is:
|
|
70
70
|
*
|
|
71
|
-
* events → pending UI requests → ui_modules_list → ui_data_list → ext_ui_decorator
|
|
71
|
+
* asset_register batch → events → pending UI requests → ui_modules_list → ui_data_list → ext_ui_decorator
|
|
72
72
|
*
|
|
73
73
|
* Exported so unit tests can drive it without standing up a full subscribe
|
|
74
74
|
* pipeline. See changes: add-extension-ui-modal, add-extension-ui-decorations.
|
|
@@ -96,6 +96,35 @@ export function replayUiState(
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Replay the per-session image asset registry to a single browser. Sends one
|
|
101
|
+
* `asset_register` message per `(hash, { data, mimeType })` entry in
|
|
102
|
+
* `Session.assets`. Called BEFORE `sendEventBatches` so any `pi-asset:<hash>`
|
|
103
|
+
* tokens in replayed `message_update` / `message_end` events have their
|
|
104
|
+
* referent in the client's session map by the time they're reduced.
|
|
105
|
+
*
|
|
106
|
+
* See change: chat-markdown-local-images-and-math.
|
|
107
|
+
*/
|
|
108
|
+
export function replaySessionAssets(
|
|
109
|
+
ws: WebSocket,
|
|
110
|
+
sessionId: string,
|
|
111
|
+
ctx: Pick<BrowserHandlerContext, "sessionManager" | "sendTo">,
|
|
112
|
+
): void {
|
|
113
|
+
const { sessionManager, sendTo } = ctx;
|
|
114
|
+
const session = sessionManager.get(sessionId);
|
|
115
|
+
if (!session?.assets) return;
|
|
116
|
+
for (const [hash, asset] of Object.entries(session.assets)) {
|
|
117
|
+
if (!asset || typeof asset.data !== "string" || typeof asset.mimeType !== "string") continue;
|
|
118
|
+
sendTo(ws, {
|
|
119
|
+
type: "asset_register",
|
|
120
|
+
sessionId,
|
|
121
|
+
hash,
|
|
122
|
+
mimeType: asset.mimeType,
|
|
123
|
+
data: asset.data,
|
|
124
|
+
} as any);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
99
128
|
export function handleSubscribe(
|
|
100
129
|
msg: Extract<BrowserToServerMessage, { type: "subscribe" }>,
|
|
101
130
|
subs: Set<string>,
|
|
@@ -108,6 +137,8 @@ export function handleSubscribe(
|
|
|
108
137
|
// while the browser is actually subscribed (responses use sendToSubscribers).
|
|
109
138
|
piGateway.sendToSession(msg.sessionId, { type: "request_commands", sessionId: msg.sessionId });
|
|
110
139
|
piGateway.sendToSession(msg.sessionId, { type: "request_models", sessionId: msg.sessionId });
|
|
140
|
+
// See change: replace-hardcoded-provider-lists.
|
|
141
|
+
piGateway.sendToSession(msg.sessionId, { type: "request_providers", sessionId: msg.sessionId });
|
|
111
142
|
piGateway.sendToSession(msg.sessionId, { type: "request_roles", sessionId: msg.sessionId });
|
|
112
143
|
|
|
113
144
|
if (eventStore.hasEvents(msg.sessionId)) {
|
|
@@ -122,6 +153,10 @@ export function handleSubscribe(
|
|
|
122
153
|
if (MAX_REPLAY_EVENTS > 0 && events.length > MAX_REPLAY_EVENTS) {
|
|
123
154
|
events = events.slice(events.length - MAX_REPLAY_EVENTS);
|
|
124
155
|
}
|
|
156
|
+
// Replay asset registry BEFORE events so pi-asset:<hash> tokens in
|
|
157
|
+
// message_update / message_end resolve on first reduce.
|
|
158
|
+
// See change: chat-markdown-local-images-and-math.
|
|
159
|
+
replaySessionAssets(ws, msg.sessionId, ctx);
|
|
125
160
|
markReplaying(ws, msg.sessionId);
|
|
126
161
|
sendEventBatches(ws, msg.sessionId, events, sendTo).then((lastSent) => {
|
|
127
162
|
clearReplaying(ws, msg.sessionId, lastSent);
|
|
@@ -133,8 +168,18 @@ export function handleSubscribe(
|
|
|
133
168
|
if (MAX_REPLAY_EVENTS > 0 && events.length > MAX_REPLAY_EVENTS) {
|
|
134
169
|
events = events.slice(events.length - MAX_REPLAY_EVENTS);
|
|
135
170
|
}
|
|
136
|
-
//
|
|
137
|
-
|
|
171
|
+
// Replay asset registry on every subscribe (delta or full). Cheap when
|
|
172
|
+
// empty; assets already known to the client are simply re-overwritten
|
|
173
|
+
// with identical bytes. See change: chat-markdown-local-images-and-math.
|
|
174
|
+
replaySessionAssets(ws, msg.sessionId, ctx);
|
|
175
|
+
// Suppress live events during paginated replay to prevent out-of-order
|
|
176
|
+
// delivery. The client's `event_replay` reset rule (firstSeq <= maxSeq)
|
|
177
|
+
// misfires if a live `event` arrives between batches and bumps maxSeq
|
|
178
|
+
// past the next batch's firstSeq — wiping state to a fresh build of
|
|
179
|
+
// only the last batch. Suppression+catch-up via clearReplaying preserves
|
|
180
|
+
// ordering for both cold (lastSeq=0) and warm (lastSeq>0) subscribes.
|
|
181
|
+
// See change: fix-cold-subscribe-replay-interleave.
|
|
182
|
+
if (events.length > 0) {
|
|
138
183
|
markReplaying(ws, msg.sessionId);
|
|
139
184
|
sendEventBatches(ws, msg.sessionId, events, sendTo).then((lastSent) => {
|
|
140
185
|
clearReplaying(ws, msg.sessionId, lastSent);
|
|
@@ -172,6 +217,8 @@ export function handleSubscribe(
|
|
|
172
217
|
}
|
|
173
218
|
const subscribers = getSubscribers(msg.sessionId);
|
|
174
219
|
for (const sub of subscribers) {
|
|
220
|
+
// Asset registry first — see change: chat-markdown-local-images-and-math.
|
|
221
|
+
replaySessionAssets(sub, msg.sessionId, ctx);
|
|
175
222
|
await sendEventBatches(sub, msg.sessionId, stored, sendTo);
|
|
176
223
|
replayPendingUiRequests(sub, msg.sessionId);
|
|
177
224
|
replayUiState(sub, msg.sessionId, ctx);
|