@jmfederico/pi-web 1.202610.2 → 1.202610.3

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.
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { statSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { readFile, writeFile } from "node:fs/promises";
6
+ import { QueueCompatibilityGuard, mirroredQueueEntries, mutableQueueAgent, mutableTextMirror, parseQueueMessageMutation, queuedMessageAttachmentCounts, queuedMessageAttachmentCount, snapshotAgentQueues } from "./queuedMessageMutation.js";
6
7
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEventBus, createEditToolDefinition, defineTool, hasTrustRequiringProjectResources, ProjectTrustStore, readStoredCredential, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
7
8
  import { projectBrowserMessage } from "../browserMessageProjection.js";
8
9
  import { isSessionMediaId } from "../../shared/sessionMedia.js";
@@ -600,6 +601,9 @@ export class PiSessionService {
600
601
  * one (not for standalone commands or compaction); completion disarms it.
601
602
  */
602
603
  this.subsessionNotifyArmed = new Map();
604
+ this.queueCompatibility = new QueueCompatibilityGuard((session, err) => {
605
+ this.logger.info({ sessionId: session.sessionId, err }, "Disabled SDK-dependent queue controls after an incompatible queue interface");
606
+ });
603
607
  this.notificationGenerationBySession = new WeakMap();
604
608
  /** Last idle-poll transcript file resolution per runtime, throttled; entries die with their runtime. */
605
609
  this.idleSessionFileResolutions = new WeakMap();
@@ -2076,7 +2080,7 @@ export class PiSessionService {
2076
2080
  submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
2077
2081
  return this.beginPromptSubmission(session, text, behavior, images, echoUserMessage).catch(() => undefined);
2078
2082
  }
2079
- beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true) {
2083
+ beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true, expandPromptTemplates = true) {
2080
2084
  this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
2081
2085
  const echoes = this.pendingPromptEchoes.get(session) ?? [];
2082
2086
  const userCount = historyMessagesFromEntries(session.sessionManager.getBranch()).filter((message) => isRecord(message) && message["role"] === "user").length;
@@ -2089,8 +2093,8 @@ export class PiSessionService {
2089
2093
  this.pendingPromptEchoes.set(session, [...echoes, echo]);
2090
2094
  this.events.publish(session.sessionId, { type: "message.append", message: this.browserTranscriptMessage(session, echo.message) }, { id: session.sessionId, cwd: session.sessionManager.getCwd() });
2091
2095
  }
2092
- const promptOptions = buildPromptOptions(behavior, images);
2093
- const commandName = text.startsWith("/") ? text.slice(1).split(" ")[0] : undefined;
2096
+ const promptOptions = expandPromptTemplates ? buildPromptOptions(behavior, images) : { ...buildPromptOptions(behavior, images), expandPromptTemplates: false };
2097
+ const commandName = expandPromptTemplates && text.startsWith("/") ? text.slice(1).split(" ")[0] : undefined;
2094
2098
  const trackEarlyFailure = behavior === undefined
2095
2099
  && this.subsessionLinkForActiveChild(session) !== undefined
2096
2100
  && !session.extensionRunner.getRegisteredCommands().some((command) => command.invocationName === commandName);
@@ -2718,6 +2722,124 @@ export class PiSessionService {
2718
2722
  this.unregisterSubsession(session.sessionId);
2719
2723
  await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
2720
2724
  }
2725
+ async mutateQueueMessage(ref, input) {
2726
+ const mutation = parseQueueMessageMutation(input);
2727
+ await this.assertWritable(ref);
2728
+ const session = await this.getOrOpen(ref);
2729
+ // Compare the entire execution-order queue after all awaits, then mutate
2730
+ // synchronously so neither another request nor the agent loop can interleave.
2731
+ const current = this.queuedMessages(session);
2732
+ if (this.queueCompatibility.reason(session) !== undefined) {
2733
+ this.publishStatus(session);
2734
+ this.queueCompatibility.assertAvailable(session);
2735
+ }
2736
+ if (current.length !== mutation.expectedQueue.length || current.some((entry, index) => {
2737
+ const expected = mutation.expectedQueue[index];
2738
+ if (expected === undefined)
2739
+ return true;
2740
+ return entry.kind !== expected.kind || entry.text !== expected.text
2741
+ || (mutation.action === "edit" && index === mutation.index && (entry.hasAttachments === true) !== (expected.hasAttachments === true));
2742
+ }))
2743
+ throw new Error("Queued messages changed; refresh the queue and try again");
2744
+ if (mutation.index >= current.length)
2745
+ throw new Error("Invalid queue index");
2746
+ try {
2747
+ this.queueCompatibility.run(session, () => { this.applyQueueMessageMutation(session, mutation); });
2748
+ }
2749
+ catch (error) {
2750
+ this.publishStatus(session);
2751
+ throw error;
2752
+ }
2753
+ this.publishStatus(session);
2754
+ return this.statusFromSession(session);
2755
+ }
2756
+ applyQueueMessageMutation(session, mutation) {
2757
+ const agent = mutableQueueAgent(session.agent);
2758
+ const mirrors = { steer: mutableTextMirror(session.getSteeringMessages()), followUp: mutableTextMirror(session.getFollowUpMessages()) };
2759
+ if (session.getSteeringMessages() !== mirrors.steer || session.getFollowUpMessages() !== mirrors.followUp) {
2760
+ throw new Error("SDK queue getters no longer expose stable backing arrays");
2761
+ }
2762
+ const queues = snapshotAgentQueues(agent);
2763
+ const originalQueues = { steer: [...queues.steer], followUp: [...queues.followUp] };
2764
+ const originalMirrors = { steer: [...mirrors.steer], followUp: [...mirrors.followUp] };
2765
+ const held = this.compactionQueuedMessages(session.sessionId).map((prompt) => ({ ...prompt }));
2766
+ const entries = ["steer", "followUp"].flatMap((kind) => [
2767
+ ...mirroredQueueEntries(queues[kind], mirrors[kind]).map((entry) => ({ ...entry, kind })),
2768
+ ...held.filter((prompt) => prompt.kind === kind).map((prompt) => ({ kind, text: prompt.text, prompt })),
2769
+ ]);
2770
+ const selected = entries[mutation.index];
2771
+ if (selected === undefined)
2772
+ throw new Error("Invalid queue index");
2773
+ const removed = mutation.action === "edit" ? entries.slice(mutation.index) : [selected];
2774
+ const remaining = entries.filter((entry) => !removed.includes(entry));
2775
+ for (const entry of removed) {
2776
+ if (entry.message !== undefined) {
2777
+ const queue = queues[entry.kind];
2778
+ queue.splice(queue.indexOf(entry.message), 1);
2779
+ }
2780
+ if (entry.prompt !== undefined)
2781
+ held.splice(held.indexOf(entry.prompt), 1);
2782
+ }
2783
+ if (mutation.action === "toggle") {
2784
+ const kind = selected.kind === "steer" ? "followUp" : "steer";
2785
+ // Destination includes held prompts: append there when necessary to keep
2786
+ // status order and eventual execution order identical.
2787
+ if (selected.prompt !== undefined) {
2788
+ selected.prompt.kind = kind;
2789
+ held.push(selected.prompt);
2790
+ remaining.push({ ...selected, kind });
2791
+ }
2792
+ else if (selected.message !== undefined) {
2793
+ if (held.some((prompt) => prompt.kind === kind)) {
2794
+ const prompt = { kind, text: selected.text, message: selected.message };
2795
+ held.push(prompt);
2796
+ remaining.push({ kind, text: selected.text, prompt });
2797
+ }
2798
+ else {
2799
+ queues[kind].push(selected.message);
2800
+ remaining.push({ ...selected, kind });
2801
+ }
2802
+ }
2803
+ }
2804
+ try {
2805
+ agent.clearAllQueues();
2806
+ for (const message of queues.steer)
2807
+ agent.steer(message);
2808
+ for (const message of queues.followUp)
2809
+ agent.followUp(message);
2810
+ for (const kind of ["steer", "followUp"]) {
2811
+ mirrors[kind].splice(0, mirrors[kind].length, ...remaining.filter((entry) => entry.kind === kind && entry.message !== undefined).map((entry) => entry.text));
2812
+ }
2813
+ }
2814
+ catch (error) {
2815
+ // Best-effort rollback when a callable SDK method fails during replacement.
2816
+ // Restoration itself can fail after an incompatible behavioral change.
2817
+ const failures = [error];
2818
+ try {
2819
+ agent.clearAllQueues();
2820
+ for (const message of originalQueues.steer)
2821
+ agent.steer(message);
2822
+ for (const message of originalQueues.followUp)
2823
+ agent.followUp(message);
2824
+ }
2825
+ catch (restoreError) {
2826
+ failures.push(restoreError);
2827
+ }
2828
+ for (const kind of ["steer", "followUp"]) {
2829
+ try {
2830
+ mirrors[kind].splice(0, mirrors[kind].length, ...originalMirrors[kind]);
2831
+ }
2832
+ catch (restoreError) {
2833
+ failures.push(restoreError);
2834
+ }
2835
+ }
2836
+ throw new AggregateError(failures, "Could not replace SDK queues; attempted to restore their original contents", { cause: error });
2837
+ }
2838
+ if (held.length === 0)
2839
+ this.clearCompactionPromptQueue(session.sessionId);
2840
+ else
2841
+ this.compactionPromptQueues.set(session.sessionId, held);
2842
+ }
2721
2843
  async clearQueue(ref) {
2722
2844
  await this.assertWritable(ref);
2723
2845
  const session = await this.getOrOpen(ref);
@@ -3644,16 +3766,25 @@ export class PiSessionService {
3644
3766
  return;
3645
3767
  this.publishStatus(session);
3646
3768
  for (const prompt of queued)
3647
- void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images, prompt.echoUserMessage ?? true);
3769
+ void this.submitHeldPrompt(session, prompt, prompt.kind);
3648
3770
  return;
3649
3771
  }
3650
3772
  const prompt = this.shiftCompactionPrompt(sessionId);
3651
3773
  if (prompt === undefined)
3652
3774
  return;
3653
3775
  this.publishStatus(session);
3654
- const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images, prompt.echoUserMessage ?? true);
3776
+ const submitted = this.submitHeldPrompt(session, prompt);
3655
3777
  void submitted.finally(() => { this.scheduleCompactionQueueDrain(sessionId); });
3656
3778
  }
