@jmfederico/pi-web 1.202609.1 → 1.202610.0
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/dist/client/assets/{index-AroYaCLx.js → index-_1GwJfSs.js} +353 -307
- package/dist/client/index.html +1 -1
- package/dist/pi-packages/captains-log/dist/browser/index.js +4 -3
- package/dist/pi-packages/captains-log/dist/browser/panel.js +2 -0
- package/dist/pi-packages/captains-log/docs/usage.md +3 -1
- package/dist/plugin-api.d.ts +13 -0
- package/dist/server/app.js +12 -19
- package/dist/server/app.js.map +1 -1
- package/dist/server/browserMessageProjection.js +39 -20
- package/dist/server/browserMessageProjection.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +31 -0
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/projects/projectLifecycleService.js +91 -0
- package/dist/server/projects/projectLifecycleService.js.map +1 -0
- package/dist/server/realtime/sessionEventHub.js +22 -9
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond/projectMutationRoutes.js +38 -0
- package/dist/server/sessiond/projectMutationRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +24 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +34 -9
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +8 -2
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +144 -18
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionMediaIndex.js +184 -0
- package/dist/server/sessions/sessionMediaIndex.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +46 -4
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionUnreadStore.js +26 -1
- package/dist/server/sessions/sessionUnreadStore.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +6 -1
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +6 -1
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +1 -1
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/filePreviewService.js +27 -3
- package/dist/server/workspaces/filePreviewService.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +24 -0
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/federatedRoutes.js +2 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/sessionMedia.js +6 -0
- package/dist/shared/sessionMedia.js.map +1 -0
- package/docs/plugins.md +16 -0
- package/package.json +3 -2
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
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
|
+
import { isSessionMediaId } from "../../shared/sessionMedia.js";
|
|
6
7
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
7
8
|
import { clientSessionFirstMessagePreview } from "./clientSessionPreview.js";
|
|
8
9
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
@@ -535,6 +536,8 @@ export class PiSessionService {
|
|
|
535
536
|
this.unpersistedTreeBranchLeaves = new WeakMap();
|
|
536
537
|
/** Counts async operations that may append an entry before they settle. */
|
|
537
538
|
this.sessionEntryMutationCounts = new WeakMap();
|
|
539
|
+
this.pendingPromptEchoes = new WeakMap();
|
|
540
|
+
this.publishedAssistantPartials = new WeakMap();
|
|
538
541
|
/** Settings-wide queue preventing enabled-model read/modify/write races across sessions. */
|
|
539
542
|
this.modelScopeMutationQueue = Promise.resolve();
|
|
540
543
|
/** Global and workspace-owned enabled-model states; live session scopes project one entry lazily. */
|
|
@@ -563,8 +566,8 @@ export class PiSessionService {
|
|
|
563
566
|
this.subsessionHydratedParents = new Set();
|
|
564
567
|
/**
|
|
565
568
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
566
|
-
* Armed when the child starts
|
|
567
|
-
*
|
|
569
|
+
* Armed when the child's agent starts a run or a prompt fails before starting
|
|
570
|
+
* one (not for standalone commands or compaction); completion disarms it.
|
|
568
571
|
*/
|
|
569
572
|
this.subsessionNotifyArmed = new Map();
|
|
570
573
|
this.notificationGenerationBySession = new WeakMap();
|
|
@@ -637,6 +640,9 @@ export class PiSessionService {
|
|
|
637
640
|
notificationCatalog() {
|
|
638
641
|
return this.notificationStore.catalogSnapshot();
|
|
639
642
|
}
|
|
643
|
+
async reconcileUnreadWorkspaces(cwds) {
|
|
644
|
+
await this.publishUnreadMutations(this.unreadStore.reconcileWorkspaces(cwds));
|
|
645
|
+
}
|
|
640
646
|
async unreadCatalog() {
|
|
641
647
|
await this.publishUnreadMutations([]);
|
|
642
648
|
return this.unreadStore.durableCatalogSnapshot();
|
|
@@ -744,7 +750,10 @@ export class PiSessionService {
|
|
|
744
750
|
finally {
|
|
745
751
|
await active.runtime.dispose();
|
|
746
752
|
}
|
|
747
|
-
})).finally(() =>
|
|
753
|
+
})).finally(async () => {
|
|
754
|
+
this.events.mediaIndex.clear();
|
|
755
|
+
await this.activityMarker.dispose();
|
|
756
|
+
});
|
|
748
757
|
await this.publishUnreadMutations([]);
|
|
749
758
|
}
|
|
750
759
|
async list(cwd) {
|
|
@@ -1518,31 +1527,34 @@ export class PiSessionService {
|
|
|
1518
1527
|
});
|
|
1519
1528
|
}
|
|
1520
1529
|
/**
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1524
|
-
* parent is busy and delivers immediately when it is idle).
|
|
1530
|
+
* Arm on a tracked child's agent_start, then notify the parent once it stops
|
|
1531
|
+
* working. Prompt failures before agent_start explicitly arm the same latch.
|
|
1532
|
+
* Standalone commands and compactions must not arm a completion notice.
|
|
1525
1533
|
*/
|
|
1526
|
-
updateSubsessionTracking(session) {
|
|
1534
|
+
updateSubsessionTracking(session, agentStarted = false) {
|
|
1527
1535
|
const link = this.subsessionLinkForActiveChild(session);
|
|
1528
1536
|
if (link === undefined)
|
|
1529
1537
|
return;
|
|
1530
1538
|
const childId = link.childSessionId;
|
|
1531
|
-
if (
|
|
1539
|
+
if (agentStarted) {
|
|
1532
1540
|
this.subsessionNotifyArmed.set(childId, true);
|
|
1533
1541
|
return;
|
|
1534
1542
|
}
|
|
1543
|
+
if (this.hasActiveWork(session))
|
|
1544
|
+
return;
|
|
1535
1545
|
if (this.subsessionNotifyArmed.get(childId) !== true)
|
|
1536
1546
|
return;
|
|
1537
1547
|
this.subsessionNotifyArmed.set(childId, false);
|
|
1538
|
-
const
|
|
1548
|
+
const activity = this.activities.get(childId);
|
|
1549
|
+
const status = activity?.phase === "error" ? "error" : "idle";
|
|
1550
|
+
const errorSection = status === "error" && activity?.detail !== undefined ? `Error: ${activity.detail}\n\n` : "";
|
|
1539
1551
|
const finalText = finalAssistantText(historyMessages(session));
|
|
1540
1552
|
const outputSection = formatSubsessionNotificationOutput(childId, finalText);
|
|
1541
1553
|
const workingIds = this.workingSubsessionIds(link.parentSessionId);
|
|
1542
1554
|
const next = workingIds.length === 0
|
|
1543
1555
|
? "No other tracked subsessions are working."
|
|
1544
1556
|
: `Still working: ${workingIds.join(", ")}. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.`;
|
|
1545
|
-
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${outputSection}`;
|
|
1557
|
+
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${errorSection}${outputSection}`;
|
|
1546
1558
|
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
|
1547
1559
|
}
|
|
1548
1560
|
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
@@ -1590,10 +1602,69 @@ export class PiSessionService {
|
|
|
1590
1602
|
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
1591
1603
|
this.logger.info({ parentSessionId: parentId, sessionId: childId, error: error instanceof Error ? error.message : String(error) }, "failed to notify parent of subsession completion");
|
|
1592
1604
|
}
|
|
1605
|
+
async transcriptSnapshot(ref, page) {
|
|
1606
|
+
const session = await this.getOrOpen(ref);
|
|
1607
|
+
const seqBeforeRead = this.events.currentSeq(session.sessionId);
|
|
1608
|
+
const activeAtCapture = this.hasActiveWork(session);
|
|
1609
|
+
const diskBranch = activeAtCapture ? undefined : await this.readableSessionBranch(ref, session);
|
|
1610
|
+
// Disk reads yield, even if a whole turn starts AND ends while they run.
|
|
1611
|
+
// From the final runtime read through the watermark capture nothing may yield.
|
|
1612
|
+
const branch = diskBranch === undefined || this.hasActiveWork(session) || this.events.currentSeq(session.sessionId) !== seqBeforeRead
|
|
1613
|
+
? session.sessionManager.getBranch()
|
|
1614
|
+
: diskBranch;
|
|
1615
|
+
const messages = historyMessagesFromEntries(branch);
|
|
1616
|
+
const userCount = messages.filter((message) => isRecord(message) && message["role"] === "user").length;
|
|
1617
|
+
const echoes = (this.pendingPromptEchoes.get(session) ?? []).filter((echo) => echo.userIndex >= userCount);
|
|
1618
|
+
messages.push(...echoes.map((echo) => echo.message));
|
|
1619
|
+
return {
|
|
1620
|
+
page: pageMessagesAtSafeBoundary(messages, page),
|
|
1621
|
+
status: this.statusFromSession(session, transcriptMessageCount(branch) + echoes.length),
|
|
1622
|
+
seq: this.events.currentSeq(session.sessionId),
|
|
1623
|
+
partial: this.publishedAssistantPartials.get(session) ?? null,
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1593
1626
|
async messages(ref, page) {
|
|
1594
1627
|
const session = await this.getOrOpen(ref);
|
|
1595
1628
|
return pageMessagesAtSafeBoundary(historyMessagesFromEntries(await this.readableSessionBranch(ref, session)), page);
|
|
1596
1629
|
}
|
|
1630
|
+
async media(ref, mediaId) {
|
|
1631
|
+
if (!isSessionMediaId(mediaId))
|
|
1632
|
+
throw new Error("Invalid media id");
|
|
1633
|
+
const index = this.events.mediaIndex;
|
|
1634
|
+
const session = this.activeForRef(ref)?.runtime.session ?? this.startupSessionForRef(ref);
|
|
1635
|
+
// Verify session/cwd ownership before consulting the session-scoped cache.
|
|
1636
|
+
// Cold binary reads do not construct an SDK runtime (which can migrate files).
|
|
1637
|
+
const archived = session === undefined ? await this.getArchived(ref) : undefined;
|
|
1638
|
+
const resolved = session === undefined && archived?.archivePath === undefined
|
|
1639
|
+
? await this.sessionManager.resolveSessionFile(ref.cwd, ref.id)
|
|
1640
|
+
: undefined;
|
|
1641
|
+
const sessionId = session?.sessionId ?? archived?.sessionId ?? resolved?.id;
|
|
1642
|
+
if (sessionId === undefined || (resolved !== undefined && !cwdPathsEqual(resolved.cwd, ref.cwd)))
|
|
1643
|
+
throw new Error("Session not found");
|
|
1644
|
+
const scope = { id: sessionId, cwd: canonicalizeStoredCwd(session?.sessionManager.getCwd() ?? archived?.cwd ?? resolved?.cwd ?? ref.cwd) };
|
|
1645
|
+
const cached = index.get(scope, mediaId);
|
|
1646
|
+
if (cached !== undefined)
|
|
1647
|
+
return cached;
|
|
1648
|
+
if (session !== undefined) {
|
|
1649
|
+
const live = index.find(scope, mediaId, [
|
|
1650
|
+
session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch(),
|
|
1651
|
+
session.messages,
|
|
1652
|
+
session.state.streamingMessage,
|
|
1653
|
+
this.publishedAssistantPartials.get(session),
|
|
1654
|
+
...(this.pendingPromptEchoes.get(session) ?? []).map((echo) => echo.message),
|
|
1655
|
+
]);
|
|
1656
|
+
if (live !== undefined)
|
|
1657
|
+
return live;
|
|
1658
|
+
}
|
|
1659
|
+
// Read disk on misses even for a cached idle runtime: other processes can
|
|
1660
|
+
// append images on any branch without emitting events through this daemon.
|
|
1661
|
+
const path = session?.sessionFile ?? session?.sessionManager.getSessionFile() ?? archived?.archivePath ?? resolved?.path
|
|
1662
|
+
?? (session === undefined ? undefined : (await this.sessionManager.resolveSessionFile(ref.cwd, sessionId))?.path);
|
|
1663
|
+
if (path === undefined)
|
|
1664
|
+
return undefined;
|
|
1665
|
+
const entries = await this.sessionManager.readEntries?.(path);
|
|
1666
|
+
return entries === undefined ? undefined : index.find(scope, mediaId, entries);
|
|
1667
|
+
}
|
|
1597
1668
|
async status(ref) {
|
|
1598
1669
|
const session = await this.sessionForStatusOrDialogClose(ref);
|
|
1599
1670
|
await this.activityMarker.refresh(session.sessionFile, this.hasActiveWork(session));
|
|
@@ -1844,14 +1915,49 @@ export class PiSessionService {
|
|
|
1844
1915
|
}
|
|
1845
1916
|
beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1846
1917
|
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
|
1847
|
-
|
|
1848
|
-
|
|
1918
|
+
const echoes = this.pendingPromptEchoes.get(session) ?? [];
|
|
1919
|
+
const userCount = historyMessagesFromEntries(session.sessionManager.getBranch()).filter((message) => isRecord(message) && message["role"] === "user").length;
|
|
1920
|
+
const echo = behavior === undefined && echoUserMessage
|
|
1921
|
+
? { userIndex: Math.max(userCount, ...echoes.map((pending) => pending.userIndex + 1)), message: userMessage(text, images) }
|
|
1922
|
+
: undefined;
|
|
1923
|
+
if (echo !== undefined) {
|
|
1924
|
+
// SDK input hooks may await before appending the user message. Keep the
|
|
1925
|
+
// already-published echo visible in snapshots until that append occurs.
|
|
1926
|
+
this.pendingPromptEchoes.set(session, [...echoes, echo]);
|
|
1927
|
+
this.events.publish(session.sessionId, { type: "message.append", message: echo.message }, { id: session.sessionId, cwd: session.sessionManager.getCwd() });
|
|
1928
|
+
}
|
|
1849
1929
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
1850
|
-
const
|
|
1930
|
+
const commandName = text.startsWith("/") ? text.slice(1).split(" ")[0] : undefined;
|
|
1931
|
+
const trackEarlyFailure = behavior === undefined
|
|
1932
|
+
&& this.subsessionLinkForActiveChild(session) !== undefined
|
|
1933
|
+
&& !session.extensionRunner.getRegisteredCommands().some((command) => command.invocationName === commandName);
|
|
1934
|
+
let agentStarted = false;
|
|
1935
|
+
const unsubscribe = !trackEarlyFailure ? undefined : session.subscribe((event) => {
|
|
1936
|
+
if (getString(event, "type") === "agent_start")
|
|
1937
|
+
agentStarted = true;
|
|
1938
|
+
});
|
|
1939
|
+
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions)).finally(() => {
|
|
1940
|
+
unsubscribe?.();
|
|
1941
|
+
if (echo !== undefined) {
|
|
1942
|
+
const remaining = (this.pendingPromptEchoes.get(session) ?? []).filter((pending) => pending !== echo);
|
|
1943
|
+
if (remaining.length === 0)
|
|
1944
|
+
this.pendingPromptEchoes.delete(session);
|
|
1945
|
+
else
|
|
1946
|
+
this.pendingPromptEchoes.set(session, remaining);
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1851
1949
|
void promptPromise.catch((error) => {
|
|
1852
1950
|
const message = error instanceof Error ? error.message : String(error);
|
|
1853
1951
|
this.publishActivity(session, "error", "error", message);
|
|
1854
1952
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1953
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1954
|
+
const link = this.subsessionLinkForActiveChild(session);
|
|
1955
|
+
// Model/auth validation can reject without any agent event. Wake the
|
|
1956
|
+
// parent, but do not re-arm a run that already emitted agent_start.
|
|
1957
|
+
if (trackEarlyFailure && !agentStarted && link !== undefined)
|
|
1958
|
+
this.subsessionNotifyArmed.set(link.childSessionId, true);
|
|
1959
|
+
this.updateSubsessionTracking(session);
|
|
1960
|
+
}
|
|
1855
1961
|
});
|
|
1856
1962
|
return promptPromise;
|
|
1857
1963
|
}
|
|
@@ -2617,6 +2723,7 @@ export class PiSessionService {
|
|
|
2617
2723
|
// Promises inside the dying runtime: settle them rather than dropping them.
|
|
2618
2724
|
this.endSessionExtensionDialogs(sessionId);
|
|
2619
2725
|
this.active.delete(sessionId);
|
|
2726
|
+
this.events.mediaIndex.forgetSession({ id: sessionId, cwd: active.runtime.session.sessionManager.getCwd() });
|
|
2620
2727
|
this.activities.delete(sessionId);
|
|
2621
2728
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
|
|
2622
2729
|
this.clearAuthLossWarningsForSession(sessionId);
|
|
@@ -3085,7 +3192,7 @@ export class PiSessionService {
|
|
|
3085
3192
|
// now rather than after the durable flush: it reads in-memory unread state
|
|
3086
3193
|
// and must not lag behind the rows the browser is about to see.
|
|
3087
3194
|
if (mutations.length > 0)
|
|
3088
|
-
this.onUnreadChanged?.();
|
|
3195
|
+
this.onUnreadChanged?.(mutations.some(({ event }) => event.unread !== null));
|
|
3089
3196
|
this.enqueueUnreadMutations(mutations);
|
|
3090
3197
|
this.unreadPublicationFlushRequested = true;
|
|
3091
3198
|
if (this.unreadPublication === undefined && this.unreadPublicationRetryTimer !== undefined) {
|
|
@@ -3185,9 +3292,27 @@ export class PiSessionService {
|
|
|
3185
3292
|
}
|
|
3186
3293
|
}
|
|
3187
3294
|
const unsubscribe = session.subscribe((event) => {
|
|
3188
|
-
this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel));
|
|
3189
|
-
this.publishActivityForEvent(session, event);
|
|
3190
3295
|
const eventType = getString(event, "type");
|
|
3296
|
+
const message = getProperty(event, "message");
|
|
3297
|
+
if ((eventType === "message_start" || eventType === "message_update") && getString(message, "role") === "assistant") {
|
|
3298
|
+
// SDK state advances before async extension hooks finish. Capture only
|
|
3299
|
+
// published messages, detaching even blocks left intact by projection.
|
|
3300
|
+
this.publishedAssistantPartials.set(session, structuredClone(annotateAssistantThinkingLevel(projectBrowserMessage(message), session.thinkingLevel)));
|
|
3301
|
+
}
|
|
3302
|
+
else if (eventType === "message_start" || eventType === "message_end" || eventType === "agent_end") {
|
|
3303
|
+
this.publishedAssistantPartials.delete(session);
|
|
3304
|
+
}
|
|
3305
|
+
this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel), { id: session.sessionId, cwd: session.sessionManager.getCwd() });
|
|
3306
|
+
this.publishActivityForEvent(session, event);
|
|
3307
|
+
// Queued messages can reach the model after an ask opened, even though
|
|
3308
|
+
// there was no ask to dismiss when the user originally submitted them.
|
|
3309
|
+
if (eventType === "message_start" && isRecord(event) && getString(event["message"], "role") === "user") {
|
|
3310
|
+
void this.voidOpenAskForUserMessage(session).catch((error) => {
|
|
3311
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3312
|
+
this.publishActivity(session, "error", "error", message);
|
|
3313
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3191
3316
|
if (eventType === "agent_end")
|
|
3192
3317
|
this.abortRunScopedExtensionDialogs(session.sessionId);
|
|
3193
3318
|
if (eventType === "compaction_end")
|
|
@@ -3195,11 +3320,12 @@ export class PiSessionService {
|
|
|
3195
3320
|
if (eventType === "agent_start" || eventType === "agent_end")
|
|
3196
3321
|
this.scheduleCompactionQueueDrain(session.sessionId);
|
|
3197
3322
|
this.publishStatus(session);
|
|
3198
|
-
this.updateSubsessionTracking(session);
|
|
3323
|
+
this.updateSubsessionTracking(session, eventType === "agent_start");
|
|
3199
3324
|
});
|
|
3200
3325
|
active.unsubscribe = () => {
|
|
3201
3326
|
this.sessionEvents.close(session);
|
|
3202
3327
|
unsubscribe();
|
|
3328
|
+
this.publishedAssistantPartials.delete(session);
|
|
3203
3329
|
};
|
|
3204
3330
|
this.active.set(session.sessionId, active);
|
|
3205
3331
|
}
|