@jmfederico/pi-web 1.202608.2 → 1.202609.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/README.md +3 -3
- package/dist/.plugins-ready +1 -0
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/apple-touch-icon-dev.png +0 -0
- package/dist/client/assets/index-AroYaCLx.js +4130 -0
- package/dist/client/assets/vendor-editor-core-YWog6mAR.js +10 -0
- package/dist/client/assets/vendor-editor-languages-DOCw3vc9.js +28 -0
- package/dist/client/favicon-dev.svg +8 -0
- package/dist/client/index.html +5 -5
- package/dist/client/pwa-icon-dev-192.png +0 -0
- package/dist/client/pwa-icon-dev-512.png +0 -0
- package/dist/config.js +20 -0
- package/dist/config.js.map +1 -1
- package/dist/docker/piWebDockerCommandPlan.js +9 -1
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
- package/dist/nativeServices/installedServiceDefinitions.js +25 -19
- package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
- package/dist/nativeServices/serviceAction.js +38 -26
- package/dist/nativeServices/serviceAction.js.map +1 -1
- package/dist/nativeServices/servicePlan.js +3 -3
- package/dist/nativeServices/servicePlan.js.map +1 -1
- package/dist/pi-packages/captains-log/README.md +11 -0
- package/dist/pi-packages/captains-log/dist/browser/channelProtocol.js +29 -0
- package/dist/pi-packages/captains-log/dist/browser/index.js +144 -0
- package/dist/pi-packages/captains-log/dist/browser/markdown.js +67 -0
- package/dist/pi-packages/captains-log/dist/browser/panel.js +105 -0
- package/dist/pi-packages/captains-log/dist/browser/protocol.js +26 -0
- package/dist/pi-packages/captains-log/dist/companion.js +126 -0
- package/dist/pi-packages/captains-log/dist/roundtrip.js +62 -0
- package/dist/pi-packages/captains-log/dist/server.js +210 -0
- package/dist/pi-packages/captains-log/dist/store.js +75 -0
- package/dist/pi-packages/captains-log/docs/usage.md +30 -0
- package/dist/pi-packages/captains-log/package.json +35 -0
- package/dist/pi-packages/captains-log/tsconfig.json +21 -0
- package/dist/pi-packages/captains-log/vite.config.mjs +28 -0
- package/dist/pi-packages/relays/package.json +1 -1
- package/dist/pi-packages/relays/pi-web-plugin.js +1 -1
- package/dist/pi-packages/relays/prompts/relay-worktree.md +13 -142
- package/dist/pi-packages/relays/prompts/relay.md +25 -132
- package/dist/pi-packages/relays/relayDiscovery.js +2 -2
- package/dist/pi-packages/relays/relaysPanelElement.js +1 -1
- package/dist/pi-packages/relays/skills/relay/SKILL.md +59 -88
- package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +253 -0
- package/dist/pi-web-plugins/files/browser/assets/files-icon-DZObYhxb.svg +3 -0
- package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +439 -0
- package/dist/pi-web-plugins/files/package.json +15 -0
- package/dist/pi-web-plugins/git/browser/git-panel.js +54 -24
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/git/git-backend.js +5 -5
- package/dist/pi-web-plugins/git/server-plugin.js +5 -3
- package/dist/pi-web-plugins/info/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +3590 -0
- package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +6 -0
- package/dist/pi-web-plugins/mermaid/package.json +13 -0
- package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +210 -0
- package/dist/pi-web-plugins/terminal/package.json +16 -0
- package/dist/pi-web-plugins/terminal/server-plugin.js +365 -0
- package/dist/{server/terminals → pi-web-plugins/terminal}/terminalService.js +162 -65
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +45 -35
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +1 -1
- package/dist/piWebVersionReport.js +12 -4
- package/dist/piWebVersionReport.js.map +1 -1
- package/dist/plugin-api.d.ts +222 -23
- package/dist/plugin-api.js +1 -0
- package/dist/pluginRecoveryCli.js +3 -2
- package/dist/pluginRecoveryCli.js.map +1 -1
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +33 -12
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +6 -1
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/deploymentIdentity.js +67 -0
- package/dist/server/deploymentIdentity.js.map +1 -0
- package/dist/server/deploymentIdentityRoutes.js +22 -0
- package/dist/server/deploymentIdentityRoutes.js.map +1 -0
- package/dist/server/knownAutoInstallPiPackages.js +1 -1
- package/dist/server/knownAutoInstallPiPackages.js.map +1 -1
- package/dist/server/knownPiPackages.js +12 -0
- package/dist/server/knownPiPackages.js.map +1 -0
- package/dist/server/machines/machineClient.js +2 -2
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +53 -11
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +70 -7
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/notices/serverNoticeRoutes.js +34 -0
- package/dist/server/notices/serverNoticeRoutes.js.map +1 -0
- package/dist/server/notices/serverNoticeService.js +26 -0
- package/dist/server/notices/serverNoticeService.js.map +1 -0
- package/dist/server/notices/serverNoticeStore.js +119 -0
- package/dist/server/notices/serverNoticeStore.js.map +1 -0
- package/dist/server/piPackageService.js +5 -5
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebPluginCatalog.js +46 -12
- package/dist/server/piWebPluginCatalog.js.map +1 -1
- package/dist/server/piWebPluginLifecycle.js +26 -3
- package/dist/server/piWebPluginLifecycle.js.map +1 -1
- package/dist/server/piWebPluginService.js +37 -2
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebStatus.js +1 -1
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/pluginCallbackDrain.js +35 -0
- package/dist/server/pluginCallbackDrain.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +84 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js +234 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +52 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -0
- package/dist/server/plugins/pluginBackendProxyRoutes.js +38 -31
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
- package/dist/server/plugins/pluginBackendRegistry.js +740 -0
- package/dist/server/plugins/pluginBackendRegistry.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js +34 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js +185 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +1028 -107
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
- package/dist/server/plugins/serverPluginWorkspacesCapability.js +114 -0
- package/dist/server/plugins/serverPluginWorkspacesCapability.js.map +1 -0
- package/dist/server/sessiond/pluginBackendChannelRoutes.js +393 -0
- package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -0
- package/dist/server/sessiond/pluginBackendRoutes.js +10 -7
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionDaemonShutdown.js +9 -2
- package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -1
- package/dist/server/sessiond/sessionProxyRoutes.js +2 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
- package/dist/server/sessiond.js +82 -21
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/attachmentService.js +1 -5
- package/dist/server/sessions/attachmentService.js.map +1 -1
- package/dist/server/sessions/builtinCommands.js +2 -2
- package/dist/server/sessions/builtinCommands.js.map +1 -1
- package/dist/server/sessions/clientSessionPreview.js +14 -0
- package/dist/server/sessions/clientSessionPreview.js.map +1 -0
- package/dist/server/sessions/piSessionEventConnections.js +65 -0
- package/dist/server/sessions/piSessionEventConnections.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +171 -1
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +442 -90
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionActivityMarker.js +105 -0
- package/dist/server/sessions/sessionActivityMarker.js.map +1 -0
- package/dist/server/sessions/sessionEnvironmentFacts.js +5 -4
- package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -1
- package/dist/server/sessions/sessionNameGenerator.js +3 -2
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +18 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +1 -1
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/transcriptBranchCache.js +81 -0
- package/dist/server/sessions/transcriptBranchCache.js.map +1 -0
- package/dist/server/sessions/transcriptMessages.js +54 -0
- package/dist/server/sessions/transcriptMessages.js.map +1 -0
- package/dist/server/terminals/requiredTerminalService.js +103 -0
- package/dist/server/terminals/requiredTerminalService.js.map +1 -0
- package/dist/server/webSocketBridge.js +339 -0
- package/dist/server/webSocketBridge.js.map +1 -1
- package/dist/server/workspaces/projectPiWebConfig.js +6 -1
- package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +23 -3
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceCatalog.js +3 -1
- package/dist/server/workspaces/workspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceProviderRegistry.js +64 -149
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
- package/dist/server/workspaces/workspaceRemovalService.js +38 -5
- package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
- package/dist/server-plugin-api.d.ts +216 -22
- package/dist/server-plugin-api.js +319 -2
- package/dist/server-plugin-api.js.map +1 -1
- package/dist/serverPluginRecovery.js +4 -0
- package/dist/serverPluginRecovery.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +3 -3
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/apiTypes.js +1 -1
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +47 -17
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/machinePluginIds.js +28 -7
- package/dist/shared/machinePluginIds.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +21 -1
- package/dist/shared/pluginApiTypes.js +0 -1
- package/dist/shared/pluginBackendProtocol.js +163 -2
- package/dist/shared/pluginBackendProtocol.js.map +1 -1
- package/dist/shared/pluginIds.js +8 -1
- package/dist/shared/pluginIds.js.map +1 -1
- package/dist/shared/requiredTerminalPlugin.js +6 -0
- package/dist/shared/requiredTerminalPlugin.js.map +1 -0
- package/dist/shared/serverNoticeContract.js +88 -0
- package/dist/shared/serverNoticeContract.js.map +1 -0
- package/dist/shared/sessionDefaults.js +41 -0
- package/dist/shared/sessionDefaults.js.map +1 -0
- package/docs/config.md +77 -37
- package/docs/plugins.md +146 -1213
- package/examples/session-bridge-plugin/README.md +20 -0
- package/examples/session-bridge-plugin/docs/usage.md +54 -0
- package/examples/session-bridge-plugin/package.json +26 -0
- package/examples/session-bridge-plugin/src/browser/index.ts +84 -0
- package/examples/session-bridge-plugin/src/browser/protocol.ts +24 -0
- package/examples/session-bridge-plugin/src/companion.ts +65 -0
- package/examples/session-bridge-plugin/src/reviewRun.ts +39 -0
- package/examples/session-bridge-plugin/src/server.ts +85 -0
- package/examples/session-bridge-plugin/src/store.ts +57 -0
- package/examples/session-bridge-plugin/tsconfig.json +21 -0
- package/examples/workspace-provider-plugin/README.md +4 -4
- package/examples/workspace-provider-plugin/package.json +1 -1
- package/examples/workspace-provider-plugin/src/browser/index.ts +9 -9
- package/examples/workspace-provider-plugin/src/server.ts +15 -11
- package/package.json +17 -11
- package/dist/client/assets/CodeViewer-BMWwxG7q.js +0 -4
- package/dist/client/assets/TerminalPanel-CacQDIYn.js +0 -187
- package/dist/client/assets/index-DUW2xnoV.js +0 -4279
- package/dist/client/assets/vendor-editor-core-CXO8gGab.js +0 -12
- package/dist/client/assets/vendor-editor-languages-CpW4sJsX.js +0 -46
- package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +0 -1
- package/dist/client/assets/vendor-terminal-BrP-ENHg.css +0 -1
- package/dist/client/assets/vendor-terminal-D8k4UKM2.js +0 -35
- package/dist/server/terminalProxyRoutes.js +0 -141
- package/dist/server/terminalProxyRoutes.js.map +0 -1
- package/dist/server/terminals/terminalRoutes.js +0 -155
- package/dist/server/terminals/terminalRoutes.js.map +0 -1
- package/dist/server/terminals/terminalService.js.map +0 -1
- package/dist/server/terminals/terminalSize.js +0 -17
- package/dist/server/terminals/terminalSize.js.map +0 -1
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
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";
|
|
4
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEventBus, 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
|
+
import { clientSessionFirstMessagePreview } from "./clientSessionPreview.js";
|
|
7
8
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
8
9
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
10
|
+
import { SessionActivityMarker } from "./sessionActivityMarker.js";
|
|
9
11
|
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
10
12
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
11
13
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
14
|
+
import { PiSessionEventConnections } from "./piSessionEventConnections.js";
|
|
12
15
|
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
13
16
|
import { computeEditPreview } from "./editPreview.js";
|
|
14
17
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
18
|
+
import { loadEffectiveProjectAttachmentsConfig } from "../workspaces/projectPiWebConfig.js";
|
|
15
19
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
16
20
|
import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
21
|
+
import { parseSessionDefaults, parseSessionDefaultsUpdate } from "../../shared/sessionDefaults.js";
|
|
17
22
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
18
23
|
import { readSessionHeaderSummary } from "./sessionFileHeader.js";
|
|
19
24
|
import { createAskUserToolDefinition } from "./askUserTool.js";
|
|
@@ -24,6 +29,7 @@ import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS } from "../../config.js";
|
|
|
24
29
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
25
30
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
26
31
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
32
|
+
import { annotateAssistantThinkingLevel, historyMessagesFromEntries } from "./transcriptMessages.js";
|
|
27
33
|
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
28
34
|
import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
29
35
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
@@ -47,6 +53,15 @@ const STARTUP_PHASE_EXTENSIONS = "Loading session extensions";
|
|
|
47
53
|
const STARTUP_CONCURRENT_CATALOG_REFRESH = "provider model lists are refreshing";
|
|
48
54
|
const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
|
|
49
55
|
const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
|
|
56
|
+
/**
|
|
57
|
+
* Upper bound on how often one idle runtime re-resolves its transcript file.
|
|
58
|
+
* A runtime created in memory and never persisted has no session file, so
|
|
59
|
+
* every poll would otherwise rescan the session directory; throttling keeps
|
|
60
|
+
* steady-state polling of such a session O(1). The window can only delay
|
|
61
|
+
* noticing a file the runtime did not write itself: once the runtime
|
|
62
|
+
* persists, `getSessionFile()` answers and this throttle is bypassed.
|
|
63
|
+
*/
|
|
64
|
+
const IDLE_SESSION_FILE_RESOLUTION_THROTTLE_MS = 30_000;
|
|
50
65
|
function noop() {
|
|
51
66
|
// Intentionally empty default unsubscribe callback.
|
|
52
67
|
}
|
|
@@ -412,7 +427,7 @@ export function piWebResourceLoaderOptions(appendSystemPromptSections) {
|
|
|
412
427
|
return undefined;
|
|
413
428
|
return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
|
|
414
429
|
}
|
|
415
|
-
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
|
|
430
|
+
function createDefaultRuntimeFactory(sessionEvents, modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
|
|
416
431
|
const resourceLoaderOptions = piWebResourceLoaderOptions(appendSystemPromptSections);
|
|
417
432
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
418
433
|
// PI WEB always honors pi's project-trust model. When the workspace ships
|
|
@@ -425,6 +440,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
425
440
|
// browser trust prompt, an untrusted project's resources are skipped
|
|
426
441
|
// (matching `pi` run without a UI). Projects without trust-requiring
|
|
427
442
|
// resources skip resolution entirely and are trusted, as before.
|
|
443
|
+
const eventBus = createEventBus();
|
|
428
444
|
const projectTrustRequiring = hasTrustRequiringProjectResources(cwd);
|
|
429
445
|
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: !projectTrustRequiring });
|
|
430
446
|
// Pre-session-creation trust failures (`project_trust` handler errors)
|
|
@@ -436,7 +452,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
436
452
|
agentDir,
|
|
437
453
|
modelRuntime,
|
|
438
454
|
settingsManager,
|
|
439
|
-
|
|
455
|
+
resourceLoaderOptions: { ...resourceLoaderOptions, eventBus },
|
|
440
456
|
...(projectTrustRequiring
|
|
441
457
|
? {
|
|
442
458
|
resourceLoaderReloadOptions: {
|
|
@@ -470,6 +486,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
470
486
|
...(modelOptions.thinkingLevel === undefined ? {} : { thinkingLevel: modelOptions.thinkingLevel }),
|
|
471
487
|
...(modelOptions.scopedModels.length === 0 ? {} : { scopedModels: modelOptions.scopedModels }),
|
|
472
488
|
});
|
|
489
|
+
sessionEvents.register(result.session, eventBus);
|
|
473
490
|
return { ...result, services, diagnostics: [...projectTrustDiagnostics, ...services.diagnostics] };
|
|
474
491
|
};
|
|
475
492
|
}
|
|
@@ -510,6 +527,12 @@ export class PiSessionService {
|
|
|
510
527
|
this.activities = new Map();
|
|
511
528
|
/** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
|
|
512
529
|
this.treeNavigations = new WeakSet();
|
|
530
|
+
/**
|
|
531
|
+
* Bare live leaf selected without an appended summary entry. Recording that
|
|
532
|
+
* leaf distinguishes the unpersisted move from a later runtime append that
|
|
533
|
+
* can anchor the selected branch on disk.
|
|
534
|
+
*/
|
|
535
|
+
this.unpersistedTreeBranchLeaves = new WeakMap();
|
|
513
536
|
/** Counts async operations that may append an entry before they settle. */
|
|
514
537
|
this.sessionEntryMutationCounts = new WeakMap();
|
|
515
538
|
/** Settings-wide queue preventing enabled-model read/modify/write races across sessions. */
|
|
@@ -545,6 +568,8 @@ export class PiSessionService {
|
|
|
545
568
|
*/
|
|
546
569
|
this.subsessionNotifyArmed = new Map();
|
|
547
570
|
this.notificationGenerationBySession = new WeakMap();
|
|
571
|
+
/** Last idle-poll transcript file resolution per runtime, throttled; entries die with their runtime. */
|
|
572
|
+
this.idleSessionFileResolutions = new WeakMap();
|
|
548
573
|
/** The parked extension Promise resolvers behind the store's open dialogs. */
|
|
549
574
|
this.dialogWaiters = new ExtensionDialogWaiters();
|
|
550
575
|
this.pendingUnreadMutations = [];
|
|
@@ -557,6 +582,10 @@ export class PiSessionService {
|
|
|
557
582
|
this.spawnTargets = deps.spawnTargets;
|
|
558
583
|
this.logger = deps.logger ?? noopLogger;
|
|
559
584
|
this.now = deps.now ?? (() => new Date());
|
|
585
|
+
this.activityMarker = new SessionActivityMarker({
|
|
586
|
+
now: () => this.now().getTime(),
|
|
587
|
+
onError: (error) => { this.logger.info({ err: error }, "Could not update advisory session activity marker"); },
|
|
588
|
+
});
|
|
560
589
|
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
561
590
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
562
591
|
this.onUnreadChanged = deps.onUnreadChanged;
|
|
@@ -564,12 +593,14 @@ export class PiSessionService {
|
|
|
564
593
|
this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
|
|
565
594
|
this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
|
|
566
595
|
this.catalogRefreshStatus = deps.catalogRefreshStatus;
|
|
596
|
+
this.config = deps.config;
|
|
567
597
|
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
568
598
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
569
599
|
// Subsessions are gated behind their own flag, and they
|
|
570
600
|
// also require the spawn capability (they share its project-scope resolver).
|
|
571
601
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
572
|
-
this.
|
|
602
|
+
this.sessionEvents = deps.sessionEvents ?? new PiSessionEventConnections();
|
|
603
|
+
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.sessionEvents, this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
573
604
|
spawn: (input) => this.spawnSubsession(input),
|
|
574
605
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
575
606
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
@@ -713,7 +744,7 @@ export class PiSessionService {
|
|
|
713
744
|
finally {
|
|
714
745
|
await active.runtime.dispose();
|
|
715
746
|
}
|
|
716
|
-
}));
|
|
747
|
+
})).finally(() => this.activityMarker.dispose());
|
|
717
748
|
await this.publishUnreadMutations([]);
|
|
718
749
|
}
|
|
719
750
|
async list(cwd) {
|
|
@@ -729,14 +760,48 @@ export class PiSessionService {
|
|
|
729
760
|
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds);
|
|
730
761
|
await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
731
762
|
const archivedSessions = archivedForCwd
|
|
732
|
-
.sort(compareArchivedRecords)
|
|
733
763
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
734
|
-
.filter(isDefined)
|
|
764
|
+
.filter(isDefined)
|
|
765
|
+
.sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified));
|
|
735
766
|
return [...unarchivedSessions, ...archivedSessions];
|
|
736
767
|
}
|
|
737
768
|
async start(cwd, options = {}) {
|
|
738
769
|
return this.startSession(cwd, options);
|
|
739
770
|
}
|
|
771
|
+
/** Publish a host-owned conversation without submitting an initial prompt. */
|
|
772
|
+
async createHostedSession(cwd, signal) {
|
|
773
|
+
const created = await this.startManagedSession(cwd, signal);
|
|
774
|
+
return Object.freeze({ id: created.id });
|
|
775
|
+
}
|
|
776
|
+
/** Atomically start and prompt one visible host-owned, non-delegating session. */
|
|
777
|
+
async startOneShotRun(cwd, text, signal) {
|
|
778
|
+
const promptText = requirePromptText(text);
|
|
779
|
+
return this.startManagedSession(cwd, signal, promptText);
|
|
780
|
+
}
|
|
781
|
+
async startManagedSession(cwd, signal, promptText) {
|
|
782
|
+
signal.throwIfAborted();
|
|
783
|
+
const active = await this.create(this.sessionManager.create(cwd), cwd, { startupIntent: "create", creationProvenance: "host-one-shot", startupSignal: signal });
|
|
784
|
+
const { session } = active.runtime;
|
|
785
|
+
try {
|
|
786
|
+
signal.throwIfAborted();
|
|
787
|
+
if (promptText !== undefined)
|
|
788
|
+
this.maybeGenerateSessionName(session, promptText);
|
|
789
|
+
const created = this.announceCreatedSession(active, cwd);
|
|
790
|
+
// Publication transfers lifetime ownership to normal hosting. Plugin
|
|
791
|
+
// cancellation after this point must not abort this or later user work.
|
|
792
|
+
const completion = promptText === undefined ? Promise.resolve() : this.submitInitialRunPrompt(session, promptText);
|
|
793
|
+
return Object.freeze({ id: created.id, completion });
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
try {
|
|
797
|
+
await this.closeActive(session.sessionId);
|
|
798
|
+
}
|
|
799
|
+
catch (cleanupError) {
|
|
800
|
+
throw new AggregateError([error, cleanupError], `Failed to clean up managed session ${session.sessionId} after startup failed`, { cause: cleanupError });
|
|
801
|
+
}
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
740
805
|
async startSession(cwd, options) {
|
|
741
806
|
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
|
|
742
807
|
startupIntent: "create",
|
|
@@ -745,6 +810,9 @@ export class PiSessionService {
|
|
|
745
810
|
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
746
811
|
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
747
812
|
});
|
|
813
|
+
return this.announceCreatedSession(active, cwd, options.parentSession);
|
|
814
|
+
}
|
|
815
|
+
announceCreatedSession(active, cwd, parentSession) {
|
|
748
816
|
const { session } = active.runtime;
|
|
749
817
|
const created = {
|
|
750
818
|
id: session.sessionId,
|
|
@@ -757,7 +825,7 @@ export class PiSessionService {
|
|
|
757
825
|
firstMessage: "",
|
|
758
826
|
// Include the parent so listeners can nest the new session in the tree
|
|
759
827
|
// immediately, instead of showing it flat until the next reload.
|
|
760
|
-
...(
|
|
828
|
+
...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
|
|
761
829
|
};
|
|
762
830
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
763
831
|
// session to their list without a manual reload.
|
|
@@ -1095,8 +1163,12 @@ export class PiSessionService {
|
|
|
1095
1163
|
* `async` so a rejected dialog becomes a rejection rather than a synchronous
|
|
1096
1164
|
* throw from a promise-returning method.
|
|
1097
1165
|
*/
|
|
1098
|
-
async openExtensionDialog(session, request, opts) {
|
|
1099
|
-
const signal = opts?.signal
|
|
1166
|
+
async openExtensionDialog(session, request, opts, startupSignal) {
|
|
1167
|
+
const signal = opts?.signal === undefined
|
|
1168
|
+
? startupSignal
|
|
1169
|
+
: startupSignal === undefined
|
|
1170
|
+
? opts.signal
|
|
1171
|
+
: AbortSignal.any([opts.signal, startupSignal]);
|
|
1100
1172
|
// A pre-aborted signal dismisses the dialog before it ever opens.
|
|
1101
1173
|
if (signal?.aborted === true)
|
|
1102
1174
|
return extensionDialogCancelValue(request.kind);
|
|
@@ -1520,10 +1592,15 @@ export class PiSessionService {
|
|
|
1520
1592
|
}
|
|
1521
1593
|
async messages(ref, page) {
|
|
1522
1594
|
const session = await this.getOrOpen(ref);
|
|
1523
|
-
return pageMessagesAtSafeBoundary(
|
|
1595
|
+
return pageMessagesAtSafeBoundary(historyMessagesFromEntries(await this.readableSessionBranch(ref, session)), page);
|
|
1524
1596
|
}
|
|
1525
1597
|
async status(ref) {
|
|
1526
|
-
|
|
1598
|
+
const session = await this.sessionForStatusOrDialogClose(ref);
|
|
1599
|
+
await this.activityMarker.refresh(session.sessionFile, this.hasActiveWork(session));
|
|
1600
|
+
if (this.hasActiveWork(session))
|
|
1601
|
+
return this.statusFromSession(session);
|
|
1602
|
+
const branch = await this.readableSessionBranch(ref, session);
|
|
1603
|
+
return this.statusFromSession(session, transcriptMessageCount(branch));
|
|
1527
1604
|
}
|
|
1528
1605
|
/**
|
|
1529
1606
|
* Join-time snapshot of the in-flight assistant stream. The `seq` watermark and
|
|
@@ -1544,6 +1621,47 @@ export class PiSessionService {
|
|
|
1544
1621
|
: annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
|
|
1545
1622
|
return { seq, partial };
|
|
1546
1623
|
}
|
|
1624
|
+
async getSessionDefaults(ref) {
|
|
1625
|
+
await this.getOrOpen(ref);
|
|
1626
|
+
const settings = SettingsManager.create(ref.cwd, this.agentDir);
|
|
1627
|
+
await settings.reload();
|
|
1628
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1629
|
+
// Do not use merged getters: workspace overrides are not global pins.
|
|
1630
|
+
return parseSessionDefaults(settings.getGlobalSettings());
|
|
1631
|
+
}
|
|
1632
|
+
async setSessionDefaults(ref, defaults) {
|
|
1633
|
+
const update = parseSessionDefaultsUpdate({ ...defaults });
|
|
1634
|
+
await this.assertWritable(ref);
|
|
1635
|
+
const session = await this.getOrOpen(ref);
|
|
1636
|
+
return this.runModelScopeMutation(async () => {
|
|
1637
|
+
// A separate manager avoids mutating the active session's settings cache.
|
|
1638
|
+
const settings = SettingsManager.create(ref.cwd, this.agentDir);
|
|
1639
|
+
await settings.reload();
|
|
1640
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1641
|
+
if (update.provider !== undefined && update.modelId !== undefined) {
|
|
1642
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1643
|
+
const target = `${update.provider}/${update.modelId}`;
|
|
1644
|
+
const enabledIds = await resolveEnabledModelIds({ settingsManager: settings, modelRuntime: session.modelRuntime, scopedModels: [] });
|
|
1645
|
+
if (!session.modelRuntime.getAvailableSnapshot().some((model) => modelScopeId(model) === target)) {
|
|
1646
|
+
throw new Error(`Model not found: ${target}`);
|
|
1647
|
+
}
|
|
1648
|
+
if (enabledIds !== null && !enabledIds.includes(target))
|
|
1649
|
+
throw new Error(`Model is not enabled: ${target}`);
|
|
1650
|
+
settings.setDefaultModelAndProvider(update.provider, update.modelId);
|
|
1651
|
+
}
|
|
1652
|
+
else if (update.thinkingLevel !== undefined) {
|
|
1653
|
+
settings.setDefaultThinkingLevel(update.thinkingLevel);
|
|
1654
|
+
}
|
|
1655
|
+
await settings.flush();
|
|
1656
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1657
|
+
return parseSessionDefaults(settings.getGlobalSettings());
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
assertDefaultsSettingsHealthy(settings) {
|
|
1661
|
+
const errors = settings.drainErrors();
|
|
1662
|
+
if (errors.length > 0)
|
|
1663
|
+
throw new Error(`Session defaults settings failed: ${errors.map(({ error }) => error.message).join("; ")}`);
|
|
1664
|
+
}
|
|
1547
1665
|
async availableModels(ref) {
|
|
1548
1666
|
const session = await this.getOrOpen(ref);
|
|
1549
1667
|
const models = await this.sessionModelCandidates(session);
|
|
@@ -1677,7 +1795,12 @@ export class PiSessionService {
|
|
|
1677
1795
|
commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
|
|
1678
1796
|
}
|
|
1679
1797
|
for (const template of session.promptTemplates) {
|
|
1680
|
-
commands.push({
|
|
1798
|
+
commands.push({
|
|
1799
|
+
name: template.name,
|
|
1800
|
+
...(template.description === undefined ? {} : { description: template.description }),
|
|
1801
|
+
...(template.argumentHint === undefined ? {} : { argumentHint: template.argumentHint }),
|
|
1802
|
+
source: "prompt",
|
|
1803
|
+
});
|
|
1681
1804
|
}
|
|
1682
1805
|
for (const skill of session.resourceLoader.getSkills().skills) {
|
|
1683
1806
|
commands.push({ name: `skill:${skill.name}`, ...(skill.description === undefined ? {} : { description: skill.description }), source: "skill" });
|
|
@@ -1717,18 +1840,69 @@ export class PiSessionService {
|
|
|
1717
1840
|
void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
|
|
1718
1841
|
}
|
|
1719
1842
|
submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1843
|
+
return this.beginPromptSubmission(session, text, behavior, images, echoUserMessage).catch(() => undefined);
|
|
1844
|
+
}
|
|
1845
|
+
beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1720
1846
|
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
|
1721
1847
|
if (behavior === undefined && echoUserMessage)
|
|
1722
1848
|
this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) });
|
|
1723
1849
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
1724
|
-
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions))
|
|
1850
|
+
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions));
|
|
1851
|
+
void promptPromise.catch((error) => {
|
|
1725
1852
|
const message = error instanceof Error ? error.message : String(error);
|
|
1726
1853
|
this.publishActivity(session, "error", "error", message);
|
|
1727
1854
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1728
1855
|
});
|
|
1729
|
-
void promptPromise;
|
|
1730
1856
|
return promptPromise;
|
|
1731
1857
|
}
|
|
1858
|
+
async submitInitialRunPrompt(session, text) {
|
|
1859
|
+
let failure;
|
|
1860
|
+
let settled = false;
|
|
1861
|
+
let sawInitialUserMessage = false;
|
|
1862
|
+
const unsubscribe = session.subscribe((event) => {
|
|
1863
|
+
if (settled || !isRecord(event))
|
|
1864
|
+
return;
|
|
1865
|
+
if (event["type"] === "agent_settled") {
|
|
1866
|
+
// Freeze the initial outcome before a later user submission can change it.
|
|
1867
|
+
settled = true;
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
if (event["type"] === "auto_retry_end" && event["success"] === false && event["finalError"] === "Retry cancelled") {
|
|
1871
|
+
// Native Pi cancels retry sleep without an aborted assistant message.
|
|
1872
|
+
failure = new DOMException("Initial PI session run was aborted", "AbortError");
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const message = event["message"];
|
|
1876
|
+
if (!isRecord(message))
|
|
1877
|
+
return;
|
|
1878
|
+
if (event["type"] === "message_start" && message["role"] === "user") {
|
|
1879
|
+
// Pi can drain a queued user follow-up inside the same prompt promise.
|
|
1880
|
+
// Its outcome belongs to that later submission, not this launcher.
|
|
1881
|
+
if (sawInitialUserMessage)
|
|
1882
|
+
settled = true;
|
|
1883
|
+
sawInitialUserMessage = true;
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (event["type"] !== "message_end" || message["role"] !== "assistant")
|
|
1887
|
+
return;
|
|
1888
|
+
// A successful retry supersedes a transient provider failure. Pi's
|
|
1889
|
+
// prompt promise can fulfill even when the final assistant is an error.
|
|
1890
|
+
const reason = message["stopReason"];
|
|
1891
|
+
failure = reason === "aborted"
|
|
1892
|
+
? new DOMException("Initial PI session run was aborted", "AbortError")
|
|
1893
|
+
: reason === "error"
|
|
1894
|
+
? new Error(typeof message["errorMessage"] === "string" ? message["errorMessage"] : "Initial PI session run failed")
|
|
1895
|
+
: undefined;
|
|
1896
|
+
});
|
|
1897
|
+
try {
|
|
1898
|
+
await this.beginPromptSubmission(session, text, undefined);
|
|
1899
|
+
if (failure !== undefined)
|
|
1900
|
+
throw failure;
|
|
1901
|
+
}
|
|
1902
|
+
finally {
|
|
1903
|
+
unsubscribe();
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1732
1906
|
enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
|
|
1733
1907
|
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
1734
1908
|
queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
|
|
@@ -1742,7 +1916,25 @@ export class PiSessionService {
|
|
|
1742
1916
|
return [];
|
|
1743
1917
|
await this.assertWritable(ref);
|
|
1744
1918
|
const active = await this.getActive(ref);
|
|
1745
|
-
|
|
1919
|
+
const cwd = active.runtime.cwd;
|
|
1920
|
+
// An explicit request folder wins; the config lookup below is only the
|
|
1921
|
+
// fallback for folder-less calls (see workspaceAttachmentsConfig).
|
|
1922
|
+
const effectiveFolder = folder ?? (await this.workspaceAttachmentsConfig(cwd)).defaultFolder;
|
|
1923
|
+
return saveAttachmentsToWorkspace(cwd, parsed, effectiveFolder === undefined ? {} : { folder: effectiveFolder });
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Fallback attachments config for save requests that omit an explicit
|
|
1927
|
+
* folder: the live global config merged with the session cwd's own
|
|
1928
|
+
* project-local override. Unlike `workspaceEffectiveConfig` in app.ts (which
|
|
1929
|
+
* resolves from the owning project's path), this lookup keys off the cwd
|
|
1930
|
+
* itself, so for secondary (worktree) workspaces it cannot see the owning
|
|
1931
|
+
* project's override. The composer therefore always sends the
|
|
1932
|
+
* workspace-effective folder it displayed explicitly; this cwd-based
|
|
1933
|
+
* resolution only governs folder-less API calls.
|
|
1934
|
+
*/
|
|
1935
|
+
async workspaceAttachmentsConfig(cwd) {
|
|
1936
|
+
const globalConfig = this.config === undefined ? {} : (await this.config.read()).effectiveConfig;
|
|
1937
|
+
return loadEffectiveProjectAttachmentsConfig(cwd, globalConfig);
|
|
1746
1938
|
}
|
|
1747
1939
|
async shell(ref, text) {
|
|
1748
1940
|
await this.assertWritable(ref);
|
|
@@ -1807,18 +1999,60 @@ export class PiSessionService {
|
|
|
1807
1999
|
// may enter this runtime until Pi's potentially asynchronous summary settles.
|
|
1808
2000
|
this.treeNavigations.add(session);
|
|
1809
2001
|
try {
|
|
1810
|
-
|
|
2002
|
+
const oldLeafId = session.sessionManager.getLeafId();
|
|
2003
|
+
if (oldLeafId !== request.expectedLeafId) {
|
|
1811
2004
|
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
1812
2005
|
}
|
|
2006
|
+
const activeEditableTargetParentId = activeEditableTreeTargetParentId(session.sessionManager, request.targetId, oldLeafId);
|
|
1813
2007
|
this.publishActivity(session, options.summarize ? "summarizing branch" : "navigating session tree", "active");
|
|
1814
2008
|
this.publishStatus(session);
|
|
1815
|
-
|
|
2009
|
+
let result = await session.navigateTree(request.targetId, options);
|
|
2010
|
+
if (activeEditableTargetParentId !== undefined
|
|
2011
|
+
&& !result.cancelled
|
|
2012
|
+
&& result.editorText === undefined
|
|
2013
|
+
&& result.summaryEntry === undefined
|
|
2014
|
+
&& session.sessionManager.getLeafId() === oldLeafId) {
|
|
2015
|
+
// Supported Pi versions can return early when the target is already the
|
|
2016
|
+
// leaf, before applying their user/custom-message edit semantics. Move
|
|
2017
|
+
// to the target's parent and delegate again so Pi still owns editor-text
|
|
2018
|
+
// extraction, agent-context rebuilding, and tree extension events.
|
|
2019
|
+
setSessionTreeLeaf(session.sessionManager, activeEditableTargetParentId);
|
|
2020
|
+
try {
|
|
2021
|
+
result = await session.navigateTree(request.targetId, options);
|
|
2022
|
+
}
|
|
2023
|
+
catch (error) {
|
|
2024
|
+
session.sessionManager.branch(request.targetId);
|
|
2025
|
+
throw error;
|
|
2026
|
+
}
|
|
2027
|
+
if (result.cancelled)
|
|
2028
|
+
session.sessionManager.branch(request.targetId);
|
|
2029
|
+
}
|
|
1816
2030
|
if (result.cancelled) {
|
|
1817
2031
|
if (this.isCurrentActiveSession(session)) {
|
|
1818
2032
|
this.publishActivity(session, result.aborted === true ? "branch summary aborted" : "tree navigation cancelled", "idle");
|
|
1819
2033
|
}
|
|
1820
2034
|
return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
|
|
1821
2035
|
}
|
|
2036
|
+
if (session.sessionManager.getLeafId() !== oldLeafId) {
|
|
2037
|
+
// Questions belong to the position we left. Close them for every browser
|
|
2038
|
+
// without appending a cancellation message or waking the rewound session.
|
|
2039
|
+
const outcome = this.pendingAskStore.cancelOpen(session.sessionId);
|
|
2040
|
+
if (outcome !== undefined)
|
|
2041
|
+
this.publishAskClosed(session.sessionId, outcome);
|
|
2042
|
+
}
|
|
2043
|
+
if (result.summaryEntry !== undefined) {
|
|
2044
|
+
// A summary entry durably identifies the selected branch as the file's
|
|
2045
|
+
// newest leaf, superseding any earlier bare selection.
|
|
2046
|
+
this.unpersistedTreeBranchLeaves.delete(session);
|
|
2047
|
+
}
|
|
2048
|
+
else {
|
|
2049
|
+
const selectedLeafId = session.sessionManager.getLeafId();
|
|
2050
|
+
if (selectedLeafId !== oldLeafId) {
|
|
2051
|
+
// SessionManager.branch()/resetLeaf() only move Pi's in-memory leaf.
|
|
2052
|
+
// Keep that branch authoritative until a later append makes disk agree.
|
|
2053
|
+
this.unpersistedTreeBranchLeaves.set(session, selectedLeafId);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1822
2056
|
if (this.isCurrentActiveSession(session))
|
|
1823
2057
|
this.publishActivity(session, "session tree navigated", "idle");
|
|
1824
2058
|
return { cancelled: false, ...(result.editorText === undefined ? {} : { editorText: result.editorText }) };
|
|
@@ -2246,10 +2480,12 @@ export class PiSessionService {
|
|
|
2246
2480
|
async archiveStoreArchiveMany(inputs) {
|
|
2247
2481
|
if (inputs.length === 0)
|
|
2248
2482
|
return [];
|
|
2483
|
+
// Work from the bottom of the activity-ordered list, using metadata already loaded.
|
|
2484
|
+
const oldestFirst = [...inputs].sort((a, b) => Date.parse(a.modified) - Date.parse(b.modified));
|
|
2249
2485
|
if (this.archiveStore.archiveMany !== undefined)
|
|
2250
|
-
return this.archiveStore.archiveMany(
|
|
2486
|
+
return this.archiveStore.archiveMany(oldestFirst);
|
|
2251
2487
|
const records = [];
|
|
2252
|
-
for (const input of
|
|
2488
|
+
for (const input of oldestFirst)
|
|
2253
2489
|
records.push(await this.archiveStore.archive(input));
|
|
2254
2490
|
return records;
|
|
2255
2491
|
}
|
|
@@ -2397,6 +2633,7 @@ export class PiSessionService {
|
|
|
2397
2633
|
await this.abortSessionOperations(active.runtime.session);
|
|
2398
2634
|
}
|
|
2399
2635
|
finally {
|
|
2636
|
+
await this.activityMarker.release(active.runtime.session.sessionFile);
|
|
2400
2637
|
await active.runtime.dispose();
|
|
2401
2638
|
}
|
|
2402
2639
|
}
|
|
@@ -2429,8 +2666,68 @@ export class PiSessionService {
|
|
|
2429
2666
|
async getOrOpen(ref) {
|
|
2430
2667
|
return (await this.getActive(ref)).runtime.session;
|
|
2431
2668
|
}
|
|
2669
|
+
/**
|
|
2670
|
+
* An idle runtime is only Pi Web's cached control view. Another Pi process may
|
|
2671
|
+
* keep appending to the same JSONL file, so transcript reads must open a fresh
|
|
2672
|
+
* read-only snapshot rather than serving that cached branch forever. Active
|
|
2673
|
+
* runtimes remain authoritative and are never replaced, reloaded, or aborted.
|
|
2674
|
+
*/
|
|
2675
|
+
async readableSessionBranch(ref, session) {
|
|
2676
|
+
if (this.hasActiveWork(session) || this.sessionManager.readBranch === undefined)
|
|
2677
|
+
return session.sessionManager.getBranch();
|
|
2678
|
+
const sessionFile = session.sessionFile ?? session.sessionManager.getSessionFile();
|
|
2679
|
+
const resolvedPath = sessionFile === undefined ? await this.idleSessionFilePath(ref, session) : undefined;
|
|
2680
|
+
if (this.hasActiveWork(session))
|
|
2681
|
+
return session.sessionManager.getBranch();
|
|
2682
|
+
const path = sessionFile ?? resolvedPath;
|
|
2683
|
+
if (path === undefined)
|
|
2684
|
+
return session.sessionManager.getBranch();
|
|
2685
|
+
const snapshot = await this.sessionManager.readBranch(path);
|
|
2686
|
+
// No snapshot exists when the transcript file is absent (a session known
|
|
2687
|
+
// by path but never persisted, or externally removed): the runtime branch
|
|
2688
|
+
// is the only readable branch then.
|
|
2689
|
+
if (snapshot === undefined)
|
|
2690
|
+
return session.sessionManager.getBranch();
|
|
2691
|
+
// Reading also yields. A prompt that started meanwhile must still win over
|
|
2692
|
+
// the completed disk snapshot and its potentially older event watermark.
|
|
2693
|
+
if (this.hasActiveWork(session))
|
|
2694
|
+
return session.sessionManager.getBranch();
|
|
2695
|
+
const selectedLeafId = this.unpersistedTreeBranchLeaves.get(session);
|
|
2696
|
+
if (selectedLeafId !== undefined) {
|
|
2697
|
+
const runtimeLeafId = session.sessionManager.getLeafId();
|
|
2698
|
+
const selectedBranchIsAnchored = runtimeLeafId !== null
|
|
2699
|
+
&& runtimeLeafId !== selectedLeafId
|
|
2700
|
+
&& transcriptBranchIncludesEntry(snapshot, runtimeLeafId);
|
|
2701
|
+
if (!selectedBranchIsAnchored)
|
|
2702
|
+
return session.sessionManager.getBranch();
|
|
2703
|
+
// A later runtime append now anchors the live selection in this persisted
|
|
2704
|
+
// branch. Resume ordinary idle snapshots, including external descendants.
|
|
2705
|
+
this.unpersistedTreeBranchLeaves.delete(session);
|
|
2706
|
+
}
|
|
2707
|
+
return snapshot;
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Resolve the transcript file for an idle runtime that does not know one,
|
|
2711
|
+
* at most once per {@link IDLE_SESSION_FILE_RESOLUTION_THROTTLE_MS}. The
|
|
2712
|
+
* negative result (no file yet) is the hot case: a never-persisted session
|
|
2713
|
+
* is polled every few seconds, and re-scanning its directory each tick is
|
|
2714
|
+
* pure waste. `getActive` never uses this path, so prompt routing always
|
|
2715
|
+
* sees a fresh resolution.
|
|
2716
|
+
*/
|
|
2717
|
+
async idleSessionFilePath(ref, session) {
|
|
2718
|
+
const cached = this.idleSessionFileResolutions.get(session);
|
|
2719
|
+
const at = this.now().getTime();
|
|
2720
|
+
if (cached !== undefined && at - cached.at < IDLE_SESSION_FILE_RESOLUTION_THROTTLE_MS)
|
|
2721
|
+
return cached.path;
|
|
2722
|
+
const match = await this.sessionManager.resolveSessionFile(ref.cwd, ref.id);
|
|
2723
|
+
this.idleSessionFileResolutions.set(session, { at, path: match?.path });
|
|
2724
|
+
return match?.path;
|
|
2725
|
+
}
|
|
2432
2726
|
async getActive(ref, options = {}) {
|
|
2433
2727
|
const active = this.activeForRef(ref);
|
|
2728
|
+
const pending = this.pendingSessionOpens.get(JSON.stringify([canonicalizeStoredCwd(ref.cwd), active?.runtime.session.sessionId ?? ref.id]));
|
|
2729
|
+
if (pending !== undefined)
|
|
2730
|
+
return pending.promise;
|
|
2434
2731
|
if (active !== undefined)
|
|
2435
2732
|
return active;
|
|
2436
2733
|
const archived = await this.getArchived(ref);
|
|
@@ -2448,13 +2745,13 @@ export class PiSessionService {
|
|
|
2448
2745
|
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
2449
2746
|
}
|
|
2450
2747
|
openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
|
|
2451
|
-
const active = this.activeForRef({ id: sessionId, cwd });
|
|
2452
|
-
if (active !== undefined)
|
|
2453
|
-
return Promise.resolve(active);
|
|
2454
2748
|
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
|
|
2455
2749
|
const existing = this.pendingSessionOpens.get(key);
|
|
2456
2750
|
if (existing !== undefined)
|
|
2457
2751
|
return existing.promise;
|
|
2752
|
+
const active = this.activeForRef({ id: sessionId, cwd });
|
|
2753
|
+
if (active !== undefined)
|
|
2754
|
+
return Promise.resolve(active);
|
|
2458
2755
|
const pending = {
|
|
2459
2756
|
sessionId,
|
|
2460
2757
|
promise: this.create(openSessionManager(), cwd, options),
|
|
@@ -2534,18 +2831,27 @@ export class PiSessionService {
|
|
|
2534
2831
|
* first is both the slowest phase and one that can fail on its own; a clear
|
|
2535
2832
|
* that only ran for the later phases would leave a stale label behind.
|
|
2536
2833
|
*/
|
|
2537
|
-
|
|
2834
|
+
create(sessionManager, cwd, options = {}) {
|
|
2538
2835
|
const startup = this.startupProgress(sessionManager, options.startupIntent ?? "open", options.startupToken);
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2836
|
+
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionManager.getSessionId()]);
|
|
2837
|
+
// Early native observation must not admit ordinary operations before
|
|
2838
|
+
// session_start finishes. Track new sessions as well as on-demand opens so
|
|
2839
|
+
// close/dispose wait for startup before tearing down its runtime.
|
|
2840
|
+
const pending = {
|
|
2841
|
+
sessionId: sessionManager.getSessionId(),
|
|
2842
|
+
promise: this.createSessionRuntime(sessionManager, cwd, options, startup),
|
|
2843
|
+
};
|
|
2844
|
+
pending.promise = pending.promise.finally(() => {
|
|
2543
2845
|
startup.end();
|
|
2544
|
-
|
|
2846
|
+
if (this.pendingSessionOpens.get(key) === pending)
|
|
2847
|
+
this.pendingSessionOpens.delete(key);
|
|
2848
|
+
});
|
|
2849
|
+
this.pendingSessionOpens.set(key, pending);
|
|
2850
|
+
return pending.promise;
|
|
2545
2851
|
}
|
|
2546
2852
|
async createSessionRuntime(sessionManager, cwd, options, startup) {
|
|
2547
2853
|
startup.report(STARTUP_PHASE_RUNTIME);
|
|
2548
|
-
const delegationToolsEnabled = options.creationProvenance
|
|
2854
|
+
const delegationToolsEnabled = options.creationProvenance === undefined
|
|
2549
2855
|
&& await sessionAllowsDelegationTools(sessionManager, this.sessionManager);
|
|
2550
2856
|
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
2551
2857
|
cwd,
|
|
@@ -2557,6 +2863,11 @@ export class PiSessionService {
|
|
|
2557
2863
|
});
|
|
2558
2864
|
const active = { runtime, unsubscribe: noop };
|
|
2559
2865
|
let boundSession = runtime.session;
|
|
2866
|
+
const startupSignal = options.startupSignal;
|
|
2867
|
+
const cancelStartup = () => { this.endSessionExtensionDialogs(boundSession.sessionId); };
|
|
2868
|
+
if (startupSignal !== undefined && !startupSignal.aborted) {
|
|
2869
|
+
startupSignal.addEventListener("abort", cancelStartup, { once: true });
|
|
2870
|
+
}
|
|
2560
2871
|
let notificationGeneration = options.notificationGeneration;
|
|
2561
2872
|
let notificationOwnership = options.notifications === "disabled"
|
|
2562
2873
|
? "disabled"
|
|
@@ -2579,15 +2890,20 @@ export class PiSessionService {
|
|
|
2579
2890
|
if (notificationGeneration !== undefined)
|
|
2580
2891
|
this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
|
|
2581
2892
|
try {
|
|
2893
|
+
startupSignal?.throwIfAborted();
|
|
2582
2894
|
if (options.creationProvenance === "tracked-subsession") {
|
|
2583
2895
|
await this.publishUnreadMutations(this.unreadStore.excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
|
|
2584
2896
|
}
|
|
2585
2897
|
else {
|
|
2586
2898
|
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
2587
2899
|
}
|
|
2900
|
+
startupSignal?.throwIfAborted();
|
|
2588
2901
|
startup.report(STARTUP_PHASE_EXTENSIONS);
|
|
2589
|
-
|
|
2902
|
+
// session_start hooks use the same native agent as ordinary prompts.
|
|
2903
|
+
// Observe them before binding extensions, including startup messages.
|
|
2590
2904
|
this.bindRuntime(active);
|
|
2905
|
+
await this.bindSessionExtensions(runtime.session, notificationGeneration, startupSignal);
|
|
2906
|
+
startupSignal?.throwIfAborted();
|
|
2591
2907
|
runtime.setRebindSession(async (session) => {
|
|
2592
2908
|
const priorGeneration = notificationGeneration;
|
|
2593
2909
|
let candidateGeneration;
|
|
@@ -2619,6 +2935,7 @@ export class PiSessionService {
|
|
|
2619
2935
|
throw error;
|
|
2620
2936
|
}
|
|
2621
2937
|
});
|
|
2938
|
+
startupSignal?.throwIfAborted();
|
|
2622
2939
|
this.active.set(runtime.session.sessionId, active);
|
|
2623
2940
|
if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
|
|
2624
2941
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
|
|
@@ -2662,13 +2979,16 @@ export class PiSessionService {
|
|
|
2662
2979
|
}
|
|
2663
2980
|
throw error;
|
|
2664
2981
|
}
|
|
2982
|
+
finally {
|
|
2983
|
+
startupSignal?.removeEventListener("abort", cancelStartup);
|
|
2984
|
+
}
|
|
2665
2985
|
}
|
|
2666
|
-
async bindSessionExtensions(session, generation) {
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2669
|
-
//
|
|
2670
|
-
//
|
|
2671
|
-
//
|
|
2986
|
+
async bindSessionExtensions(session, generation, startupSignal) {
|
|
2987
|
+
const startupDialogs = { signal: startupSignal };
|
|
2988
|
+
const uiContext = this.sessionUiContext(session, generation, startupDialogs);
|
|
2989
|
+
// A session_start hook can park this bind on a browser dialog. Keep
|
|
2990
|
+
// startup lookup and cancellation available for the duration of the bind,
|
|
2991
|
+
// including paths which resolve a dialog before session creation returns.
|
|
2672
2992
|
this.startupSessions.set(session.sessionId, session);
|
|
2673
2993
|
try {
|
|
2674
2994
|
await session.bindExtensions({
|
|
@@ -2682,13 +3002,16 @@ export class PiSessionService {
|
|
|
2682
3002
|
});
|
|
2683
3003
|
}
|
|
2684
3004
|
finally {
|
|
3005
|
+
// Even a captured startup UI context must not carry plugin cancellation
|
|
3006
|
+
// into later user turns after startup has finished.
|
|
3007
|
+
startupDialogs.signal = undefined;
|
|
2685
3008
|
this.startupSessions.delete(session.sessionId);
|
|
2686
3009
|
}
|
|
2687
3010
|
}
|
|
2688
3011
|
replaceSessionNotificationContext(session, generation) {
|
|
2689
3012
|
session.extensionRunner.setUIContext(this.sessionUiContext(session, generation), "rpc");
|
|
2690
3013
|
}
|
|
2691
|
-
sessionUiContext(session, generation) {
|
|
3014
|
+
sessionUiContext(session, generation, startupDialogs) {
|
|
2692
3015
|
const baseUiContext = session.extensionRunner.getUIContext();
|
|
2693
3016
|
const notify = (message, type) => {
|
|
2694
3017
|
if (generation === undefined) {
|
|
@@ -2714,13 +3037,13 @@ export class PiSessionService {
|
|
|
2714
3037
|
if (property === "theme")
|
|
2715
3038
|
return plainTextTheme;
|
|
2716
3039
|
if (property === "confirm") {
|
|
2717
|
-
return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts);
|
|
3040
|
+
return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts, startupDialogs?.signal);
|
|
2718
3041
|
}
|
|
2719
3042
|
if (property === "select") {
|
|
2720
|
-
return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts);
|
|
3043
|
+
return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts, startupDialogs?.signal);
|
|
2721
3044
|
}
|
|
2722
3045
|
if (property === "input") {
|
|
2723
|
-
return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts);
|
|
3046
|
+
return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts, startupDialogs?.signal);
|
|
2724
3047
|
}
|
|
2725
3048
|
const value = Reflect.get(target, property, receiver);
|
|
2726
3049
|
return value;
|
|
@@ -2842,6 +3165,16 @@ export class PiSessionService {
|
|
|
2842
3165
|
this.unreadPublicationRetryTimer = undefined;
|
|
2843
3166
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
2844
3167
|
}
|
|
3168
|
+
connectSessionEvents(ref, lifetime) {
|
|
3169
|
+
const active = this.active.get(ref.id);
|
|
3170
|
+
if (active?.runtime.session.sessionId !== ref.id
|
|
3171
|
+
|| !cwdPathsEqual(active.runtime.session.sessionManager.getCwd(), ref.cwd)) {
|
|
3172
|
+
throw new Error("Selected session is not hosted in this workspace on this machine");
|
|
3173
|
+
}
|
|
3174
|
+
if (this.startupSessions.has(ref.id))
|
|
3175
|
+
throw new Error("Selected session is still initializing its extensions");
|
|
3176
|
+
return this.sessionEvents.connect(active.runtime.session, lifetime);
|
|
3177
|
+
}
|
|
2845
3178
|
bindRuntime(active, session = active.runtime.session) {
|
|
2846
3179
|
active.unsubscribe();
|
|
2847
3180
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
@@ -2851,7 +3184,7 @@ export class PiSessionService {
|
|
|
2851
3184
|
this.clearCompactionPromptQueue(sessionId);
|
|
2852
3185
|
}
|
|
2853
3186
|
}
|
|
2854
|
-
|
|
3187
|
+
const unsubscribe = session.subscribe((event) => {
|
|
2855
3188
|
this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel));
|
|
2856
3189
|
this.publishActivityForEvent(session, event);
|
|
2857
3190
|
const eventType = getString(event, "type");
|
|
@@ -2864,6 +3197,10 @@ export class PiSessionService {
|
|
|
2864
3197
|
this.publishStatus(session);
|
|
2865
3198
|
this.updateSubsessionTracking(session);
|
|
2866
3199
|
});
|
|
3200
|
+
active.unsubscribe = () => {
|
|
3201
|
+
this.sessionEvents.close(session);
|
|
3202
|
+
unsubscribe();
|
|
3203
|
+
};
|
|
2867
3204
|
this.active.set(session.sessionId, active);
|
|
2868
3205
|
}
|
|
2869
3206
|
scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
|
|
@@ -3025,6 +3362,7 @@ export class PiSessionService {
|
|
|
3025
3362
|
// the session still reports active work transiently, so the event-driven
|
|
3026
3363
|
// latch may not fire. The heartbeat re-checks once the session settles.
|
|
3027
3364
|
this.updateSubsessionTracking(session);
|
|
3365
|
+
void this.refreshActivityMarker(session);
|
|
3028
3366
|
const activity = this.activities.get(session.sessionId);
|
|
3029
3367
|
if (!this.hasActiveWork(session)) {
|
|
3030
3368
|
if (activity?.phase === "active")
|
|
@@ -3251,7 +3589,18 @@ export class PiSessionService {
|
|
|
3251
3589
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
3252
3590
|
this.observeUnreadActivityState(session);
|
|
3253
3591
|
}
|
|
3592
|
+
async refreshActivityMarker(session) {
|
|
3593
|
+
if (this.active.get(session.sessionId)?.runtime.session !== session)
|
|
3594
|
+
return;
|
|
3595
|
+
const previous = this.activityMarker.isActiveElsewhere(session.sessionFile);
|
|
3596
|
+
await this.activityMarker.refresh(session.sessionFile, this.hasActiveWork(session));
|
|
3597
|
+
if (this.active.get(session.sessionId)?.runtime.session === session
|
|
3598
|
+
&& previous !== this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3599
|
+
this.publishStatus(session);
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3254
3602
|
publishStatus(session) {
|
|
3603
|
+
void this.refreshActivityMarker(session);
|
|
3255
3604
|
const status = this.statusFromSession(session);
|
|
3256
3605
|
this.clearStaleActiveActivity(session);
|
|
3257
3606
|
this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status);
|
|
@@ -3270,7 +3619,7 @@ export class PiSessionService {
|
|
|
3270
3619
|
this.events.publish(session.sessionId, { type: "activity.update", activity });
|
|
3271
3620
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
3272
3621
|
}
|
|
3273
|
-
statusFromSession(session) {
|
|
3622
|
+
statusFromSession(session, messageCount = session.messages.length) {
|
|
3274
3623
|
const stats = session.getSessionStats();
|
|
3275
3624
|
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
|
3276
3625
|
const contextUsage = session.getContextUsage();
|
|
@@ -3287,7 +3636,7 @@ export class PiSessionService {
|
|
|
3287
3636
|
isBashRunning: session.isBashRunning,
|
|
3288
3637
|
pendingMessageCount: this.pendingMessageCount(session),
|
|
3289
3638
|
queuedMessages: queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)),
|
|
3290
|
-
messageCount
|
|
3639
|
+
messageCount,
|
|
3291
3640
|
tokens: stats.tokens,
|
|
3292
3641
|
cost: stats.cost,
|
|
3293
3642
|
...(contextUsage === undefined ? {} : { contextUsage }),
|
|
@@ -3305,6 +3654,13 @@ export class PiSessionService {
|
|
|
3305
3654
|
warningsForSession(session) {
|
|
3306
3655
|
const runtime = this.active.get(session.sessionId)?.runtime;
|
|
3307
3656
|
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
|
|
3657
|
+
if (this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3658
|
+
warnings.push({
|
|
3659
|
+
severity: "info",
|
|
3660
|
+
message: "Recently active in another PI-WEB instance. Avoid working on this session in both instances at once.",
|
|
3661
|
+
source: "PI-WEB",
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3308
3664
|
const anthropic = anthropicSubscriptionWarning(session, join(this.agentDir, "auth.json"));
|
|
3309
3665
|
if (anthropic !== undefined)
|
|
3310
3666
|
warnings.push(anthropic);
|
|
@@ -3402,7 +3758,7 @@ function clientSessionFromListEntry(session) {
|
|
|
3402
3758
|
created: session.created.toISOString(),
|
|
3403
3759
|
modified: session.modified.toISOString(),
|
|
3404
3760
|
messageCount: session.messageCount,
|
|
3405
|
-
firstMessage: session.firstMessage,
|
|
3761
|
+
firstMessage: clientSessionFirstMessagePreview(session.firstMessage),
|
|
3406
3762
|
...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
|
|
3407
3763
|
};
|
|
3408
3764
|
}
|
|
@@ -3505,7 +3861,7 @@ function clientSessionFromArchivedRecord(record, fallback) {
|
|
|
3505
3861
|
created,
|
|
3506
3862
|
modified,
|
|
3507
3863
|
messageCount,
|
|
3508
|
-
firstMessage,
|
|
3864
|
+
firstMessage: clientSessionFirstMessagePreview(firstMessage),
|
|
3509
3865
|
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
3510
3866
|
archived: true,
|
|
3511
3867
|
archivedAt: record.archivedAt,
|
|
@@ -3516,13 +3872,6 @@ function addSessionName(names, name) {
|
|
|
3516
3872
|
if (trimmed !== undefined && trimmed !== "")
|
|
3517
3873
|
names.add(trimmed);
|
|
3518
3874
|
}
|
|
3519
|
-
function compareArchivedRecords(a, b) {
|
|
3520
|
-
return archivedTimestamp(b) - archivedTimestamp(a);
|
|
3521
|
-
}
|
|
3522
|
-
function archivedTimestamp(record) {
|
|
3523
|
-
const time = Date.parse(record.archivedAt);
|
|
3524
|
-
return Number.isNaN(time) ? 0 : time;
|
|
3525
|
-
}
|
|
3526
3875
|
function isDefined(value) {
|
|
3527
3876
|
return value !== undefined;
|
|
3528
3877
|
}
|
|
@@ -3742,45 +4091,41 @@ function buildPromptOptions(behavior, images) {
|
|
|
3742
4091
|
options.images = images;
|
|
3743
4092
|
return Object.keys(options).length > 0 ? options : undefined;
|
|
3744
4093
|
}
|
|
3745
|
-
function
|
|
3746
|
-
return
|
|
4094
|
+
function historyMessages(session) {
|
|
4095
|
+
return historyMessagesFromEntries(session.sessionManager.getBranch());
|
|
3747
4096
|
}
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
if (thinkingLevel === undefined || thinkingLevel === "" || thinkingLevel === "off")
|
|
3756
|
-
return message;
|
|
3757
|
-
if (!isRecord(message) || message["role"] !== "assistant")
|
|
3758
|
-
return message;
|
|
3759
|
-
return { ...message, thinkingLevel };
|
|
4097
|
+
function transcriptMessageCount(entries) {
|
|
4098
|
+
let count = 0;
|
|
4099
|
+
for (const entry of entries) {
|
|
4100
|
+
if (isRecord(entry) && entry["type"] === "message")
|
|
4101
|
+
count += 1;
|
|
4102
|
+
}
|
|
4103
|
+
return count;
|
|
3760
4104
|
}
|
|
3761
|
-
function
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
4105
|
+
function activeEditableTreeTargetParentId(manager, targetId, activeLeafId) {
|
|
4106
|
+
if (activeLeafId !== targetId)
|
|
4107
|
+
return undefined;
|
|
4108
|
+
const entry = manager.getBranch().at(-1);
|
|
4109
|
+
if (!isRecord(entry) || entry["id"] !== targetId)
|
|
4110
|
+
return undefined;
|
|
4111
|
+
const isUserMessage = entry["type"] === "message"
|
|
4112
|
+
&& isRecord(entry["message"])
|
|
4113
|
+
&& entry["message"]["role"] === "user";
|
|
4114
|
+
if (!isUserMessage && entry["type"] !== "custom_message")
|
|
4115
|
+
return undefined;
|
|
4116
|
+
const parentId = entry["parentId"];
|
|
4117
|
+
if (parentId === targetId)
|
|
4118
|
+
return undefined;
|
|
4119
|
+
return parentId === null || typeof parentId === "string" ? parentId : undefined;
|
|
4120
|
+
}
|
|
4121
|
+
function setSessionTreeLeaf(manager, leafId) {
|
|
4122
|
+
if (leafId === null)
|
|
4123
|
+
manager.resetLeaf();
|
|
4124
|
+
else
|
|
4125
|
+
manager.branch(leafId);
|
|
4126
|
+
}
|
|
4127
|
+
function transcriptBranchIncludesEntry(entries, entryId) {
|
|
4128
|
+
return entries.some((entry) => isRecord(entry) && entry["id"] === entryId);
|
|
3784
4129
|
}
|
|
3785
4130
|
/** custom entry type used to persist parent -> child subsession links outside LLM context. */
|
|
3786
4131
|
const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
@@ -3838,6 +4183,13 @@ function toClientEvent(event, thinkingLevel) {
|
|
|
3838
4183
|
const result = getProperty(event, "result");
|
|
3839
4184
|
return { type: "tool.end", toolName: getString(event, "toolName") ?? "", toolCallId: getString(event, "toolCallId") ?? "", text: stringifyToolResult(result), content: toolResultContent(result), details: toolResultDetails(result), isError: getBoolean(event, "isError") === true };
|
|
3840
4185
|
}
|
|
4186
|
+
if (eventType === "entry_appended") {
|
|
4187
|
+
// Boundary drafts do not emit message_end. Use the same raw-history
|
|
4188
|
+
// projection as reconnects: context edits must not rewrite the transcript.
|
|
4189
|
+
const [message] = historyMessagesFromEntries([getProperty(event, "entry")]);
|
|
4190
|
+
if (message !== undefined)
|
|
4191
|
+
return { type: "message.append", message };
|
|
4192
|
+
}
|
|
3841
4193
|
if (eventType === "agent_start")
|
|
3842
4194
|
return { type: "agent.start" };
|
|
3843
4195
|
if (eventType === "agent_end")
|