@jmfederico/pi-web 1.202608.0 → 1.202608.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 +4 -4
- package/dist/cli.js +69 -7
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-D91Sp61M.js → CodeViewer-mEiBZFA5.js} +2 -2
- package/dist/client/assets/{TerminalPanel-fYrKc3jh.js → TerminalPanel-O9381Eme.js} +6 -6
- package/dist/client/assets/index-uI2AGFUy.js +4091 -0
- package/dist/client/index.html +1 -1
- package/dist/config.js +109 -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/nativeServices/servicePlan.js +1 -1
- package/dist/nativeServices/servicePlan.js.map +1 -1
- 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 +1 -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/relays/package.json +1 -1
- package/dist/pi-web-plugins/relays/pi-web-plugin.js +3 -3
- 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/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 +35 -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/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 +22 -0
- package/dist/server/machines/machineService.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 +28 -7
- 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/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/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 +265 -99
- 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 +141 -134
- 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/sessionRoutes.js +14 -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/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 +28 -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 +29 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +148 -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 +89 -51
- package/docs/plugins.md +383 -136
- 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 +29 -10
- package/server-plugin-api.d.ts +1 -0
- package/dist/client/assets/UnifiedDiffViewer-BafLWGtF.js +0 -39
- package/dist/client/assets/index-CA8q9_o7.js +0 -4025
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readFile,
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
6
6
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.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";
|
|
@@ -56,6 +54,14 @@ function spawnTargetError(decision) {
|
|
|
56
54
|
return new Error("Spawning session is not in a registered project");
|
|
57
55
|
return new Error(`cwd must be a workspace of this project. Allowed: ${decision.allowedCwds.join(", ")}`);
|
|
58
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Tracked subsessions are worktree-scoped, so a requested target other than the
|
|
59
|
+
* parent's own cwd fails closed instead of being silently retargeted. The
|
|
60
|
+
* message names the rule and both supported ways to get work done elsewhere.
|
|
61
|
+
*/
|
|
62
|
+
function subsessionCwdError(spawningCwd, requestedCwd) {
|
|
63
|
+
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.`);
|
|
64
|
+
}
|
|
59
65
|
function modelSpecOf(model) {
|
|
60
66
|
return `${model.provider}/${model.id}`;
|
|
61
67
|
}
|
|
@@ -275,9 +281,28 @@ export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn,
|
|
|
275
281
|
...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
|
|
276
282
|
];
|
|
277
283
|
}
|
|
278
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Resource-loader options that append PI WEB's own system-prompt sections.
|
|
286
|
+
*
|
|
287
|
+
* `appendSystemPromptOverride` composes with what the loader already resolved,
|
|
288
|
+
* so the operator's `SYSTEM.md` / `APPEND_SYSTEM.md` files keep their content
|
|
289
|
+
* and PI WEB's sections land after them. Returns `undefined` when there is
|
|
290
|
+
* nothing to add, leaving the loader exactly as pi configures it.
|
|
291
|
+
*/
|
|
292
|
+
export function piWebResourceLoaderOptions(appendSystemPromptSections) {
|
|
293
|
+
if (appendSystemPromptSections.length === 0)
|
|
294
|
+
return undefined;
|
|
295
|
+
return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
|
|
296
|
+
}
|
|
297
|
+
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
|
|
298
|
+
const resourceLoaderOptions = piWebResourceLoaderOptions(appendSystemPromptSections);
|
|
279
299
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
280
|
-
const services = await createAgentSessionServices({
|
|
300
|
+
const services = await createAgentSessionServices({
|
|
301
|
+
cwd,
|
|
302
|
+
agentDir,
|
|
303
|
+
modelRuntime,
|
|
304
|
+
...(resourceLoaderOptions === undefined ? {} : { resourceLoaderOptions }),
|
|
305
|
+
});
|
|
281
306
|
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
282
307
|
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
283
308
|
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, askUser);
|
|
@@ -347,13 +372,6 @@ export class PiSessionService {
|
|
|
347
372
|
this.subsessionLinks = new Map();
|
|
348
373
|
/** Parent id/file identities whose persisted links have already been loaded. */
|
|
349
374
|
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
375
|
/**
|
|
358
376
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
359
377
|
* Armed when the child starts working; firing on completion disarms it so a
|
|
@@ -366,46 +384,23 @@ export class PiSessionService {
|
|
|
366
384
|
this.pendingUnreadMutations = [];
|
|
367
385
|
this.unreadPublicationFlushRequested = false;
|
|
368
386
|
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
387
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
393
388
|
this.agentDir = deps.agentDir;
|
|
394
389
|
this.sessionManager = deps.sessionManager;
|
|
395
390
|
this.modelRuntime = deps.modelRuntime;
|
|
396
391
|
this.spawnTargets = deps.spawnTargets;
|
|
397
|
-
this.projectWorkspaces = deps.projectWorkspaces;
|
|
398
392
|
this.logger = deps.logger ?? noopLogger;
|
|
399
393
|
this.now = deps.now ?? (() => new Date());
|
|
400
394
|
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
401
395
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
396
|
+
this.onUnreadChanged = deps.onUnreadChanged;
|
|
402
397
|
this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
|
|
403
398
|
this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
|
|
404
399
|
this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
|
|
405
400
|
this.catalogRefreshStatus = deps.catalogRefreshStatus;
|
|
406
401
|
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
407
402
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
408
|
-
// Subsessions are
|
|
403
|
+
// Subsessions are gated behind their own flag, and they
|
|
409
404
|
// also require the spawn capability (they share its project-scope resolver).
|
|
410
405
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
411
406
|
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
@@ -413,7 +408,7 @@ export class PiSessionService {
|
|
|
413
408
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
414
409
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
415
410
|
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
416
|
-
}, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined);
|
|
411
|
+
}, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined, deps.appendSystemPromptSections ?? []);
|
|
417
412
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
418
413
|
this.workspaceActivity = deps.workspaceActivity;
|
|
419
414
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
@@ -541,8 +536,6 @@ export class PiSessionService {
|
|
|
541
536
|
this.subsessionLinks.clear();
|
|
542
537
|
this.subsessionHydratedParents.clear();
|
|
543
538
|
this.subsessionNotifyArmed.clear();
|
|
544
|
-
this.sessionHeaderCache.clear();
|
|
545
|
-
this.pendingSessionHeaderReads.clear();
|
|
546
539
|
this.notificationStore.clearAll("service-dispose");
|
|
547
540
|
await Promise.all(activeSessions.map(async (active) => {
|
|
548
541
|
active.unsubscribe();
|
|
@@ -573,83 +566,7 @@ export class PiSessionService {
|
|
|
573
566
|
.sort(compareArchivedRecords)
|
|
574
567
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
575
568
|
.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;
|
|
569
|
+
return [...unarchivedSessions, ...archivedSessions];
|
|
653
570
|
}
|
|
654
571
|
async start(cwd, options = {}) {
|
|
655
572
|
return this.startSession(cwd, options);
|
|
@@ -711,15 +628,22 @@ export class PiSessionService {
|
|
|
711
628
|
};
|
|
712
629
|
}
|
|
713
630
|
/**
|
|
714
|
-
* Start a *tracked* child session on behalf of a LLM.
|
|
715
|
-
* {@link spawnSession}
|
|
716
|
-
*
|
|
717
|
-
*
|
|
631
|
+
* Start a *tracked* child session on behalf of a LLM. Unlike
|
|
632
|
+
* {@link spawnSession}, a tracked child always runs in the parent's own
|
|
633
|
+
* workspace: parent/child trees are worktree-scoped, so a child elsewhere
|
|
634
|
+
* would be invisible to the parent's listing. The child records its parent
|
|
635
|
+
* (so it shows in the session tree) and is registered so the parent is
|
|
636
|
+
* notified when it stops working and can inspect it later.
|
|
718
637
|
*/
|
|
719
638
|
async spawnSubsession(input) {
|
|
720
639
|
if (this.spawnTargets === undefined)
|
|
721
640
|
throw new Error("Spawning sessions is disabled");
|
|
722
|
-
|
|
641
|
+
if (input.cwd !== undefined && input.cwd !== "" && !cwdPathsEqual(input.cwd, input.spawningCwd)) {
|
|
642
|
+
throw subsessionCwdError(input.spawningCwd, input.cwd);
|
|
643
|
+
}
|
|
644
|
+
// Resolved against the parent's own cwd only: this still refuses spawning
|
|
645
|
+
// from an unregistered directory, which keeps the child visible in the UI.
|
|
646
|
+
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, undefined);
|
|
723
647
|
if (!decision.allowed)
|
|
724
648
|
throw spawnTargetError(decision);
|
|
725
649
|
// A model spec overrides the inherited model and is resolved against the
|
|
@@ -756,10 +680,12 @@ export class PiSessionService {
|
|
|
756
680
|
/**
|
|
757
681
|
* The models a session may pick from: its scoped set when model-scoped,
|
|
758
682
|
* otherwise the runtime's available snapshot. Refreshes the runtime catalog
|
|
759
|
-
* first so callers see newly configured providers and models.
|
|
683
|
+
* first so callers see newly configured providers and models. The refresh
|
|
684
|
+
* stays local (`allowNetwork: false`); network refreshes belong to the
|
|
685
|
+
* bounded background catalog refresher, not this request path.
|
|
760
686
|
*/
|
|
761
687
|
async sessionModelCandidates(session) {
|
|
762
|
-
await session.modelRuntime.refresh();
|
|
688
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
763
689
|
return session.scopedModels.length > 0
|
|
764
690
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
765
691
|
: session.modelRuntime.getAvailableSnapshot();
|
|
@@ -1585,6 +1511,83 @@ export class PiSessionService {
|
|
|
1585
1511
|
}
|
|
1586
1512
|
}
|
|
1587
1513
|
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Fork the session from one entry of its tree into a new session file,
|
|
1516
|
+
* leaving the original session untouched. The forked runtime replaces the
|
|
1517
|
+
* current one, so the outcome is reported for the session the client is
|
|
1518
|
+
* about to join rather than the forked-from record.
|
|
1519
|
+
*/
|
|
1520
|
+
async forkFromTree(ref, request) {
|
|
1521
|
+
if (request.entryId.trim() === "")
|
|
1522
|
+
throw new Error("Session tree entry is required");
|
|
1523
|
+
if (this.isTreeExclusiveSessionIdentityActive(ref.id)) {
|
|
1524
|
+
throw new Error("Stop current session activity before forking the session tree");
|
|
1525
|
+
}
|
|
1526
|
+
await this.assertWritable(ref);
|
|
1527
|
+
const session = await this.getOrOpen(ref);
|
|
1528
|
+
if (this.hasActiveWork(session))
|
|
1529
|
+
throw new Error("Stop current session activity before forking the session tree");
|
|
1530
|
+
if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
|
|
1531
|
+
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
1532
|
+
}
|
|
1533
|
+
this.publishActivity(session, "forking session from entry", "active");
|
|
1534
|
+
this.publishStatus(session);
|
|
1535
|
+
try {
|
|
1536
|
+
const result = await this.commandService.forkEntry(session.sessionId, request.entryId, {
|
|
1537
|
+
expectedLeafId: request.expectedLeafId,
|
|
1538
|
+
});
|
|
1539
|
+
if (result.type === "unsupported")
|
|
1540
|
+
throw new Error(result.message);
|
|
1541
|
+
if (result.type !== "done")
|
|
1542
|
+
throw new Error("Session fork is unavailable");
|
|
1543
|
+
if (result.session === undefined) {
|
|
1544
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1545
|
+
this.publishActivity(session, "fork cancelled", "idle");
|
|
1546
|
+
this.publishStatus(session);
|
|
1547
|
+
}
|
|
1548
|
+
return { cancelled: true };
|
|
1549
|
+
}
|
|
1550
|
+
const forkedSession = this.active.get(result.session.id)?.runtime.session;
|
|
1551
|
+
if (forkedSession !== undefined && this.isCurrentActiveSession(forkedSession)) {
|
|
1552
|
+
this.publishActivity(forkedSession, "session forked", "idle");
|
|
1553
|
+
this.publishStatus(forkedSession);
|
|
1554
|
+
}
|
|
1555
|
+
if (result.session.id !== session.sessionId)
|
|
1556
|
+
this.clearSupersededSessionActivity(session);
|
|
1557
|
+
return {
|
|
1558
|
+
cancelled: false,
|
|
1559
|
+
session: result.session,
|
|
1560
|
+
...(result.promptDraft === undefined ? {} : { promptDraft: result.promptDraft }),
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
catch (error) {
|
|
1564
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1565
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1566
|
+
this.publishActivity(session, "fork failed", "error", message);
|
|
1567
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1568
|
+
this.publishStatus(session);
|
|
1569
|
+
}
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* A changed-id fork can rebind its runtime after a heartbeat published the
|
|
1575
|
+
* prior identity as active. Clear every observable owner before forgetting
|
|
1576
|
+
* that identity's local activity record.
|
|
1577
|
+
*/
|
|
1578
|
+
clearSupersededSessionActivity(session) {
|
|
1579
|
+
const sessionId = session.sessionId;
|
|
1580
|
+
if (this.activities.get(sessionId)?.phase === "active") {
|
|
1581
|
+
const at = new Date().toISOString();
|
|
1582
|
+
const stored = { phase: "idle", label: "idle", at };
|
|
1583
|
+
this.activities.set(sessionId, stored);
|
|
1584
|
+
const activity = { sessionId, ...stored };
|
|
1585
|
+
this.events.publish(sessionId, { type: "activity.update", activity });
|
|
1586
|
+
this.events.publishGlobal({ type: "activity.update", activity });
|
|
1587
|
+
}
|
|
1588
|
+
this.workspaceActivity?.removeSession(sessionId, session.sessionManager.getCwd());
|
|
1589
|
+
this.activities.delete(sessionId);
|
|
1590
|
+
}
|
|
1588
1591
|
async reloadSessionRuntime(session) {
|
|
1589
1592
|
if (this.hasActiveWork(session))
|
|
1590
1593
|
throw new Error("Stop current session activity before reloading");
|
|
@@ -1832,13 +1835,10 @@ export class PiSessionService {
|
|
|
1832
1835
|
if (sessionFile === undefined || sessionFile === "")
|
|
1833
1836
|
throw new Error("Session is not persisted");
|
|
1834
1837
|
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.
|
|
1838
|
+
// The header rewrite keeps the inode, and whenever it leaves the file's
|
|
1839
|
+
// size unchanged it is invisible to the gateway's summary memo, which
|
|
1840
|
+
// cannot detect such rewrites from identity + size alone and would keep
|
|
1841
|
+
// listing the old parent link until restart.
|
|
1842
1842
|
this.sessionManager.invalidateSessionFile(sessionFile);
|
|
1843
1843
|
clearParentSessionHeader(session.sessionManager);
|
|
1844
1844
|
this.unregisterSubsession(session.sessionId);
|
|
@@ -2106,9 +2106,11 @@ export class PiSessionService {
|
|
|
2106
2106
|
const { archivePath } = archived;
|
|
2107
2107
|
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
|
|
2108
2108
|
}
|
|
2109
|
-
// Resolve the session file directly by id: opening one
|
|
2110
|
-
//
|
|
2111
|
-
|
|
2109
|
+
// Resolve the session file directly by id: opening one known session must
|
|
2110
|
+
// not depend on — or wait behind — a full transcript listing of its whole
|
|
2111
|
+
// workspace. `getActive` routes prompt/shell/runCommand, so coupling it to
|
|
2112
|
+
// the listing would let an in-flight listing serialize unrelated sends.
|
|
2113
|
+
const match = await this.sessionManager.resolveSessionFile(ref.cwd, ref.id);
|
|
2112
2114
|
if (!match)
|
|
2113
2115
|
throw new Error("Session not found");
|
|
2114
2116
|
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
@@ -2424,6 +2426,11 @@ export class PiSessionService {
|
|
|
2424
2426
|
void this.publishUnreadMutations(mutations).catch(() => undefined);
|
|
2425
2427
|
}
|
|
2426
2428
|
publishUnreadMutations(mutations) {
|
|
2429
|
+
// The store applied the mutations already, so the status projection is told
|
|
2430
|
+
// now rather than after the durable flush: it reads in-memory unread state
|
|
2431
|
+
// and must not lag behind the rows the browser is about to see.
|
|
2432
|
+
if (mutations.length > 0)
|
|
2433
|
+
this.onUnreadChanged?.();
|
|
2427
2434
|
this.enqueueUnreadMutations(mutations);
|
|
2428
2435
|
this.unreadPublicationFlushRequested = true;
|
|
2429
2436
|
if (this.unreadPublication === undefined && this.unreadPublicationRetryTimer !== undefined) {
|