diff --git a/packages/durable/CHANGELOG.md b/packages/durable/CHANGELOG.md index 88683db4b..9ef9732d9 100644 --- a/packages/durable/CHANGELOG.md +++ b/packages/durable/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- `Tx.submission(id)` reads a committed submission record by ID, so a tool can find the entries of its run's inputs (`pi.live.run.inputs`) without scanning the transcript. + ### Changed - `SqliteStorage.commit()` costs a few SQL statements per call instead of about eight per batch: it reads the check state of all batches up front, checks them in memory, and writes the accepted batches per table in multi-row statements (rows as one JSON array parameter; rows with much text bound column by column), so a group commit over a remote SQLite takes a few round trips, not hundreds. A stored document record that cannot be parsed now rejects the call as a storage failure instead of refusing the batch. diff --git a/packages/durable/docs/spec.md b/packages/durable/docs/spec.md index 509dd29a6..fcae714a3 100644 --- a/packages/durable/docs/spec.md +++ b/packages/durable/docs/spec.md @@ -1439,6 +1439,7 @@ interface Tx { scanConversations(query: ConversationQuery, limit: number, cursor?: Cursor): Promise>; scanEntries(query: EntryQuery, limit: number, cursor?: Cursor): Promise>; scanTasks(query: TaskQuery, limit: number, cursor?: Cursor): Promise, Cursor>>; + submission(id: SubmissionId): Promise; submissionByRequest(conversationId: ConversationId, requestId: string): Promise; createConversation(options: { readonly ownership: ConversationOwnership }): Promise; diff --git a/packages/durable/src/session/transaction.ts b/packages/durable/src/session/transaction.ts index e6a18e781..534807a84 100644 --- a/packages/durable/src/session/transaction.ts +++ b/packages/durable/src/session/transaction.ts @@ -338,7 +338,7 @@ export class Transaction implements Tx { return this.#read("scanTasks", () => this.#host.storage.scanTasks(query, limit, cursor, this.#context)); } - /** Internal: committed submission record. */ + /** Committed submission record. */ submission(id: SubmissionId): Promise { return this.#read("submission", () => this.#host.storage.submission(id, this.#context)); } diff --git a/packages/durable/src/types.ts b/packages/durable/src/types.ts index c59827ce5..583c02d9f 100644 --- a/packages/durable/src/types.ts +++ b/packages/durable/src/types.ts @@ -1007,6 +1007,8 @@ export interface Tx { limit: number, cursor?: Cursor, ): Promise, Cursor>>; + /** Committed submission record, such as one of `pi.live.run.inputs`; a placed input's `entry` is its transcript entry. */ + submission(id: SubmissionId): Promise; /** Committed submission with a conversation-scoped request ID. */ submissionByRequest(conversationId: ConversationId, requestId: string): Promise; diff --git a/packages/durable/test/harness-submit-in-commit.test.ts b/packages/durable/test/harness-submit-in-commit.test.ts index 9f4e08d9e..1edb9e422 100644 --- a/packages/durable/test/harness-submit-in-commit.test.ts +++ b/packages/durable/test/harness-submit-in-commit.test.ts @@ -282,6 +282,22 @@ describe("submit inside the caller's commit", () => { await harness.close(context); }); + it("reads a submission by ID in a commit, with the entry its input was placed at", async () => { + const setup = chatSetup(); + setup.faux.setResponses([fauxAssistantMessage([fauxText("answer")])]); + const { harness, root } = await openChat(new ControlledStorage(), setup); + const id = await harness.commit((tx) => root.submit(tx, { type: "input", content: "hi" }), context); + await settledStatus(harness, id); + const found = await harness.commit(async (tx) => { + const submission = await tx.submission(id); + const entry = submission?.entry === undefined ? undefined : await tx.entry(submission.entry); + return { status: submission?.status, conversationId: entry?.conversationId, kind: entry?.kind }; + }, context); + expect(found).toEqual({ status: "done", conversationId: root.id, kind: "pi.user" }); + expect(await harness.commit((tx) => tx.submission(999_999 as SubmissionId), context)).toBeUndefined(); + await harness.close(context); + }); + // #11149 it("queues a model-visible write while busy and places it at the next boundary", async () => { const setup = chatSetup();