Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/pi-receipt-trim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed
- A Chat message to Pi that starts or ends with a space, holds a tab, or ends with an `@mention`
no longer stays "waiting for transcript" forever and holds every later send on the pane behind it.
7 changes: 5 additions & 2 deletions server/core/pending.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const isClient = (owner: PendingOwner): owner is Client => owner !== FIRST_MESSA
/** a first message's lease is the delivering connection's, taken at each attempt: none is kept on the record */
type PendingItem = PendingRecord<PendingOwner, PendingLease | null>;
type SubmitReply = { ok: boolean; pending?: PendingMessage; code?: string; message?: string };
type PiTypedReceipt = { owner: Client; resumeKey?: string; message: PendingMessage; identity: PendingIdentity; anchor: PiDeliveryAnchor | null };
type PiTypedReceipt = { owner: Client; resumeKey?: string; message: PendingMessage; match: string; identity: PendingIdentity; anchor: PiDeliveryAnchor | null };
/** a first message refused before anything was typed because its connection could not type it yet stays queued for its next attach or turn */
const FIRST_MESSAGE_RETRY_CODES = new Set(["input_not_ready", "pending_lease_lost", "disconnected", "read_only"]);
/** a first message whose page left before it went, or that named none: held for an explicit Send now or Discard */
Expand Down Expand Up @@ -229,6 +229,9 @@ export function createPendingDelivery({ options, clients, attachments, sight, se
prunePi();
const receipt: PiTypedReceipt = {
owner, resumeKey: resumeKey(owner, paneId), identity: { ...identity }, anchor,
// Pi expands pasted tabs and trims its record, including closeMention's space (#2454).
// Keep that matching value separate from the saved copy of the text sent.
match: text.replace(/\t/g, " ").trim(),
message: { id: `pi:${crypto.randomUUID()}`, request_id: requestId, text, state: "uncertain", created_at: new Date().toISOString(),
error: anchor === null
? { code: "transcript_unconfirmed", message: "Pi's exact transcript could not be resolved before Enter" }
Expand Down Expand Up @@ -277,7 +280,7 @@ export function createPendingDelivery({ options, clients, attachments, sight, se
let matching = same.filter((item) => activePiReceipt(paneId, item) && path === item.anchor.path);
failPi(paneId, same.filter((item) => !matching.includes(item)));
if (matching.length === 0 || "unwritten" in found) return;
const candidates = matching.map((item) => ({ id: item.message.id, text: item.message.text, anchor: item.anchor }));
const candidates = matching.map((item) => ({ id: item.message.id, text: item.match, anchor: item.anchor }));
const entries = piConsumed.get(paneId) ?? new Set<string>();
piConsumed.set(paneId, entries);
let result = matchPiDeliveries(path, candidates, entries);
Expand Down
31 changes: 27 additions & 4 deletions server/pending-input.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ describe("Ovis-origin Pi transcript receipts", () => {
await f.socket.wait((frame) => frame.type === "pending-messages" && frame.removed?.some((item: any) => item.id === receipt.id && item.outcome === "consumed"));
}, 20_000);

it("tracks a maximum-length message with the space closing its file mention", async () => {
it("tracks a maximum-length message whose closing space Pi trims off its file mention", async () => {
const transcript = piFile("pi-best-effort");
const f = await setup("pi-best-effort", "pi", "› Message\n", {}, false, transcript);
f.socket.send({ type: "conversation-watch", pane_id: f.pane, enabled: true });
Expand All @@ -478,11 +478,11 @@ describe("Ovis-origin Pi transcript receipts", () => {
const receipt = sent.messages.find((item) => item.request_id === 914)!;
expect(receipt.text).toBe(`${text} `);
const consumed = f.socket.event((frame) => frame.removed?.some((item) => item.id === receipt.id && item.outcome === "consumed") === true);
appendFileSync(transcript, piLine("u1", "a0", "user", `${text} `));
appendFileSync(transcript, piLine("u1", "a0", "user", text));
await consumed;
}, 20_000);

it("matches the exact Pi-facing text after closing a trailing file mention", async () => {
it("matches Pi's trimmed record after closing a trailing file mention (#2454)", async () => {
const transcript = piFile("pi-facing-text");
const f = await setup("pi-facing-text", "pi", "› Message\n", {}, false, transcript);
f.socket.send({ type: "conversation-watch", pane_id: f.pane, enabled: true });
Expand All @@ -491,9 +491,32 @@ describe("Ovis-origin Pi transcript receipts", () => {
const sent = await f.socket.wait((frame) => frame.type === "pending-messages" && frame.messages.some((item: PendingMessage) => item.request_id === 915));
const receipt = sent.messages.find((item: PendingMessage) => item.request_id === 915);
expect(receipt.text).toBe("inspect @file ");
appendFileSync(transcript, piLine("u1", "a0", "user", "inspect @file "));
appendFileSync(transcript, piLine("u1", "a0", "user", "inspect @file"));
await f.socket.wait((frame) => frame.type === "pending-messages" && frame.removed?.some((item: any) => item.id === receipt.id && item.outcome === "consumed"));
}, 20_000);

it("confirms a message Pi records trimmed, and the send queued behind it (#2454)", async () => {
const transcript = piFile("pi-trimmed");
const f = await setup("pi-trimmed", "pi", "› Message\n", {}, false, transcript);
f.socket.send({ type: "conversation-watch", pane_id: f.pane, enabled: true });
// Pi's editor expands a pasted tab to four spaces, and it trims what it records
const padded = " \tdictated\tmessage \t";
f.socket.send({ type: "submit", id: 916, pane_id: f.pane, text: padded, payload: padded, delivery: "immediate" });
expect((await f.socket.result(916)).ok).toBe(true);
f.socket.send({ type: "submit", id: 917, pane_id: f.pane, text: "next", payload: "next", delivery: "immediate" });
expect((await f.socket.result(917)).ok).toBe(true);
const sent = await f.socket.wait((frame) => frame.type === "pending-messages" && [916, 917].every((id) => frame.messages.some((item: PendingMessage) => item.request_id === id)));
expect(sent.messages.find((item: PendingMessage) => item.request_id === 916).text).toBe(padded);
const ids = [916, 917].map((id) => sent.messages.find((item: PendingMessage) => item.request_id === id).id);
const consumed = new Set<string>();
const both = f.socket.event((frame) => {
for (const item of frame.removed ?? []) if (item.outcome === "consumed") consumed.add(item.id);
return ids.every((id) => consumed.has(id));
});
appendFileSync(transcript, piLine("u1", "a0", "user", "dictated message"));
appendFileSync(transcript, piLine("u2", "u1", "user", "next"));
await both;
}, 20_000);
});

describe("connection-owned pending input", () => {
Expand Down
Loading