@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
|
@@ -45,6 +45,37 @@ export interface DirectoryAddedResult {
|
|
|
45
45
|
openspecData: OpenSpecData;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* `true` if `OpenSpecData` represents "no useful data yet" — either
|
|
50
|
+
* absent, or `{ initialized: false, changes: [] }` (cold cache).
|
|
51
|
+
* Used by broadcast call-sites to decide whether a transition from
|
|
52
|
+
* empty→populated warrants firing `openspec_update`. Pulled into
|
|
53
|
+
* shared scope so `server.ts` (post-install repair) and
|
|
54
|
+
* `session-bootstrap.ts` (initial poll) both use the same predicate.
|
|
55
|
+
*
|
|
56
|
+
* See change: fix-cold-boot-openspec-protocol.
|
|
57
|
+
*/
|
|
58
|
+
export function isOpenSpecDataEmpty(d: OpenSpecData | undefined): boolean {
|
|
59
|
+
if (!d) return true;
|
|
60
|
+
return !d.initialized && (!d.changes || d.changes.length === 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Synchronous, spawn-free probe for `<cwd>/openspec/changes`. Returns
|
|
65
|
+
* `true` iff the path exists and is a directory. Used by the WS
|
|
66
|
+
* on-connect snapshot to disambiguate "no openspec here" from
|
|
67
|
+
* "openspec here, polling pending". ~10 μs per call.
|
|
68
|
+
*
|
|
69
|
+
* See change: fix-cold-boot-openspec-protocol.
|
|
70
|
+
*/
|
|
71
|
+
export function hasOpenSpecDir(cwd: string): boolean {
|
|
72
|
+
try {
|
|
73
|
+
return fs.statSync(path.join(cwd, "openspec", "changes")).isDirectory();
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
export interface DirectoryService {
|
|
49
80
|
knownDirectories(): string[];
|
|
50
81
|
discoverSessions(cwd: string): DiscoveredSession[];
|
|
@@ -11,11 +11,12 @@ import type { PendingForkRegistry } from "./pending-fork-registry.js";
|
|
|
11
11
|
import type { DirectoryService } from "./directory-service.js";
|
|
12
12
|
import { extractSessionUpdates, isActivityEvent, isUnreadTrigger } from "./event-status-extraction.js";
|
|
13
13
|
import type { ViewedSessionTracker } from "./viewed-session-tracker.js";
|
|
14
|
+
import { setCatalogueForSession } from "./provider-catalogue-cache.js";
|
|
14
15
|
import { spawnPiSession } from "./process-manager.js";
|
|
15
16
|
import { loadConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
16
17
|
import { writeSessionMeta } from "@blackbelt-technology/pi-dashboard-shared/session-meta.js";
|
|
17
18
|
import type { DashboardSession } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
18
|
-
import { detectOpenSpecActivity } from "@blackbelt-technology/pi-dashboard-shared/openspec-activity-detector.js";
|
|
19
|
+
import { detectOpenSpecActivity, isValidOpenSpecChangeSlug } from "@blackbelt-technology/pi-dashboard-shared/openspec-activity-detector.js";
|
|
19
20
|
import { extractTurnStats } from "@blackbelt-technology/pi-dashboard-shared/stats-extractor.js";
|
|
20
21
|
import { attachRenameTarget, isNameAutoSetFromAttachment } from "./proposal-attach-naming.js";
|
|
21
22
|
|
|
@@ -42,6 +43,14 @@ export interface EventWiringDeps {
|
|
|
42
43
|
* See change: session-card-unread-stripes.
|
|
43
44
|
*/
|
|
44
45
|
viewedSessionTracker?: ViewedSessionTracker;
|
|
46
|
+
/**
|
|
47
|
+
* Optional client-correlation registry. When provided, the wiring
|
|
48
|
+
* consumes the requestId for the resolved spawnToken after a successful
|
|
49
|
+
* three-tier link and surfaces it on `session_added` as `spawnRequestId`,
|
|
50
|
+
* letting the client auto-select / dismiss its placeholder by exact
|
|
51
|
+
* correlation. See change: spawn-correlation-token.
|
|
52
|
+
*/
|
|
53
|
+
pendingClientCorrelations?: import("./pending-client-correlations.js").PendingClientCorrelations;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
/**
|
|
@@ -61,6 +70,7 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
61
70
|
pendingDashboardSpawns,
|
|
62
71
|
pendingAttachRegistry,
|
|
63
72
|
viewedSessionTracker,
|
|
73
|
+
pendingClientCorrelations,
|
|
64
74
|
} = deps;
|
|
65
75
|
|
|
66
76
|
// Broadcast placeholder session to browsers when auto-created from early events
|
|
@@ -217,10 +227,19 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
217
227
|
// Server-side OpenSpec activity detection from forwarded events
|
|
218
228
|
// Skip during replay — replayed events from a forked session would set stale phase/change
|
|
219
229
|
if (msg.event.eventType === "tool_execution_start" && !replayingSessions.has(sessionId)) {
|
|
220
|
-
const
|
|
230
|
+
const detectedRaw = detectOpenSpecActivity(
|
|
221
231
|
msg.event.data.toolName as string,
|
|
222
232
|
msg.event.data.args as Record<string, unknown> | undefined,
|
|
223
233
|
);
|
|
234
|
+
// Defense-in-depth (see change: fix-uuid-rename-bug). Even if a future
|
|
235
|
+
// detector regression returns a junk-shaped `changeName` (UUID, mixed
|
|
236
|
+
// case, etc.), refuse to stamp openspecChange / attachedProposal /
|
|
237
|
+
// name. Manual attach paths (browser handler, REST) bypass this and
|
|
238
|
+
// accept any name from a server-curated list.
|
|
239
|
+
const detected =
|
|
240
|
+
detectedRaw && (!detectedRaw.changeName || isValidOpenSpecChangeSlug(detectedRaw.changeName))
|
|
241
|
+
? detectedRaw
|
|
242
|
+
: null;
|
|
224
243
|
if (detected) {
|
|
225
244
|
const session = sessionManager.get(sessionId);
|
|
226
245
|
const activityUpdates: Partial<DashboardSession> = {};
|
|
@@ -442,7 +461,34 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
442
461
|
}
|
|
443
462
|
}
|
|
444
463
|
|
|
445
|
-
|
|
464
|
+
// Three-tier link: token → pid → cwd-FIFO. Each tier is independently
|
|
465
|
+
// correct; `linkByToken` is the strong identity introduced by
|
|
466
|
+
// `spawn-correlation-token`. cwd-FIFO is the legacy fallback for old
|
|
467
|
+
// bridges that send neither token nor pid (and is logged so we can see
|
|
468
|
+
// when it actually triggers).
|
|
469
|
+
let linked = false;
|
|
470
|
+
if (msg.spawnToken) {
|
|
471
|
+
linked = browserGateway.headlessPidRegistry.linkByToken(msg.spawnToken, sessionId, msg.pid);
|
|
472
|
+
}
|
|
473
|
+
if (!linked && msg.pid !== undefined) {
|
|
474
|
+
linked = browserGateway.headlessPidRegistry.linkByPid(sessionId, msg.pid);
|
|
475
|
+
}
|
|
476
|
+
if (!linked) {
|
|
477
|
+
if (msg.spawnToken || msg.pid !== undefined) {
|
|
478
|
+
console.error(
|
|
479
|
+
`[event-wiring] cwd-FIFO fallback for session ${sessionId} — token=${msg.spawnToken ?? ""} pid=${msg.pid ?? ""} cwd=${msg.cwd}`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
browserGateway.headlessPidRegistry.linkSession(sessionId, msg.cwd);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Resolve the originating browser `requestId` (when known) so the
|
|
486
|
+
// upcoming session_added broadcast can carry spawnRequestId and the
|
|
487
|
+
// client can auto-select / dismiss its placeholder.
|
|
488
|
+
// See change: spawn-correlation-token.
|
|
489
|
+
const spawnRequestId = (msg.spawnToken && pendingClientCorrelations)
|
|
490
|
+
? pendingClientCorrelations.consume(msg.spawnToken)
|
|
491
|
+
: undefined;
|
|
446
492
|
|
|
447
493
|
const isNewSession = !knownSessionIds.has(sessionId);
|
|
448
494
|
knownSessionIds.add(sessionId);
|
|
@@ -459,7 +505,11 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
459
505
|
}
|
|
460
506
|
}
|
|
461
507
|
|
|
462
|
-
|
|
508
|
+
// Fork-parent lookup is keyed by spawn token (was: cwd, racy on
|
|
509
|
+
// multi-fork-in-same-cwd). See change: spawn-correlation-token.
|
|
510
|
+
const forkParent = msg.spawnToken
|
|
511
|
+
? pendingForkRegistry.consumeFork(msg.spawnToken)
|
|
512
|
+
: undefined;
|
|
463
513
|
sessionOrderManager.insert(msg.cwd, sessionId);
|
|
464
514
|
|
|
465
515
|
if (forkParent) {
|
|
@@ -479,7 +529,7 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
479
529
|
|
|
480
530
|
const updatedSession = sessionManager.get(sessionId);
|
|
481
531
|
if (updatedSession) {
|
|
482
|
-
browserGateway.broadcastSessionAdded(updatedSession);
|
|
532
|
+
browserGateway.broadcastSessionAdded(updatedSession, spawnRequestId ? { spawnRequestId } : undefined);
|
|
483
533
|
}
|
|
484
534
|
|
|
485
535
|
const isNewCwd = !sessionManager.listAll().some(
|
|
@@ -610,6 +660,28 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
610
660
|
} as any);
|
|
611
661
|
}
|
|
612
662
|
|
|
663
|
+
if (msg.type === "providers_list") {
|
|
664
|
+
// Cache the bridge-pushed catalogue. Browsers don't subscribe to it
|
|
665
|
+
// directly; they read via GET /api/provider-auth/status.
|
|
666
|
+
// Broadcast `models_refreshed` ONLY when the catalogue contents
|
|
667
|
+
// actually changed. Routine state-syncs (every fork/resume/reconnect/
|
|
668
|
+
// subscribe) re-send identical content; broadcasting unconditionally
|
|
669
|
+
// wipes every browser's modelsMap and — because App.tsx's
|
|
670
|
+
// auto-subscribe effect skips re-requesting models for any session
|
|
671
|
+
// that's already in `subscribedRef`, leaves previously-visited
|
|
672
|
+
// sessions with an empty model selector until reconnect.
|
|
673
|
+
//
|
|
674
|
+
// The catalogue cache is now a pure read consumer for the Settings
|
|
675
|
+
// UI (`GET /api/provider-auth/status`). No broadcast: the model-
|
|
676
|
+
// selector dropdown lives on the independent `models_list` channel
|
|
677
|
+
// which is per-session-broadcast already; per-session updates are
|
|
678
|
+
// self-healing without a global wipe.
|
|
679
|
+
// See changes: replace-hardcoded-provider-lists,
|
|
680
|
+
// fix-providers-list-spurious-models-refreshed,
|
|
681
|
+
// simplify-model-selection-channels.
|
|
682
|
+
setCatalogueForSession(sessionId, msg.providers);
|
|
683
|
+
}
|
|
684
|
+
|
|
613
685
|
if (msg.type === "roles_list") {
|
|
614
686
|
browserGateway.broadcastToAll({
|
|
615
687
|
type: "roles_list",
|
|
@@ -678,6 +750,34 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
678
750
|
} as any);
|
|
679
751
|
}
|
|
680
752
|
|
|
753
|
+
// ── Asset register: per-session image asset cache + broadcast ──
|
|
754
|
+
// See change: chat-markdown-local-images-and-math.
|
|
755
|
+
if (msg.type === "asset_register") {
|
|
756
|
+
const { hash, mimeType, data } = msg;
|
|
757
|
+
// Reject malformed messages defensively. The bridge always populates
|
|
758
|
+
// these fields; this guard is purely defense-in-depth so a
|
|
759
|
+
// misbehaving extension cannot inject placeholder asset entries.
|
|
760
|
+
if (typeof hash === "string" && hash.length > 0 &&
|
|
761
|
+
typeof mimeType === "string" && mimeType.length > 0 &&
|
|
762
|
+
typeof data === "string" && data.length > 0) {
|
|
763
|
+
const session = sessionManager.get(sessionId);
|
|
764
|
+
if (session) {
|
|
765
|
+
const next = { ...(session.assets ?? {}) };
|
|
766
|
+
next[hash] = { data, mimeType };
|
|
767
|
+
sessionManager.update(sessionId, { assets: next });
|
|
768
|
+
}
|
|
769
|
+
// Broadcast verbatim regardless of whether the session is known —
|
|
770
|
+
// mirrors the Phase-1 / Phase-2 contract for extension UI messages.
|
|
771
|
+
browserGateway.sendToSubscribers(sessionId, {
|
|
772
|
+
type: "asset_register",
|
|
773
|
+
sessionId,
|
|
774
|
+
hash,
|
|
775
|
+
mimeType,
|
|
776
|
+
data,
|
|
777
|
+
} as any);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
681
781
|
// ── Extension UI System (Phase 2): live decorator cache + broadcast ──
|
|
682
782
|
// See change: add-extension-ui-decorations.
|
|
683
783
|
if (msg.type === "ext_ui_decorator") {
|
|
@@ -23,6 +23,13 @@ export interface HeadlessEntry {
|
|
|
23
23
|
process: ChildProcess;
|
|
24
24
|
sessionId?: string;
|
|
25
25
|
spawnedAt: number;
|
|
26
|
+
/**
|
|
27
|
+
* Server-minted spawn correlation token. Stored at `register` time.
|
|
28
|
+
* Used by `linkByToken` (tier 1) to resolve sessionId↔pid mapping
|
|
29
|
+
* deterministically, replacing the racy cwd-FIFO `linkSession`.
|
|
30
|
+
* See change: spawn-correlation-token.
|
|
31
|
+
*/
|
|
32
|
+
spawnToken?: string;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
/** Serialized format for disk persistence */
|
|
@@ -37,9 +44,31 @@ interface PidFileData {
|
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
export interface HeadlessPidRegistry {
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Register a newly spawned headless process. The optional `spawnToken`
|
|
49
|
+
* is the server-minted UUID injected into the spawned process's env;
|
|
50
|
+
* storing it lets `linkByToken` resolve identity precisely later.
|
|
51
|
+
* See change: spawn-correlation-token.
|
|
52
|
+
*/
|
|
53
|
+
register(pid: number, cwd: string, proc: ChildProcess, spawnToken?: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Tier 1 link: find entry by `spawnToken`, set its `sessionId`. Returns
|
|
56
|
+
* `true` on match. The strongest identity — used when the bridge sent
|
|
57
|
+
* `session_register.spawnToken`. See change: spawn-correlation-token.
|
|
58
|
+
*/
|
|
59
|
+
linkByToken(spawnToken: string, sessionId: string, pid?: number): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Tier 2 link: find entry by `pid` (where `!sessionId`), set its
|
|
62
|
+
* `sessionId`. Returns `true` on match. Used when the bridge sent
|
|
63
|
+
* `session_register.pid` but no token. See change: spawn-correlation-token.
|
|
64
|
+
*/
|
|
65
|
+
linkByPid(sessionId: string, pid: number): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Tier 3 (legacy) link: find first entry by `cwd` where `!sessionId`,
|
|
68
|
+
* set its `sessionId`. Returns `true` on match. Cwd-FIFO fallback for
|
|
69
|
+
* old bridges that send neither token nor pid. Race-prone for
|
|
70
|
+
* concurrent same-cwd spawns; tiers 1–2 should pre-empt this.
|
|
71
|
+
*/
|
|
43
72
|
linkSession(sessionId: string, cwd: string): boolean;
|
|
44
73
|
/** Get the PID linked to a session ID. */
|
|
45
74
|
getPid(sessionId: string): number | undefined;
|
|
@@ -84,8 +113,8 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
84
113
|
}
|
|
85
114
|
|
|
86
115
|
return {
|
|
87
|
-
register(pid: number, cwd: string, proc: ChildProcess) {
|
|
88
|
-
entries.set(pid, { pid, cwd, process: proc, spawnedAt: Date.now() });
|
|
116
|
+
register(pid: number, cwd: string, proc: ChildProcess, spawnToken?: string) {
|
|
117
|
+
entries.set(pid, { pid, cwd, process: proc, spawnedAt: Date.now(), spawnToken });
|
|
89
118
|
proc.on("exit", () => {
|
|
90
119
|
entries.delete(pid);
|
|
91
120
|
persist();
|
|
@@ -93,6 +122,26 @@ export function createHeadlessPidRegistry(options?: HeadlessPidRegistryOptions):
|
|
|
93
122
|
persist();
|
|
94
123
|
},
|
|
95
124
|
|
|
125
|
+
linkByToken(spawnToken: string, sessionId: string, _pid?: number): boolean {
|
|
126
|
+
if (!spawnToken) return false;
|
|
127
|
+
for (const entry of entries.values()) {
|
|
128
|
+
if (entry.spawnToken === spawnToken && !entry.sessionId) {
|
|
129
|
+
entry.sessionId = sessionId;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
linkByPid(sessionId: string, pid: number): boolean {
|
|
137
|
+
const entry = entries.get(pid);
|
|
138
|
+
if (entry && !entry.sessionId) {
|
|
139
|
+
entry.sessionId = sessionId;
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
},
|
|
144
|
+
|
|
96
145
|
linkSession(sessionId: string, cwd: string): boolean {
|
|
97
146
|
for (const entry of entries.values()) {
|
|
98
147
|
if (entry.cwd === cwd && !entry.sessionId) {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** Metadata written alongside the lock file. JSON-serialized. */
|
|
2
|
+
export interface LockMetadata {
|
|
3
|
+
pid: number;
|
|
4
|
+
ppid: number;
|
|
5
|
+
httpPort: number;
|
|
6
|
+
piPort: number;
|
|
7
|
+
startedAt: number;
|
|
8
|
+
/** Stable per-instance identifier. Verified against /api/health to detect
|
|
9
|
+
* "port in use by unrelated dashboard or stale process with same pid." */
|
|
10
|
+
identity: string;
|
|
11
|
+
version: string;
|
|
12
|
+
url: string;
|
|
13
|
+
hostname: string;
|
|
14
|
+
}
|
|
15
|
+
/** Result of `acquireOrAttach`. Callers branch on `mode`. */
|
|
16
|
+
export type LockAcquireResult = {
|
|
17
|
+
mode: "acquired";
|
|
18
|
+
meta: LockMetadata;
|
|
19
|
+
/** Release the lock + remove the metadata sidecar. Idempotent. */
|
|
20
|
+
release: () => Promise<void>;
|
|
21
|
+
} | {
|
|
22
|
+
mode: "attach";
|
|
23
|
+
meta: LockMetadata;
|
|
24
|
+
};
|
|
25
|
+
/** Thrown when port is held by an unrelated process. Non-fatal to this
|
|
26
|
+
* module; caller decides (exit with message / retry / override). */
|
|
27
|
+
export declare class InstanceLockMismatchError extends Error {
|
|
28
|
+
readonly meta: LockMetadata;
|
|
29
|
+
readonly observedIdentity: string | null;
|
|
30
|
+
readonly code = "E_INSTANCE_MISMATCH";
|
|
31
|
+
constructor(meta: LockMetadata, observedIdentity: string | null);
|
|
32
|
+
}
|
|
33
|
+
export interface AcquireConfig {
|
|
34
|
+
httpPort: number;
|
|
35
|
+
piPort: number;
|
|
36
|
+
version: string;
|
|
37
|
+
identity?: string;
|
|
38
|
+
/** Injection hooks for tests. Production callers pass no options. */
|
|
39
|
+
hooks?: AcquireHooks;
|
|
40
|
+
}
|
|
41
|
+
export interface AcquireHooks {
|
|
42
|
+
now?: () => number;
|
|
43
|
+
hostname?: () => string;
|
|
44
|
+
lockPath?: string;
|
|
45
|
+
metaPath?: string;
|
|
46
|
+
probeHealth?: (port: number) => Promise<{
|
|
47
|
+
running: boolean;
|
|
48
|
+
pid?: number;
|
|
49
|
+
identity?: string;
|
|
50
|
+
} | null>;
|
|
51
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
52
|
+
/** Stale threshold forwarded to `proper-lockfile`. Default 10s. */
|
|
53
|
+
staleMs?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Canonical HOME directory.
|
|
57
|
+
*
|
|
58
|
+
* Uses `os.userInfo().homedir` in preference to `os.homedir()` because on
|
|
59
|
+
* POSIX the latter honors the `$HOME` environment variable (Node docs say:
|
|
60
|
+
* "On POSIX, it uses the `$HOME` environment variable if defined"), which
|
|
61
|
+
* the design (§4) explicitly prohibits — a GUI-launched process and a
|
|
62
|
+
* shell-launched process would otherwise disagree on "where HOME is".
|
|
63
|
+
* `userInfo().homedir` consults `getpwuid(3)` on POSIX, immune to `$HOME`.
|
|
64
|
+
*
|
|
65
|
+
* On Windows, both APIs ultimately use `USERPROFILE`, so the Git Bash
|
|
66
|
+
* drift case (`$HOME=/c/Users/R` vs `USERPROFILE=C:\Users\R`) is handled
|
|
67
|
+
* either way; keeping `userInfo().homedir` first is still correct.
|
|
68
|
+
*
|
|
69
|
+
* Result is then passed through `fs.realpathSync` to collapse symlinks,
|
|
70
|
+
* FileVault migrations, and other canonicalization drift. Tolerant: falls
|
|
71
|
+
* back to the raw path if realpath fails.
|
|
72
|
+
*/
|
|
73
|
+
export declare function canonicalHomedir(): string;
|
|
74
|
+
/**
|
|
75
|
+
* Lock file path. This is what `proper-lockfile` locks.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getLockPath(homedir?: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Metadata sidecar path (`<lockPath>.meta.json`).
|
|
80
|
+
*/
|
|
81
|
+
export declare function getMetaPath(lockPath?: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Atomically write the metadata sidecar via tmp + rename.
|
|
84
|
+
* Never leaves a partial file visible.
|
|
85
|
+
*/
|
|
86
|
+
export declare function writeMetadataAtomic(meta: LockMetadata, metaPath?: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* Read the metadata sidecar. Returns null on any failure (missing, corrupt,
|
|
89
|
+
* permission-denied). Callers MUST treat null as "assume stale."
|
|
90
|
+
*/
|
|
91
|
+
export declare function readMetadata(metaPath?: string): LockMetadata | null;
|
|
92
|
+
/**
|
|
93
|
+
* Remove the metadata sidecar. Silent on any error (missing is fine).
|
|
94
|
+
*/
|
|
95
|
+
export declare function removeMetadata(metaPath?: string): void;
|
|
96
|
+
/**
|
|
97
|
+
* Determine if the recorded lock holder is a responsive, identity-matching
|
|
98
|
+
* dashboard. Returns:
|
|
99
|
+
* - `"alive-match"`: attach to it
|
|
100
|
+
* - `"alive-mismatch"`: someone else is on that port
|
|
101
|
+
* - `"dead"`: treat as stale, proceed to acquire
|
|
102
|
+
*/
|
|
103
|
+
export declare function isLockHolderResponsive(meta: LockMetadata, hooks?: Pick<AcquireHooks, "probeHealth" | "isProcessAlive">): Promise<"alive-match" | "alive-mismatch" | "dead">;
|
|
104
|
+
/**
|
|
105
|
+
* Acquire the per-HOME lock, or fall back to attach semantics if a live
|
|
106
|
+
* dashboard already holds it.
|
|
107
|
+
*
|
|
108
|
+
* Flow:
|
|
109
|
+
* 1. Ensure `~/.pi/dashboard/` exists (proper-lockfile requires parent).
|
|
110
|
+
* 2. `proper-lockfile.lock(path, { stale, retries: 0 })`
|
|
111
|
+
* ↪ on success: write metadata, return { mode: "acquired", release }
|
|
112
|
+
* ↪ on ELOCKED: read metadata, check liveness
|
|
113
|
+
* - dead: steal via `proper-lockfile.lock({ realpath:false, stale: 0 })`
|
|
114
|
+
* (Note: proper-lockfile already does stale-stealing when
|
|
115
|
+
* `stale` is configured — we just retry once.)
|
|
116
|
+
* - alive-match: return { mode: "attach", meta }
|
|
117
|
+
* - alive-mismatch: throw InstanceLockMismatchError
|
|
118
|
+
*/
|
|
119
|
+
export declare function acquireOrAttach(config: AcquireConfig): Promise<LockAcquireResult>;
|
|
120
|
+
/**
|
|
121
|
+
* True when the user has opted out of the per-HOME lock. Caller should
|
|
122
|
+
* log a warning and skip acquireOrAttach when set.
|
|
123
|
+
*/
|
|
124
|
+
export declare function isLockDisabled(env?: NodeJS.ProcessEnv): boolean;
|