3779
+ submitHeldPrompt(session, prompt, kind) {
3780
+ if (prompt.message === undefined)
3781
+ return this.submitPrompt(session, prompt.text, kind, prompt.images, prompt.echoUserMessage ?? true);
3782
+ // Use the public session API even after the queue adapter has been disabled.
3783
+ // Keep auth, run ownership and settlement SDK-owned; text is already expanded.
3784
+ const images = prompt.message.role === "user" && Array.isArray(prompt.message.content)
3785
+ ? prompt.message.content.filter((part) => part.type === "image") : [];
3786
+ return this.beginPromptSubmission(session, prompt.text, kind, images, true, false).catch(() => undefined);
3787
+ }
3657
3788
  takeCompactionPromptQueue(sessionId) {
3658
3789
  const queued = this.compactionPromptQueues.get(sessionId) ?? [];
3659
3790
  this.compactionPromptQueues.delete(sessionId);
@@ -3661,7 +3792,8 @@ export class PiSessionService {
3661
3792
  }
3662
3793
  shiftCompactionPrompt(sessionId) {
3663
3794
  const queue = this.compactionPromptQueues.get(sessionId);
3664
- const prompt = queue?.shift();
3795
+ const steeringIndex = queue?.findIndex((prompt) => prompt.kind === "steer") ?? -1;
3796
+ const prompt = queue?.splice(steeringIndex < 0 ? 0 : steeringIndex, 1)[0];
3665
3797
  if (queue === undefined || queue.length === 0)
3666
3798
  this.compactionPromptQueues.delete(sessionId);
3667
3799
  return prompt;
@@ -4038,6 +4170,8 @@ export class PiSessionService {
4038
4170
  const warnings = this.warningsForSession(session);
4039
4171
  const pendingDialogs = this.pendingExtensionDialogStore.pendingDialogs(session.sessionId);
4040
4172
  const suggestedInput = this.suggestedInputs.get(session.sessionId);
4173
+ const queuedMessages = this.queuedMessages(session);
4174
+ const queueMessageActionsUnavailable = this.queueCompatibility.reason(session);
4041
4175
  return {
4042
4176
  sessionId: session.sessionId,
4043
4177
  persisted: sessionFileExists(session.sessionFile),
@@ -4048,7 +4182,8 @@ export class PiSessionService {
4048
4182
  isCompacting: session.isCompacting,
4049
4183
  isBashRunning: session.isBashRunning,
4050
4184
  pendingMessageCount: this.pendingMessageCount(session),
4051
- queuedMessages: queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)),
4185
+ queuedMessages,
4186
+ ...(queueMessageActionsUnavailable === undefined ? {} : { queueMessageActionsUnavailable }),
4052
4187
  messageCount,
4053
4188
  tokens: stats.tokens,
4054
4189
  cost: stats.cost,
@@ -4078,8 +4213,12 @@ export class PiSessionService {
4078
4213
  compactionQueuedMessages(sessionId) {
4079
4214
  return this.compactionPromptQueues.get(sessionId) ?? [];
4080
4215
  }
4216
+ queuedMessages(session) {
4217
+ const held = this.compactionQueuedMessages(session.sessionId);
4218
+ return this.queueCompatibility.inspect(session, () => queuedMessagesFromSession(session, held), () => queuedMessagesFromSession(session, held, false));
4219
+ }
4081
4220
  hasQueuedMessageText(session, text) {
4082
- return queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)).some((message) => message.text === text);
4221
+ return this.queuedMessages(session).some((message) => message.text === text);
4083
4222
  }
4084
4223
  }
4085
4224
  function previewResponseFromPlan(plan) {
@@ -4462,12 +4601,37 @@ function clearParentSessionHeader(sessionManager) {
4462
4601
  function clearSessionQueue(session) {
4463
4602
  session.clearQueue();
4464
4603
  }
4465
- function queuedMessagesFromSession(session, extraQueuedMessages = []) {
4466
- return [
4467
- ...session.getSteeringMessages().map((text) => ({ kind: "steer", text })),
4468
- ...session.getFollowUpMessages().map((text) => ({ kind: "followUp", text })),
4469
- ...extraQueuedMessages,
4470
- ];
4604
+ function queuedMessagesFromSession(session, extraQueuedMessages = [], inspectSdk = true) {
4605
+ const texts = inspectSdk
4606
+ ? { steer: mutableTextMirror(session.getSteeringMessages()), followUp: mutableTextMirror(session.getFollowUpMessages()) }
4607
+ : { steer: publicQueueTexts(() => session.getSteeringMessages()), followUp: publicQueueTexts(() => session.getFollowUpMessages()) };
4608
+ const attachmentCounts = { steer: [], followUp: [] };
4609
+ if (inspectSdk && (texts.steer.length > 0 || texts.followUp.length > 0 || extraQueuedMessages.length > 0)) {
4610
+ const queues = snapshotAgentQueues(mutableQueueAgent(session.agent));
4611
+ for (const kind of ["steer", "followUp"]) {
4612
+ attachmentCounts[kind] = queuedMessageAttachmentCounts(queues[kind], texts[kind]);
4613
+ }
4614
+ }
4615
+ return ["steer", "followUp"].flatMap((kind) => [
4616
+ ...texts[kind].map((text, index) => ({ kind, text, ...queuedAttachmentMetadata(attachmentCounts[kind][index] ?? 0) })),
4617
+ ...extraQueuedMessages.filter((message) => message.kind === kind).map((prompt) => ({
4618
+ kind, text: prompt.text,
4619
+ ...queuedAttachmentMetadata(prompt.message === undefined ? prompt.images?.length ?? 0 : queuedMessageAttachmentCount(prompt.message)),
4620
+ })),
4621
+ ]);
4622
+ }
4623
+ /** Degraded status still uses public text getters where their values are usable. */
4624
+ function publicQueueTexts(read) {
4625
+ try {
4626
+ const value = read();
4627
+ return Array.isArray(value) ? value.filter((text) => typeof text === "string") : [];
4628
+ }
4629
+ catch {
4630
+ return [];
4631
+ } // The compatibility guard already records the original failure.
4632
+ }
4633
+ function queuedAttachmentMetadata(attachmentCount) {
4634
+ return attachmentCount > 0 ? { hasAttachments: true, attachmentCount } : {};
4471
4635
  }
4472
4636
  function userTextMessage(text) {
4473
4637
  return { role: "user", content: text };