@jmfederico/pi-web 1.202608.0 → 1.202608.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli.js +287 -113
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/CodeViewer-BMWwxG7q.js +4 -0
- package/dist/client/assets/{TerminalPanel-fYrKc3jh.js → TerminalPanel-CacQDIYn.js} +6 -6
- package/dist/client/assets/index-DUW2xnoV.js +4279 -0
- package/dist/client/assets/vendor-editor-core-CXO8gGab.js +12 -0
- package/dist/client/assets/vendor-editor-languages-CpW4sJsX.js +46 -0
- package/dist/client/index.html +3 -3
- package/dist/config.js +110 -73
- package/dist/config.js.map +1 -1
- package/dist/docker/piWebDockerCommandPlan.js +4 -3
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
- package/dist/environment.js +4 -0
- package/dist/environment.js.map +1 -0
- package/dist/nativeServices/installedServiceDefinitions.js +602 -0
- package/dist/nativeServices/installedServiceDefinitions.js.map +1 -0
- package/dist/nativeServices/serviceAction.js +174 -0
- package/dist/nativeServices/serviceAction.js.map +1 -0
- package/dist/nativeServices/serviceDoctor.js +374 -71
- package/dist/nativeServices/serviceDoctor.js.map +1 -1
- package/dist/nativeServices/servicePlan.js +1 -1
- package/dist/nativeServices/servicePlan.js.map +1 -1
- package/dist/{pi-web-plugins → pi-packages}/relays/markdownDocument.js +1 -1
- package/dist/pi-packages/relays/package.json +11 -0
- package/dist/{pi-web-plugins → pi-packages}/relays/pi-web-plugin.js +4 -4
- package/dist/pi-packages/relays/prompts/relay-worktree.md +157 -0
- package/dist/pi-packages/relays/prompts/relay.md +152 -0
- package/dist/{pi-web-plugins → pi-packages}/relays/relayDiscovery.js +1 -1
- package/dist/{pi-web-plugins → pi-packages}/relays/relaysPanelElement.js +1 -1
- package/dist/pi-packages/relays/skills/relay/SKILL.md +123 -0
- package/dist/pi-web-plugins/git/browser/git-contract.js +108 -0
- package/dist/pi-web-plugins/git/browser/git-panel.js +609 -0
- package/dist/pi-web-plugins/git/browser/gitFileList.js +43 -0
- package/dist/pi-web-plugins/git/browser/gitFileShared.js +16 -0
- package/dist/pi-web-plugins/git/browser/gitFileTree.js +81 -0
- package/dist/pi-web-plugins/git/browser/gitFileViewPreference.js +35 -0
- package/dist/pi-web-plugins/git/browser/gitRoute.js +42 -0
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +10 -0
- package/dist/pi-web-plugins/git/browser/unifiedDiff.js +301 -0
- package/dist/{server/git/gitService.js → pi-web-plugins/git/git-backend.js} +209 -60
- package/dist/pi-web-plugins/git/package.json +16 -0
- package/dist/pi-web-plugins/git/server-plugin.js +176 -0
- package/dist/pi-web-plugins/info/infoInternals.js +16 -2
- package/dist/pi-web-plugins/info/package.json +1 -1
- package/dist/pi-web-plugins/info/pi-web-plugin.js +2 -2
- package/dist/pi-web-plugins/updates/package.json +1 -1
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/workspace-tasks/package.json +1 -1
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +3 -3
- package/dist/piWebVersionReport.js +63 -10
- package/dist/piWebVersionReport.js.map +1 -1
- package/dist/plugin-api.d.ts +33 -16
- package/dist/pluginRecoveryCli.js +172 -0
- package/dist/pluginRecoveryCli.js.map +1 -0
- package/dist/server/activity/workspaceActivityService.js +27 -19
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +41 -15
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +3 -21
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/knownAutoInstallPiPackages.js +30 -0
- package/dist/server/knownAutoInstallPiPackages.js.map +1 -0
- package/dist/server/machines/machineClient.js +23 -4
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +74 -17
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +165 -8
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineService.js +23 -0
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/piPackageIdentity.js +25 -0
- package/dist/server/piPackageIdentity.js.map +1 -0
- package/dist/server/piPackageService.js +69 -8
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebPluginCatalog.js +584 -0
- package/dist/server/piWebPluginCatalog.js.map +1 -0
- package/dist/server/piWebPluginLifecycle.js +162 -0
- package/dist/server/piWebPluginLifecycle.js.map +1 -0
- package/dist/server/piWebPluginService.js +159 -261
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebStatus.js +41 -9
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/plugins/pluginBackendProxyRoutes.js +69 -0
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -0
- package/dist/server/plugins/serverPluginExec.js +184 -0
- package/dist/server/plugins/serverPluginExec.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +480 -0
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -0
- package/dist/server/projectTrustRoutes.js +80 -0
- package/dist/server/projectTrustRoutes.js.map +1 -0
- package/dist/server/projects/projectService.js +3 -1
- package/dist/server/projects/projectService.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +26 -11
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/requestCancellation.js +32 -0
- package/dist/server/requestCancellation.js.map +1 -0
- package/dist/server/sessiond/agentProcessEnvironment.js +29 -30
- package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -1
- package/dist/server/sessiond/autoInstallPiPackages.js +63 -0
- package/dist/server/sessiond/autoInstallPiPackages.js.map +1 -0
- package/dist/server/sessiond/pluginBackendRoutes.js +64 -0
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js +24 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +2 -2
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +2 -1
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
- package/dist/server/sessiond/sessiondStateOwnership.js +235 -0
- package/dist/server/sessiond/sessiondStateOwnership.js.map +1 -0
- package/dist/server/sessiond/workspaceCatalogRoutes.js +31 -0
- package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -0
- package/dist/server/sessiond/workspaceRemovalRoutes.js +41 -0
- package/dist/server/sessiond/workspaceRemovalRoutes.js.map +1 -0
- package/dist/server/sessiond.js +284 -98
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/authService.js +15 -54
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/dockerEnvironmentFacts.js +254 -0
- package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -0
- package/dist/server/sessions/modelCatalogRefresher.js +4 -3
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +18 -67
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +495 -142
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +24 -1
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionEnvironmentFacts.js +48 -0
- package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -0
- package/dist/server/sessions/sessionFileFormat.js +27 -0
- package/dist/server/sessions/sessionFileFormat.js.map +1 -0
- package/dist/server/sessions/sessionFileHeader.js +1 -14
- package/dist/server/sessions/sessionFileHeader.js.map +1 -1
- package/dist/server/sessions/sessionModelScope.js +160 -0
- package/dist/server/sessions/sessionModelScope.js.map +1 -0
- package/dist/server/sessions/sessionNameGenerator.js +6 -6
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +55 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionSummaryScanner.js +96 -224
- package/dist/server/sessions/sessionSummaryScanner.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +5 -9
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/spawnTargetResolver.js +1 -1
- package/dist/server/status/machineStatusRoutes.js +8 -0
- package/dist/server/status/machineStatusRoutes.js.map +1 -0
- package/dist/server/status/machineStatusService.js +181 -0
- package/dist/server/status/machineStatusService.js.map +1 -0
- package/dist/server/status/workspaceAttribution.js +87 -0
- package/dist/server/status/workspaceAttribution.js.map +1 -0
- package/dist/server/storage/piPackageDismissalStore.js +78 -0
- package/dist/server/storage/piPackageDismissalStore.js.map +1 -0
- package/dist/server/terminalProxyRoutes.js +2 -1
- package/dist/server/terminalProxyRoutes.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +20 -13
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/fileContentService.js +18 -9
- package/dist/server/workspaces/fileContentService.js.map +1 -1
- package/dist/server/workspaces/filePreviewResponseHeaders.js +18 -0
- package/dist/server/workspaces/filePreviewResponseHeaders.js.map +1 -0
- package/dist/server/workspaces/filePreviewResponsePolicy.js +55 -0
- package/dist/server/workspaces/filePreviewResponsePolicy.js.map +1 -0
- package/dist/server/workspaces/filePreviewService.js +81 -0
- package/dist/server/workspaces/filePreviewService.js.map +1 -0
- package/dist/server/workspaces/projectWorkspaceCwds.js +0 -5
- package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -1
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +402 -0
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -0
- package/dist/server/workspaces/workspaceCatalog.js +47 -0
- package/dist/server/workspaces/workspaceCatalog.js.map +1 -0
- package/dist/server/workspaces/workspaceContext.js +1 -3
- package/dist/server/workspaces/workspaceContext.js.map +1 -1
- package/dist/server/workspaces/workspaceDeletionRoutes.js +35 -125
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
- package/dist/server/workspaces/workspaceProviderRegistry.js +651 -0
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -0
- package/dist/server/workspaces/workspaceRemovalService.js +256 -0
- package/dist/server/workspaces/workspaceRemovalService.js.map +1 -0
- package/dist/server/workspaces/workspaceRouteErrors.js +7 -0
- package/dist/server/workspaces/workspaceRouteErrors.js.map +1 -0
- package/dist/server/workspaces/worktreePreRemoveHook.js +39 -0
- package/dist/server/workspaces/worktreePreRemoveHook.js.map +1 -0
- package/dist/server-plugin-api.d.ts +140 -0
- package/dist/server-plugin-api.js +2 -0
- package/dist/server-plugin-api.js.map +1 -0
- package/dist/serverPluginRecovery.js +138 -0
- package/dist/serverPluginRecovery.js.map +1 -0
- package/dist/sessiond/activeAgentProfile.js +3 -22
- package/dist/sessiond/activeAgentProfile.js.map +1 -1
- package/dist/sessiond/config.js +11 -0
- package/dist/sessiond/config.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +11 -9
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/activeAgentProfile.js +2 -41
- package/dist/shared/activeAgentProfile.js.map +1 -1
- package/dist/shared/activity.js +0 -3
- package/dist/shared/activity.js.map +1 -1
- package/dist/shared/apiTypes.js +7 -5
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +8 -6
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +36 -5
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/machinePluginIds.js +4 -2
- package/dist/shared/machinePluginIds.js.map +1 -1
- package/dist/shared/machineStatus.js +94 -0
- package/dist/shared/machineStatus.js.map +1 -0
- package/dist/shared/piWebStatusParsing.js +33 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +150 -0
- package/dist/shared/pluginApiTypes.js +3 -0
- package/dist/shared/pluginApiTypes.js.map +1 -0
- package/dist/shared/pluginBackendProtocol.js +110 -0
- package/dist/shared/pluginBackendProtocol.js.map +1 -0
- package/dist/shared/pluginIds.js +5 -0
- package/dist/shared/pluginIds.js.map +1 -1
- package/dist/shared/pluginRecoveryCommands.js +14 -0
- package/dist/shared/pluginRecoveryCommands.js.map +1 -0
- package/dist/shared/workspaceFiles.js +41 -2
- package/dist/shared/workspaceFiles.js.map +1 -1
- package/dist/shared/workspaceRemovalProtocol.js +24 -0
- package/dist/shared/workspaceRemovalProtocol.js.map +1 -0
- package/docs/config.md +105 -52
- package/docs/plugins.md +403 -146
- package/examples/workspace-provider-plugin/README.md +52 -0
- package/examples/workspace-provider-plugin/package.json +25 -0
- package/examples/workspace-provider-plugin/src/browser/index.ts +69 -0
- package/examples/workspace-provider-plugin/src/server.ts +64 -0
- package/examples/workspace-provider-plugin/tsconfig.json +21 -0
- package/package.json +30 -11
- package/server-plugin-api.d.ts +1 -0
- package/dist/client/assets/CodeViewer-D91Sp61M.js +0 -4
- package/dist/client/assets/UnifiedDiffViewer-BafLWGtF.js +0 -39
- package/dist/client/assets/index-CA8q9_o7.js +0 -4025
- package/dist/client/assets/vendor-editor-core-vUi74DnD.js +0 -12
- package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +0 -46
- package/dist/pi-web-plugins/relays/package.json +0 -9
- package/dist/plugin-api/unstable.d.ts +0 -22
- package/dist/server/activity/workspaceActivityRoutes.js +0 -4
- package/dist/server/activity/workspaceActivityRoutes.js.map +0 -1
- package/dist/server/git/gitService.js.map +0 -1
- package/dist/server/gitRoutes.js +0 -23
- package/dist/server/gitRoutes.js.map +0 -1
- package/dist/server/sessions/parentSessionLocator.js +0 -75
- package/dist/server/sessions/parentSessionLocator.js.map +0 -1
- package/dist/server/workspaces/gitWorktreeDiscovery.js +0 -43
- package/dist/server/workspaces/gitWorktreeDiscovery.js.map +0 -1
- package/dist/server/workspaces/imagePreviewService.js +0 -40
- package/dist/server/workspaces/imagePreviewService.js.map +0 -1
- package/dist/server/workspaces/workspaceService.js +0 -52
- package/dist/server/workspaces/workspaceService.js.map +0 -1
- package/dist/shared/apiTypes.d.ts +0 -1217
- package/dist/shared/thinkingLevels.d.ts +0 -27
- package/plugin-api/unstable.d.ts +0 -1
- /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/README.md +0 -0
- /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/marked.esm.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readFile,
|
|
4
|
-
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, hasTrustRequiringProjectResources, ProjectTrustStore, readStoredCredential, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
6
6
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
7
7
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
@@ -16,8 +16,6 @@ import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
|
16
16
|
import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
17
17
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
18
18
|
import { readSessionHeaderSummary } from "./sessionFileHeader.js";
|
|
19
|
-
import { countOutOfListingChildren, locateOutOfListingParents } from "./parentSessionLocator.js";
|
|
20
|
-
import { siblingWorkspaceCwds } from "../workspaces/projectWorkspaceCwds.js";
|
|
21
19
|
import { createAskUserToolDefinition } from "./askUserTool.js";
|
|
22
20
|
import { PendingAskStore, renderAskUserAnswersText } from "./pendingAskStore.js";
|
|
23
21
|
import { PendingExtensionDialogStore } from "./pendingExtensionDialogStore.js";
|
|
@@ -30,6 +28,7 @@ import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionC
|
|
|
30
28
|
import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
31
29
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
32
30
|
import { SessionUnreadStore } from "./sessionUnreadStore.js";
|
|
31
|
+
import { applyEnabledModelToggle, catalogWithEnabledFirst, modelScopeId, persistedEnabledModelPatterns, resolveEnabledModelIds, resolveSessionModelOptions, scopedModelsFromEnabledIds } from "./sessionModelScope.js";
|
|
33
32
|
const noopLogger = { info() { } };
|
|
34
33
|
const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
|
|
35
34
|
/**
|
|
@@ -56,6 +55,14 @@ function spawnTargetError(decision) {
|
|
|
56
55
|
return new Error("Spawning session is not in a registered project");
|
|
57
56
|
return new Error(`cwd must be a workspace of this project. Allowed: ${decision.allowedCwds.join(", ")}`);
|
|
58
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Tracked subsessions are worktree-scoped, so a requested target other than the
|
|
60
|
+
* parent's own cwd fails closed instead of being silently retargeted. The
|
|
61
|
+
* message names the rule and both supported ways to get work done elsewhere.
|
|
62
|
+
*/
|
|
63
|
+
function subsessionCwdError(spawningCwd, requestedCwd) {
|
|
64
|
+
return new Error(`A tracked subsession runs in this session's working directory (${spawningCwd}); ${requestedCwd} was requested. Instruct the child to work elsewhere from this workspace, or use spawn_session for an independent session in another workspace.`);
|
|
65
|
+
}
|
|
59
66
|
function modelSpecOf(model) {
|
|
60
67
|
return `${model.provider}/${model.id}`;
|
|
61
68
|
}
|
|
@@ -275,9 +282,182 @@ export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn,
|
|
|
275
282
|
...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
|
|
276
283
|
];
|
|
277
284
|
}
|
|
278
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Run the `project_trust` event over a pre-trust extension set, mirroring
|
|
287
|
+
* `emitProjectTrustEvent` in the SDK's `dist/core/extensions/runner.js`. That
|
|
288
|
+
* helper is not part of the package's public exports (the main index exports
|
|
289
|
+
* only the `ProjectTrust*` types and `ProjectTrustStore`, and the package's
|
|
290
|
+
* `exports` map blocks subpath imports, so `resolveProjectTrusted`/
|
|
291
|
+
* `emitProjectTrustEvent` are not callable from here), so PI WEB reimplements
|
|
292
|
+
* its documented decision loop over the SDK-provided extension objects: per
|
|
293
|
+
* extension, the registered `project_trust` handlers run in order; the first
|
|
294
|
+
* handler returning `yes`/`no` decides and `undecided` falls through to the
|
|
295
|
+
* next handler/extension; a throwing handler is collected as an error and
|
|
296
|
+
* later handlers still get their chance.
|
|
297
|
+
*/
|
|
298
|
+
export async function emitWebProjectTrustEvent(extensionsResult, event, ctx) {
|
|
299
|
+
const errors = [];
|
|
300
|
+
for (const extension of extensionsResult.extensions) {
|
|
301
|
+
// A single extension may register multiple handlers for the same event.
|
|
302
|
+
// The handlers map is keyed exactly as the extension registered it, so a
|
|
303
|
+
// `project_trust` key guarantees `ProjectTrustHandler` entries — the same
|
|
304
|
+
// assumption the SDK's emitProjectTrustEvent makes.
|
|
305
|
+
const handlers = extension.handlers.get("project_trust");
|
|
306
|
+
if (handlers === undefined || handlers.length === 0)
|
|
307
|
+
continue;
|
|
308
|
+
for (const handler of handlers) {
|
|
309
|
+
try {
|
|
310
|
+
const handlerResult = await handler(event, ctx);
|
|
311
|
+
// The SDK reads `trusted` straight off the handler result, so a
|
|
312
|
+
// non-object would throw there; PI WEB reports it as a handler error
|
|
313
|
+
// and lets the next handler/extension try.
|
|
314
|
+
if (typeof handlerResult !== "object" || handlerResult === null) {
|
|
315
|
+
errors.push({ extensionPath: extension.path, error: "project_trust handler returned a non-object result" });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const trusted = "trusted" in handlerResult ? handlerResult.trusted : undefined;
|
|
319
|
+
if (trusted === "undecided") {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
// Rebuild the decision so only the documented `trusted`/`remember`
|
|
323
|
+
// fields carry over — the SDK's resolver reads exactly those two.
|
|
324
|
+
const remember = "remember" in handlerResult ? handlerResult.remember : undefined;
|
|
325
|
+
return {
|
|
326
|
+
result: {
|
|
327
|
+
trusted: trusted === "yes" ? "yes" : "no",
|
|
328
|
+
...(remember === true ? { remember: true } : {}),
|
|
329
|
+
},
|
|
330
|
+
errors,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
errors.push({
|
|
335
|
+
extensionPath: extension.path,
|
|
336
|
+
error: error instanceof Error ? error.message : String(error),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { errors };
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* PI WEB's headless project-trust context. `hasUI` is false because there is
|
|
345
|
+
* no browser trust prompt (chartered behavior: `ask` never loads untrusted
|
|
346
|
+
* resources), so the UI methods are inert — the same no-UI shape the SDK
|
|
347
|
+
* passes when pi runs without a trust UI. The host mode is `rpc`, mirroring
|
|
348
|
+
* how PI WEB binds its session extension contexts.
|
|
349
|
+
*/
|
|
350
|
+
function webProjectTrustContext(cwd) {
|
|
351
|
+
return {
|
|
352
|
+
cwd,
|
|
353
|
+
mode: "rpc",
|
|
354
|
+
hasUI: false,
|
|
355
|
+
ui: {
|
|
356
|
+
select: () => Promise.resolve(undefined),
|
|
357
|
+
confirm: () => Promise.resolve(false),
|
|
358
|
+
input: () => Promise.resolve(undefined),
|
|
359
|
+
notify: () => undefined,
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Resolve whether a workspace's project-local `.pi/` resources may load, the
|
|
365
|
+
* way `pi` resolves it — a faithful mirror of the SDK's `resolveProjectTrusted`
|
|
366
|
+
* (`dist/core/project-trust.js`, also not a public export). PI WEB has no
|
|
367
|
+
* browser trust prompt, so the precedence is, in order:
|
|
368
|
+
*
|
|
369
|
+
* 1. Nothing trust-requiring under `cwd` → trusted.
|
|
370
|
+
* 2. Pre-trust extensions (user/global — project-local ones are not loaded
|
|
371
|
+
* yet) may decide via the `project_trust` event; `remember: true` persists
|
|
372
|
+
* the decision to the agent dir's `trust.json`. Handler errors are reported
|
|
373
|
+
* through {@link WebProjectTrustResolution.onExtensionError} and never
|
|
374
|
+
* abort resolution.
|
|
375
|
+
* 3. Otherwise the saved `trust.json` decision wins.
|
|
376
|
+
* 4. Otherwise `defaultProjectTrust` decides (`always` trusts; `never`/`ask`
|
|
377
|
+
* do not — `ask` cannot prompt in the browser, matching `pi` run without a
|
|
378
|
+
* trust UI).
|
|
379
|
+
*/
|
|
380
|
+
export async function resolveWebProjectTrusted(resolution) {
|
|
381
|
+
const { cwd, trustStore, settingsManager } = resolution;
|
|
382
|
+
if (!hasTrustRequiringProjectResources(cwd))
|
|
383
|
+
return true;
|
|
384
|
+
if (resolution.extensionsResult) {
|
|
385
|
+
const { result, errors } = await emitWebProjectTrustEvent(resolution.extensionsResult, { type: "project_trust", cwd }, webProjectTrustContext(cwd));
|
|
386
|
+
for (const error of errors) {
|
|
387
|
+
resolution.onExtensionError?.(`Extension "${error.extensionPath}" project_trust error: ${error.error}`);
|
|
388
|
+
}
|
|
389
|
+
if (result) {
|
|
390
|
+
const trusted = result.trusted === "yes";
|
|
391
|
+
if (result.remember === true) {
|
|
392
|
+
trustStore.set(cwd, trusted);
|
|
393
|
+
}
|
|
394
|
+
return trusted;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const saved = trustStore.get(cwd);
|
|
398
|
+
if (saved !== null)
|
|
399
|
+
return saved;
|
|
400
|
+
return settingsManager.getDefaultProjectTrust() === "always";
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Resource-loader options that append PI WEB's own system-prompt sections.
|
|
404
|
+
*
|
|
405
|
+
* `appendSystemPromptOverride` composes with what the loader already resolved,
|
|
406
|
+
* so the operator's `SYSTEM.md` / `APPEND_SYSTEM.md` files keep their content
|
|
407
|
+
* and PI WEB's sections land after them. Returns `undefined` when there is
|
|
408
|
+
* nothing to append, leaving the loader exactly as pi configures it.
|
|
409
|
+
*/
|
|
410
|
+
export function piWebResourceLoaderOptions(appendSystemPromptSections) {
|
|
411
|
+
if (appendSystemPromptSections.length === 0)
|
|
412
|
+
return undefined;
|
|
413
|
+
return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
|
|
414
|
+
}
|
|
415
|
+
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
|
|
416
|
+
const resourceLoaderOptions = piWebResourceLoaderOptions(appendSystemPromptSections);
|
|
279
417
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
280
|
-
|
|
418
|
+
// PI WEB always honors pi's project-trust model. When the workspace ships
|
|
419
|
+
// trust-requiring resources, trust is resolved exactly once, mirroring the
|
|
420
|
+
// SDK's flow: the resource loader first loads the pre-trust extension set
|
|
421
|
+
// (user/global; project-local ones stay out) and calls back with it, so
|
|
422
|
+
// those extensions may decide via the `project_trust` event; the resolved
|
|
423
|
+
// value then lands in the SettingsManager before any project-local
|
|
424
|
+
// resource (extensions, packages, settings, prompts) loads. With no
|
|
425
|
+
// browser trust prompt, an untrusted project's resources are skipped
|
|
426
|
+
// (matching `pi` run without a UI). Projects without trust-requiring
|
|
427
|
+
// resources skip resolution entirely and are trusted, as before.
|
|
428
|
+
const projectTrustRequiring = hasTrustRequiringProjectResources(cwd);
|
|
429
|
+
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: !projectTrustRequiring });
|
|
430
|
+
// Pre-session-creation trust failures (`project_trust` handler errors)
|
|
431
|
+
// land in the runtime diagnostics next to the services diagnostics,
|
|
432
|
+
// exactly as the CLI appends its project-trust diagnostics.
|
|
433
|
+
const projectTrustDiagnostics = [];
|
|
434
|
+
const services = await createAgentSessionServices({
|
|
435
|
+
cwd,
|
|
436
|
+
agentDir,
|
|
437
|
+
modelRuntime,
|
|
438
|
+
settingsManager,
|
|
439
|
+
...(resourceLoaderOptions === undefined ? {} : { resourceLoaderOptions }),
|
|
440
|
+
...(projectTrustRequiring
|
|
441
|
+
? {
|
|
442
|
+
resourceLoaderReloadOptions: {
|
|
443
|
+
resolveProjectTrust: async ({ extensionsResult }) => resolveWebProjectTrusted({
|
|
444
|
+
cwd,
|
|
445
|
+
trustStore: new ProjectTrustStore(agentDir),
|
|
446
|
+
settingsManager,
|
|
447
|
+
extensionsResult,
|
|
448
|
+
onExtensionError: (message) => projectTrustDiagnostics.push({ type: "warning", message }),
|
|
449
|
+
}),
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
: {}),
|
|
453
|
+
});
|
|
454
|
+
const modelOptions = await resolveSessionModelOptions({
|
|
455
|
+
services,
|
|
456
|
+
hasExistingSession: sessionManager.buildSessionContext().messages.length > 0,
|
|
457
|
+
...(initialModel === undefined ? {} : { initialModel }),
|
|
458
|
+
...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
|
|
459
|
+
});
|
|
460
|
+
services.diagnostics.push(...modelOptions.diagnostics);
|
|
281
461
|
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
282
462
|
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
283
463
|
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, askUser);
|
|
@@ -286,10 +466,11 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
286
466
|
sessionManager,
|
|
287
467
|
customTools,
|
|
288
468
|
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
289
|
-
...(
|
|
290
|
-
...(
|
|
469
|
+
...(modelOptions.model === undefined ? {} : { model: modelOptions.model }),
|
|
470
|
+
...(modelOptions.thinkingLevel === undefined ? {} : { thinkingLevel: modelOptions.thinkingLevel }),
|
|
471
|
+
...(modelOptions.scopedModels.length === 0 ? {} : { scopedModels: modelOptions.scopedModels }),
|
|
291
472
|
});
|
|
292
|
-
return { ...result, services, diagnostics: services.diagnostics };
|
|
473
|
+
return { ...result, services, diagnostics: [...projectTrustDiagnostics, ...services.diagnostics] };
|
|
293
474
|
};
|
|
294
475
|
}
|
|
295
476
|
function createPiWebEditToolDefinition(cwd) {
|
|
@@ -331,6 +512,16 @@ export class PiSessionService {
|
|
|
331
512
|
this.treeNavigations = new WeakSet();
|
|
332
513
|
/** Counts async operations that may append an entry before they settle. */
|
|
333
514
|
this.sessionEntryMutationCounts = new WeakMap();
|
|
515
|
+
/** Settings-wide queue preventing enabled-model read/modify/write races across sessions. */
|
|
516
|
+
this.modelScopeMutationQueue = Promise.resolve();
|
|
517
|
+
/** Global and workspace-owned enabled-model states; live session scopes project one entry lazily. */
|
|
518
|
+
this.modelScopeStates = new Map();
|
|
519
|
+
/** Coalesces initialization reads independently for the global and each workspace scope. */
|
|
520
|
+
this.modelScopeInitializations = new Map();
|
|
521
|
+
/** Monotonic revision published for global scope changes and consumed by clients. */
|
|
522
|
+
this.modelScopeRevision = 0;
|
|
523
|
+
/** Last scope/catalog revision projected into each runtime, synchronized lazily on use. */
|
|
524
|
+
this.modelScopeCache = new WeakMap();
|
|
334
525
|
/** Runtime/session-identity reservations for operations that must not overlap tree navigation. */
|
|
335
526
|
this.treeExclusiveRuntimeOperationCounts = new WeakMap();
|
|
336
527
|
this.treeExclusiveSessionOperationCounts = new Map();
|
|
@@ -347,13 +538,6 @@ export class PiSessionService {
|
|
|
347
538
|
this.subsessionLinks = new Map();
|
|
348
539
|
/** Parent id/file identities whose persisted links have already been loaded. */
|
|
349
540
|
this.subsessionHydratedParents = new Set();
|
|
350
|
-
/**
|
|
351
|
-
* Session file path -> its parsed header and the file identity at read
|
|
352
|
-
* time. Headers are written once, so identity-verified reads are cached.
|
|
353
|
-
*/
|
|
354
|
-
this.sessionHeaderCache = new Map();
|
|
355
|
-
/** Session file path -> its in-flight header read, so concurrent scans share one read per file. */
|
|
356
|
-
this.pendingSessionHeaderReads = new Map();
|
|
357
541
|
/**
|
|
358
542
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
359
543
|
* Armed when the child starts working; firing on completion disarms it so a
|
|
@@ -366,46 +550,23 @@ export class PiSessionService {
|
|
|
366
550
|
this.pendingUnreadMutations = [];
|
|
367
551
|
this.unreadPublicationFlushRequested = false;
|
|
368
552
|
this.unreadPublicationStopped = false;
|
|
369
|
-
/**
|
|
370
|
-
* Read a session file header, memoized per path, with one in-flight read
|
|
371
|
-
* per path: concurrent scans of a shared session directory share the read
|
|
372
|
-
* instead of each opening the file. Pi writes the header once at session
|
|
373
|
-
* creation, so a successful read stays valid as long as the path still
|
|
374
|
-
* holds the same file: every hit re-checks the file's identity (dev/ino),
|
|
375
|
-
* and a replacement landed at a cached path is re-read instead of serving
|
|
376
|
-
* the previous file's header — the summary scanner detects such
|
|
377
|
-
* replacements, so the listing and the header reads must agree. The one
|
|
378
|
-
* thing identity cannot detect is an in-place rewrite that keeps the
|
|
379
|
-
* inode; detach, the only such writer, deletes the entry itself. Failures
|
|
380
|
-
* are not cached so a session file that appears later is picked up.
|
|
381
|
-
*/
|
|
382
|
-
this.readCachedSessionHeader = (sessionFile) => {
|
|
383
|
-
const pending = this.pendingSessionHeaderReads.get(sessionFile);
|
|
384
|
-
if (pending !== undefined)
|
|
385
|
-
return pending;
|
|
386
|
-
const read = this.loadSessionHeader(sessionFile).finally(() => {
|
|
387
|
-
this.pendingSessionHeaderReads.delete(sessionFile);
|
|
388
|
-
});
|
|
389
|
-
this.pendingSessionHeaderReads.set(sessionFile, read);
|
|
390
|
-
return read;
|
|
391
|
-
};
|
|
392
553
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
393
554
|
this.agentDir = deps.agentDir;
|
|
394
555
|
this.sessionManager = deps.sessionManager;
|
|
395
556
|
this.modelRuntime = deps.modelRuntime;
|
|
396
557
|
this.spawnTargets = deps.spawnTargets;
|
|
397
|
-
this.projectWorkspaces = deps.projectWorkspaces;
|
|
398
558
|
this.logger = deps.logger ?? noopLogger;
|
|
399
559
|
this.now = deps.now ?? (() => new Date());
|
|
400
560
|
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
401
561
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
562
|
+
this.onUnreadChanged = deps.onUnreadChanged;
|
|
402
563
|
this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
|
|
403
564
|
this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
|
|
404
565
|
this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
|
|
405
566
|
this.catalogRefreshStatus = deps.catalogRefreshStatus;
|
|
406
567
|
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
407
568
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
408
|
-
// Subsessions are
|
|
569
|
+
// Subsessions are gated behind their own flag, and they
|
|
409
570
|
// also require the spawn capability (they share its project-scope resolver).
|
|
410
571
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
411
572
|
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
@@ -413,7 +574,7 @@ export class PiSessionService {
|
|
|
413
574
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
414
575
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
415
576
|
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
416
|
-
}, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined);
|
|
577
|
+
}, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined, deps.appendSystemPromptSections ?? []);
|
|
417
578
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
418
579
|
this.workspaceActivity = deps.workspaceActivity;
|
|
419
580
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
@@ -541,8 +702,6 @@ export class PiSessionService {
|
|
|
541
702
|
this.subsessionLinks.clear();
|
|
542
703
|
this.subsessionHydratedParents.clear();
|
|
543
704
|
this.subsessionNotifyArmed.clear();
|
|
544
|
-
this.sessionHeaderCache.clear();
|
|
545
|
-
this.pendingSessionHeaderReads.clear();
|
|
546
705
|
this.notificationStore.clearAll("service-dispose");
|
|
547
706
|
await Promise.all(activeSessions.map(async (active) => {
|
|
548
707
|
active.unsubscribe();
|
|
@@ -573,83 +732,7 @@ export class PiSessionService {
|
|
|
573
732
|
.sort(compareArchivedRecords)
|
|
574
733
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
575
734
|
.filter(isDefined);
|
|
576
|
-
return
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Annotate a listing with the session relationships that cross workspace
|
|
580
|
-
* boundaries: where an out-of-listing parent lives, and how many children a
|
|
581
|
-
* listed session has in sibling workspaces.
|
|
582
|
-
*
|
|
583
|
-
* Both directions are best-effort. An unreadable parent header or a sibling
|
|
584
|
-
* workspace that cannot be listed leaves the session unannotated rather than
|
|
585
|
-
* failing the listing, and the browser falls back to its generic states.
|
|
586
|
-
*/
|
|
587
|
-
async withRelatedSessionsElsewhere(sessions, cwd) {
|
|
588
|
-
const [parentLocations, childCounts] = await Promise.all([
|
|
589
|
-
locateOutOfListingParents(sessions, cwd, this.readCachedSessionHeader),
|
|
590
|
-
this.countChildrenInSiblingWorkspaces(sessions, cwd),
|
|
591
|
-
]);
|
|
592
|
-
return sessions.map((session) => {
|
|
593
|
-
const parent = session.parentSessionPath === undefined ? undefined : parentLocations.get(session.parentSessionPath);
|
|
594
|
-
const childrenElsewhere = childCounts.get(session.path);
|
|
595
|
-
if (parent === undefined && childrenElsewhere === undefined)
|
|
596
|
-
return session;
|
|
597
|
-
const annotated = { ...session };
|
|
598
|
-
if (parent !== undefined) {
|
|
599
|
-
annotated.parentSessionId = parent.parentSessionId;
|
|
600
|
-
annotated.parentSessionCwd = parent.parentSessionCwd;
|
|
601
|
-
}
|
|
602
|
-
if (childrenElsewhere !== undefined)
|
|
603
|
-
annotated.childSessionsElsewhere = childrenElsewhere;
|
|
604
|
-
return annotated;
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* Count children of the listed sessions that live in other workspaces of the
|
|
609
|
-
* same project.
|
|
610
|
-
*
|
|
611
|
-
* Only sibling workspaces are scanned: agents may only spawn into workspaces
|
|
612
|
-
* of the spawning session's own project, so that bounds where a child can be.
|
|
613
|
-
* Each sibling scan reads only the header line of every session file (the
|
|
614
|
-
* only place `parentSessionPath` is recorded), never the transcripts. Listing
|
|
615
|
-
* is skipped entirely when no project-workspace locator is configured or the
|
|
616
|
-
* cwd belongs to no registered project.
|
|
617
|
-
*/
|
|
618
|
-
async countChildrenInSiblingWorkspaces(sessions, cwd) {
|
|
619
|
-
if (this.projectWorkspaces === undefined || sessions.length === 0)
|
|
620
|
-
return new Map();
|
|
621
|
-
try {
|
|
622
|
-
const siblingCwds = await siblingWorkspaceCwds(this.projectWorkspaces, cwd);
|
|
623
|
-
if (siblingCwds.length === 0)
|
|
624
|
-
return new Map();
|
|
625
|
-
const listings = await Promise.all(siblingCwds.map((siblingCwd) => this.sessionManager.listParentSessionPaths(siblingCwd, this.readCachedSessionHeader)));
|
|
626
|
-
return countOutOfListingChildren(sessions, listings.flat());
|
|
627
|
-
}
|
|
628
|
-
catch (error) {
|
|
629
|
-
this.logger.info({ cwd, error: error instanceof Error ? error.message : String(error) }, "failed to count child sessions in sibling workspaces");
|
|
630
|
-
return new Map();
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
async loadSessionHeader(sessionFile) {
|
|
634
|
-
let stats;
|
|
635
|
-
try {
|
|
636
|
-
stats = await stat(sessionFile);
|
|
637
|
-
}
|
|
638
|
-
catch {
|
|
639
|
-
// Gone or unreadable: report no header and forget any cached entry.
|
|
640
|
-
this.sessionHeaderCache.delete(sessionFile);
|
|
641
|
-
return undefined;
|
|
642
|
-
}
|
|
643
|
-
const cached = this.sessionHeaderCache.get(sessionFile);
|
|
644
|
-
if (cached?.dev === stats.dev && cached.ino === stats.ino)
|
|
645
|
-
return cached.header;
|
|
646
|
-
const header = await readSessionHeaderSummary(sessionFile);
|
|
647
|
-
if (header === undefined) {
|
|
648
|
-
this.sessionHeaderCache.delete(sessionFile);
|
|
649
|
-
return undefined;
|
|
650
|
-
}
|
|
651
|
-
this.sessionHeaderCache.set(sessionFile, { header, dev: stats.dev, ino: stats.ino });
|
|
652
|
-
return header;
|
|
735
|
+
return [...unarchivedSessions, ...archivedSessions];
|
|
653
736
|
}
|
|
654
737
|
async start(cwd, options = {}) {
|
|
655
738
|
return this.startSession(cwd, options);
|
|
@@ -711,15 +794,22 @@ export class PiSessionService {
|
|
|
711
794
|
};
|
|
712
795
|
}
|
|
713
796
|
/**
|
|
714
|
-
* Start a *tracked* child session on behalf of a LLM.
|
|
715
|
-
* {@link spawnSession}
|
|
716
|
-
*
|
|
717
|
-
*
|
|
797
|
+
* Start a *tracked* child session on behalf of a LLM. Unlike
|
|
798
|
+
* {@link spawnSession}, a tracked child always runs in the parent's own
|
|
799
|
+
* workspace: parent/child trees are worktree-scoped, so a child elsewhere
|
|
800
|
+
* would be invisible to the parent's listing. The child records its parent
|
|
801
|
+
* (so it shows in the session tree) and is registered so the parent is
|
|
802
|
+
* notified when it stops working and can inspect it later.
|
|
718
803
|
*/
|
|
719
804
|
async spawnSubsession(input) {
|
|
720
805
|
if (this.spawnTargets === undefined)
|
|
721
806
|
throw new Error("Spawning sessions is disabled");
|
|
722
|
-
|
|
807
|
+
if (input.cwd !== undefined && input.cwd !== "" && !cwdPathsEqual(input.cwd, input.spawningCwd)) {
|
|
808
|
+
throw subsessionCwdError(input.spawningCwd, input.cwd);
|
|
809
|
+
}
|
|
810
|
+
// Resolved against the parent's own cwd only: this still refuses spawning
|
|
811
|
+
// from an unregistered directory, which keeps the child visible in the UI.
|
|
812
|
+
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, undefined);
|
|
723
813
|
if (!decision.allowed)
|
|
724
814
|
throw spawnTargetError(decision);
|
|
725
815
|
// A model spec overrides the inherited model and is resolved against the
|
|
@@ -754,15 +844,117 @@ export class PiSessionService {
|
|
|
754
844
|
};
|
|
755
845
|
}
|
|
756
846
|
/**
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
847
|
+
* Resolve the settings domain for a session. Pi's settings manager deep-merges
|
|
848
|
+
* objects, but an `enabledModels` array in `.pi/settings.json` replaces the
|
|
849
|
+
* global array, so that workspace gets its own effective scope and is not
|
|
850
|
+
* affected by global picker edits.
|
|
851
|
+
*/
|
|
852
|
+
modelScopeContext(session) {
|
|
853
|
+
const projectSettings = session.settingsManager.getProjectSettings?.() ?? {};
|
|
854
|
+
if (!Object.hasOwn(projectSettings, "enabledModels")) {
|
|
855
|
+
return { key: "global", source: "global", editable: true };
|
|
856
|
+
}
|
|
857
|
+
const workspace = canonicalizeStoredCwd(session.sessionManager.getCwd());
|
|
858
|
+
return { key: `workspace:${workspace}`, source: "workspace", editable: false };
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Read the effective scope for one settings domain. Global sessions share one
|
|
862
|
+
* snapshot; project-overridden sessions share a snapshot keyed by workspace.
|
|
863
|
+
* Neither path enumerates or mutates other active sessions.
|
|
864
|
+
*/
|
|
865
|
+
async modelScopeSnapshot(session, context = this.modelScopeContext(session)) {
|
|
866
|
+
const existing = this.modelScopeStates.get(context.key);
|
|
867
|
+
if (existing !== undefined)
|
|
868
|
+
return existing;
|
|
869
|
+
const pending = this.modelScopeInitializations.get(context.key);
|
|
870
|
+
if (pending !== undefined)
|
|
871
|
+
return pending;
|
|
872
|
+
const initialization = (async () => {
|
|
873
|
+
const enabledIds = await resolveEnabledModelIds(sessionScopeSource(session));
|
|
874
|
+
const snapshot = {
|
|
875
|
+
enabledIds: enabledIds === null ? null : [...enabledIds],
|
|
876
|
+
revision: 0,
|
|
877
|
+
};
|
|
878
|
+
this.modelScopeStates.set(context.key, snapshot);
|
|
879
|
+
return snapshot;
|
|
880
|
+
})();
|
|
881
|
+
this.modelScopeInitializations.set(context.key, initialization);
|
|
882
|
+
try {
|
|
883
|
+
return await initialization;
|
|
884
|
+
}
|
|
885
|
+
finally {
|
|
886
|
+
if (this.modelScopeInitializations.get(context.key) === initialization)
|
|
887
|
+
this.modelScopeInitializations.delete(context.key);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Project one settings-domain scope into one SDK session only when that
|
|
892
|
+
* session is used. `PiAgentSession` needs an in-memory array for cycling, but
|
|
893
|
+
* that array is a cache; the keyed snapshot above is the source of truth.
|
|
894
|
+
*/
|
|
895
|
+
async synchronizeSessionModelScope(session, available) {
|
|
896
|
+
const context = this.modelScopeContext(session);
|
|
897
|
+
const snapshot = await this.modelScopeSnapshot(session, context);
|
|
898
|
+
const catalogKey = available.map(modelScopeId).join("\0");
|
|
899
|
+
const cached = this.modelScopeCache.get(session);
|
|
900
|
+
if (cached?.scopeKey === context.key && cached.revision === snapshot.revision && cached.catalogKey === catalogKey)
|
|
901
|
+
return;
|
|
902
|
+
session.setScopedModels(scopedModelsFromEnabledIds(available, snapshot.enabledIds, session.scopedModels));
|
|
903
|
+
this.modelScopeCache.set(session, { scopeKey: context.key, revision: snapshot.revision, catalogKey });
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* The models a session may pick from: its synchronized scoped set when
|
|
907
|
+
* model-scoped, otherwise the runtime's available snapshot. Refreshes the
|
|
908
|
+
* shared runtime catalog first so callers see newly configured providers and
|
|
909
|
+
* models. The refresh stays local (`allowNetwork: false`); network refreshes
|
|
910
|
+
* belong to the bounded background catalog refresher, not this request path.
|
|
760
911
|
*/
|
|
761
912
|
async sessionModelCandidates(session) {
|
|
762
|
-
await session.modelRuntime.refresh();
|
|
913
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
914
|
+
const available = session.modelRuntime.getAvailableSnapshot();
|
|
915
|
+
await this.synchronizeSessionModelScope(session, available);
|
|
763
916
|
return session.scopedModels.length > 0
|
|
764
917
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
765
|
-
:
|
|
918
|
+
: available;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* The session machine's full available catalog with per-model enabled state,
|
|
922
|
+
* ordered enabled-first for scope semantics while retaining each model's
|
|
923
|
+
* natural catalog index for stable picker placement. The enabled state comes
|
|
924
|
+
* from the keyed scope snapshot rather than a session-local SettingsManager.
|
|
925
|
+
*/
|
|
926
|
+
async enabledModelCatalog(session) {
|
|
927
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
928
|
+
const available = session.modelRuntime.getAvailableSnapshot();
|
|
929
|
+
await this.synchronizeSessionModelScope(session, available);
|
|
930
|
+
const context = this.modelScopeContext(session);
|
|
931
|
+
const enabledIds = (await this.modelScopeSnapshot(session, context)).enabledIds;
|
|
932
|
+
return catalogWithEnabledFirst(available, enabledIds);
|
|
933
|
+
}
|
|
934
|
+
runModelScopeMutation(operation) {
|
|
935
|
+
const queued = this.modelScopeMutationQueue.then(operation);
|
|
936
|
+
this.modelScopeMutationQueue = queued.then(() => undefined, () => undefined);
|
|
937
|
+
return queued;
|
|
938
|
+
}
|
|
939
|
+
/** Persist one global enabled-model selection and publish its new revision. */
|
|
940
|
+
applyEnabledModelScope(session, available, enabledIds) {
|
|
941
|
+
const availableIds = available.map(modelScopeId);
|
|
942
|
+
const persisted = persistedEnabledModelPatterns(enabledIds, availableIds);
|
|
943
|
+
const nextState = {
|
|
944
|
+
// Pi normalizes a scope covering the complete current catalog to an
|
|
945
|
+
// omitted setting, which is the global canonical all-enabled state.
|
|
946
|
+
enabledIds: persisted === undefined ? null : enabledIds === null ? [] : [...enabledIds],
|
|
947
|
+
revision: ++this.modelScopeRevision,
|
|
948
|
+
};
|
|
949
|
+
session.settingsManager.setEnabledModels(persisted);
|
|
950
|
+
this.modelScopeStates.set("global", nextState);
|
|
951
|
+
session.setScopedModels(scopedModelsFromEnabledIds(available, nextState.enabledIds, session.scopedModels));
|
|
952
|
+
this.modelScopeCache.set(session, {
|
|
953
|
+
scopeKey: "global",
|
|
954
|
+
revision: nextState.revision,
|
|
955
|
+
catalogKey: available.map(modelScopeId).join("\0"),
|
|
956
|
+
});
|
|
957
|
+
this.events.publishGlobal({ type: "models.changed", revision: nextState.revision });
|
|
766
958
|
}
|
|
767
959
|
/**
|
|
768
960
|
* Resolve a strict `provider/model-id` spec from a spawn tool against the
|
|
@@ -1357,6 +1549,68 @@ export class PiSessionService {
|
|
|
1357
1549
|
const models = await this.sessionModelCandidates(session);
|
|
1358
1550
|
return models.map(modelToClientModel);
|
|
1359
1551
|
}
|
|
1552
|
+
async modelCatalog(ref) {
|
|
1553
|
+
const session = await this.getOrOpen(ref);
|
|
1554
|
+
const scope = this.modelScopeContext(session);
|
|
1555
|
+
return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry, scope.editable));
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Add/remove one model to/from the global pi `enabledModels` scope. A
|
|
1559
|
+
* workspace `.pi/settings.json` override remains authoritative for that
|
|
1560
|
+
* workspace and is intentionally read-only in this picker.
|
|
1561
|
+
*/
|
|
1562
|
+
async setModelEnabled(ref, provider, modelId, enabled) {
|
|
1563
|
+
await this.assertWritable(ref);
|
|
1564
|
+
const session = await this.getOrOpen(ref);
|
|
1565
|
+
return this.runModelScopeMutation(async () => {
|
|
1566
|
+
const scope = this.modelScopeContext(session);
|
|
1567
|
+
if (scope.source !== "global")
|
|
1568
|
+
throw new Error("Model availability is controlled by this workspace's .pi/settings.json");
|
|
1569
|
+
this.assertTreeNavigationInactive(session, "change enabled models");
|
|
1570
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1571
|
+
const currentIds = (await this.modelScopeSnapshot(session, scope)).enabledIds;
|
|
1572
|
+
const available = session.modelRuntime.getAvailableSnapshot();
|
|
1573
|
+
const availableIds = available.map(modelScopeId);
|
|
1574
|
+
const targetId = `${provider}/${modelId}`;
|
|
1575
|
+
if (!availableIds.includes(targetId))
|
|
1576
|
+
throw new Error(`Model not found: ${targetId}`);
|
|
1577
|
+
if (!enabled && session.model !== undefined && modelScopeId(session.model) === targetId) {
|
|
1578
|
+
throw new Error("Current model cannot be disabled");
|
|
1579
|
+
}
|
|
1580
|
+
const nextIds = applyEnabledModelToggle(currentIds, availableIds, targetId, enabled);
|
|
1581
|
+
this.assertTreeNavigationInactive(session, "change enabled models");
|
|
1582
|
+
if (nextIds !== currentIds)
|
|
1583
|
+
this.applyEnabledModelScope(session, available, nextIds);
|
|
1584
|
+
// Respond from a fresh post-edit read so the response is exactly what
|
|
1585
|
+
// GET models/catalog returns after the edit, including pi's normalizations.
|
|
1586
|
+
return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
/** Atomically expose every model or narrow the global enabled scope to the current model. */
|
|
1590
|
+
async setModelScope(ref, mode) {
|
|
1591
|
+
await this.assertWritable(ref);
|
|
1592
|
+
const session = await this.getOrOpen(ref);
|
|
1593
|
+
return this.runModelScopeMutation(async () => {
|
|
1594
|
+
const scope = this.modelScopeContext(session);
|
|
1595
|
+
if (scope.source !== "global")
|
|
1596
|
+
throw new Error("Model availability is controlled by this workspace's .pi/settings.json");
|
|
1597
|
+
this.assertTreeNavigationInactive(session, "change enabled models");
|
|
1598
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1599
|
+
const available = session.modelRuntime.getAvailableSnapshot();
|
|
1600
|
+
const availableIds = available.map(modelScopeId);
|
|
1601
|
+
let nextIds = null;
|
|
1602
|
+
if (mode === "current") {
|
|
1603
|
+
const current = session.model;
|
|
1604
|
+
if (current === undefined || !availableIds.includes(modelScopeId(current))) {
|
|
1605
|
+
throw new Error("Current model is unavailable");
|
|
1606
|
+
}
|
|
1607
|
+
nextIds = [modelScopeId(current)];
|
|
1608
|
+
}
|
|
1609
|
+
this.assertTreeNavigationInactive(session, "change enabled models");
|
|
1610
|
+
this.applyEnabledModelScope(session, available, nextIds);
|
|
1611
|
+
return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1360
1614
|
async setModel(ref, provider, modelId) {
|
|
1361
1615
|
await this.assertWritable(ref);
|
|
1362
1616
|
const session = await this.getOrOpen(ref);
|
|
@@ -1375,7 +1629,11 @@ export class PiSessionService {
|
|
|
1375
1629
|
async cycleModel(ref, direction) {
|
|
1376
1630
|
await this.assertWritable(ref);
|
|
1377
1631
|
const session = await this.getOrOpen(ref);
|
|
1378
|
-
const result = await this.
|
|
1632
|
+
const result = await this.runModelScopeMutation(async () => {
|
|
1633
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1634
|
+
await this.synchronizeSessionModelScope(session, session.modelRuntime.getAvailableSnapshot());
|
|
1635
|
+
return this.runSessionEntryMutation(session, "change models", () => session.cycleModel(direction));
|
|
1636
|
+
});
|
|
1379
1637
|
if (result === undefined)
|
|
1380
1638
|
throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
|
|
1381
1639
|
this.publishActivity(session, `model: ${result.model.id}`, "idle", result.model.provider);
|
|
@@ -1585,6 +1843,83 @@ export class PiSessionService {
|
|
|
1585
1843
|
}
|
|
1586
1844
|
}
|
|
1587
1845
|
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Fork the session from one entry of its tree into a new session file,
|
|
1848
|
+
* leaving the original session untouched. The forked runtime replaces the
|
|
1849
|
+
* current one, so the outcome is reported for the session the client is
|
|
1850
|
+
* about to join rather than the forked-from record.
|
|
1851
|
+
*/
|
|
1852
|
+
async forkFromTree(ref, request) {
|
|
1853
|
+
if (request.entryId.trim() === "")
|
|
1854
|
+
throw new Error("Session tree entry is required");
|
|
1855
|
+
if (this.isTreeExclusiveSessionIdentityActive(ref.id)) {
|
|
1856
|
+
throw new Error("Stop current session activity before forking the session tree");
|
|
1857
|
+
}
|
|
1858
|
+
await this.assertWritable(ref);
|
|
1859
|
+
const session = await this.getOrOpen(ref);
|
|
1860
|
+
if (this.hasActiveWork(session))
|
|
1861
|
+
throw new Error("Stop current session activity before forking the session tree");
|
|
1862
|
+
if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
|
|
1863
|
+
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
1864
|
+
}
|
|
1865
|
+
this.publishActivity(session, "forking session from entry", "active");
|
|
1866
|
+
this.publishStatus(session);
|
|
1867
|
+
try {
|
|
1868
|
+
const result = await this.commandService.forkEntry(session.sessionId, request.entryId, {
|
|
1869
|
+
expectedLeafId: request.expectedLeafId,
|
|
1870
|
+
});
|
|
1871
|
+
if (result.type === "unsupported")
|
|
1872
|
+
throw new Error(result.message);
|
|
1873
|
+
if (result.type !== "done")
|
|
1874
|
+
throw new Error("Session fork is unavailable");
|
|
1875
|
+
if (result.session === undefined) {
|
|
1876
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1877
|
+
this.publishActivity(session, "fork cancelled", "idle");
|
|
1878
|
+
this.publishStatus(session);
|
|
1879
|
+
}
|
|
1880
|
+
return { cancelled: true };
|
|
1881
|
+
}
|
|
1882
|
+
const forkedSession = this.active.get(result.session.id)?.runtime.session;
|
|
1883
|
+
if (forkedSession !== undefined && this.isCurrentActiveSession(forkedSession)) {
|
|
1884
|
+
this.publishActivity(forkedSession, "session forked", "idle");
|
|
1885
|
+
this.publishStatus(forkedSession);
|
|
1886
|
+
}
|
|
1887
|
+
if (result.session.id !== session.sessionId)
|
|
1888
|
+
this.clearSupersededSessionActivity(session);
|
|
1889
|
+
return {
|
|
1890
|
+
cancelled: false,
|
|
1891
|
+
session: result.session,
|
|
1892
|
+
...(result.promptDraft === undefined ? {} : { promptDraft: result.promptDraft }),
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
catch (error) {
|
|
1896
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1897
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1898
|
+
this.publishActivity(session, "fork failed", "error", message);
|
|
1899
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1900
|
+
this.publishStatus(session);
|
|
1901
|
+
}
|
|
1902
|
+
throw error;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* A changed-id fork can rebind its runtime after a heartbeat published the
|
|
1907
|
+
* prior identity as active. Clear every observable owner before forgetting
|
|
1908
|
+
* that identity's local activity record.
|
|
1909
|
+
*/
|
|
1910
|
+
clearSupersededSessionActivity(session) {
|
|
1911
|
+
const sessionId = session.sessionId;
|
|
1912
|
+
if (this.activities.get(sessionId)?.phase === "active") {
|
|
1913
|
+
const at = new Date().toISOString();
|
|
1914
|
+
const stored = { phase: "idle", label: "idle", at };
|
|
1915
|
+
this.activities.set(sessionId, stored);
|
|
1916
|
+
const activity = { sessionId, ...stored };
|
|
1917
|
+
this.events.publish(sessionId, { type: "activity.update", activity });
|
|
1918
|
+
this.events.publishGlobal({ type: "activity.update", activity });
|
|
1919
|
+
}
|
|
1920
|
+
this.workspaceActivity?.removeSession(sessionId, session.sessionManager.getCwd());
|
|
1921
|
+
this.activities.delete(sessionId);
|
|
1922
|
+
}
|
|
1588
1923
|
async reloadSessionRuntime(session) {
|
|
1589
1924
|
if (this.hasActiveWork(session))
|
|
1590
1925
|
throw new Error("Stop current session activity before reloading");
|
|
@@ -1832,13 +2167,10 @@ export class PiSessionService {
|
|
|
1832
2167
|
if (sessionFile === undefined || sessionFile === "")
|
|
1833
2168
|
throw new Error("Session is not persisted");
|
|
1834
2169
|
await clearParentSession(sessionFile);
|
|
1835
|
-
// The header
|
|
1836
|
-
//
|
|
1837
|
-
//
|
|
1838
|
-
|
|
1839
|
-
// The same in-place rewrite is invisible to the gateway's summary memo
|
|
1840
|
-
// (same inode, possibly grown file), which would otherwise keep listing
|
|
1841
|
-
// the old parent link and a stale message count until restart.
|
|
2170
|
+
// The header rewrite keeps the inode, and whenever it leaves the file's
|
|
2171
|
+
// size unchanged it is invisible to the gateway's summary memo, which
|
|
2172
|
+
// cannot detect such rewrites from identity + size alone and would keep
|
|
2173
|
+
// listing the old parent link until restart.
|
|
1842
2174
|
this.sessionManager.invalidateSessionFile(sessionFile);
|
|
1843
2175
|
clearParentSessionHeader(session.sessionManager);
|
|
1844
2176
|
this.unregisterSubsession(session.sessionId);
|
|
@@ -2106,9 +2438,11 @@ export class PiSessionService {
|
|
|
2106
2438
|
const { archivePath } = archived;
|
|
2107
2439
|
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
|
|
2108
2440
|
}
|
|
2109
|
-
// Resolve the session file directly by id: opening one
|
|
2110
|
-
//
|
|
2111
|
-
|
|
2441
|
+
// Resolve the session file directly by id: opening one known session must
|
|
2442
|
+
// not depend on — or wait behind — a full transcript listing of its whole
|
|
2443
|
+
// workspace. `getActive` routes prompt/shell/runCommand, so coupling it to
|
|
2444
|
+
// the listing would let an in-flight listing serialize unrelated sends.
|
|
2445
|
+
const match = await this.sessionManager.resolveSessionFile(ref.cwd, ref.id);
|
|
2112
2446
|
if (!match)
|
|
2113
2447
|
throw new Error("Session not found");
|
|
2114
2448
|
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
@@ -2424,6 +2758,11 @@ export class PiSessionService {
|
|
|
2424
2758
|
void this.publishUnreadMutations(mutations).catch(() => undefined);
|
|
2425
2759
|
}
|
|
2426
2760
|
publishUnreadMutations(mutations) {
|
|
2761
|
+
// The store applied the mutations already, so the status projection is told
|
|
2762
|
+
// now rather than after the durable flush: it reads in-memory unread state
|
|
2763
|
+
// and must not lag behind the rows the browser is about to see.
|
|
2764
|
+
if (mutations.length > 0)
|
|
2765
|
+
this.onUnreadChanged?.();
|
|
2427
2766
|
this.enqueueUnreadMutations(mutations);
|
|
2428
2767
|
this.unreadPublicationFlushRequested = true;
|
|
2429
2768
|
if (this.unreadPublication === undefined && this.unreadPublicationRetryTimer !== undefined) {
|
|
@@ -3033,6 +3372,20 @@ function modelToClientModel(model) {
|
|
|
3033
3372
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
3034
3373
|
};
|
|
3035
3374
|
}
|
|
3375
|
+
/** The scope source pi's enabled-id precedence reads from: live scope, settings patterns, runtime catalog. */
|
|
3376
|
+
function sessionScopeSource(session) {
|
|
3377
|
+
return { settingsManager: session.settingsManager, modelRuntime: session.modelRuntime, scopedModels: session.scopedModels };
|
|
3378
|
+
}
|
|
3379
|
+
function catalogEntryToClientModel(entry, editable = true) {
|
|
3380
|
+
return {
|
|
3381
|
+
...modelToClientModel(entry.model),
|
|
3382
|
+
provider: entry.model.provider,
|
|
3383
|
+
id: entry.model.id,
|
|
3384
|
+
enabled: entry.enabled,
|
|
3385
|
+
catalogIndex: entry.catalogIndex,
|
|
3386
|
+
...(editable ? {} : { editable: false }),
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3036
3389
|
function notificationIdentityForSession(session) {
|
|
3037
3390
|
return {
|
|
3038
3391
|
sessionId: session.sessionId,
|