From 8c6e80a39ce5f904aeb22af2d827b38831ff50c3 Mon Sep 17 00:00:00 2001 From: pavelravvich Date: Wed, 4 Nov 2026 10:25:09 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(#746):=20a=20question=20role's=20pause?= =?UTF-8?q?=20is=20read=20back=20=E2=80=94=20its=20ask=20is=20the=20role's?= =?UTF-8?q?=20DTO,=20not=20a=20tool=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012Kh8JmysuhZyQRSpNyJdPg --- CHANGELOG.md | 5 +++ src/stores/framework-json.ts | 19 +++++++-- tests/stores/paused-execution.test.ts | 58 ++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c7cd7..5a62313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,11 @@ own process, no shared disk — share every store on one database. ### Fixed +- **A question role's pause is read back** (agbossjs/agboss#746) — the pause row check took every + `ask` for an approval's tool call (`callId`, `tool`, `summary`), so a question asked by a flow step + (agbossjs/agboss#659) or an agent's tool (#712) failed its execution reading its own pause. A + question's ask (`component.kind: "question"`) is kept as written; an approval's still needs its + tool call. The core's `pausedExecutionContract` now has the case. - **The README's wiring compiles** (agbossjs/agboss#749) — _1. List the stores_ imported `days` from `@agboss/core/units`, which has none; it reads `executionRecordRetention: hours(24 * 30)`. diff --git a/src/stores/framework-json.ts b/src/stores/framework-json.ts index c95a36c..59ed27c 100644 --- a/src/stores/framework-json.ts +++ b/src/stores/framework-json.ts @@ -1,4 +1,5 @@ import { + ComponentKind, PauseAnswerer, PauseResolution, PauseStatus, @@ -52,7 +53,9 @@ const StoredPause = z.looseObject({ agent: text, channel: text, answeredBy: z.enum(PauseAnswerer), - ask: z.looseObject({ callId: text, tool: text, summary: text }), + // an approval's tool call, or a question role's own ask (agbossjs/agboss#659, #712): the component says + ask: z.looseObject({}), + component: StoredComponent.optional(), status: z.enum(PauseStatus), isSent: z.boolean(), askedAt: text, @@ -60,6 +63,14 @@ const StoredPause = z.looseObject({ resolvedAt: optionalText, }); +/** The tool call an approval's pause asks about. */ +const StoredApprovalAsk = z.looseObject({ callId: text, tool: text, summary: text }); + +/** agbossjs/agboss#746: a question's ask is the role's own DTO; every other pause asks a tool call. */ +const isAskOfItsKind = (pause: z.infer): boolean => + pause.component?.kind === ComponentKind.Question || + StoredApprovalAsk.safeParse(pause.ask).success; + /** A paused run: the fields a resume relies on (`budgetUsd` may be `Infinity`: no cost limit). */ const StoredRun = z.looseObject({ status: z.literal("paused"), @@ -73,8 +84,10 @@ const StoredRun = z.looseObject({ const isPauseAnswer = (value: unknown): value is PauseAnswer => StoredPauseAnswer.safeParse(value).success; -const isPauseRecord = (value: unknown): value is PauseRecord => - StoredPause.safeParse(value).success; +const isPauseRecord = (value: unknown): value is PauseRecord => { + const parsed = StoredPause.safeParse(value); + return parsed.success && isAskOfItsKind(parsed.data); +}; const isPausedRun = (value: unknown): value is PausedExecution["run"] => StoredRun.safeParse(value).success; diff --git a/tests/stores/paused-execution.test.ts b/tests/stores/paused-execution.test.ts index 7beac22..d54d387 100644 --- a/tests/stores/paused-execution.test.ts +++ b/tests/stores/paused-execution.test.ts @@ -1,4 +1,10 @@ -import { PauseAnswerer, PauseResolution, PauseStatus, type PauseRecord } from "@agboss/core"; +import { + ComponentKind, + PauseAnswerer, + PauseResolution, + PauseStatus, + type PauseRecord, +} from "@agboss/core"; import { VirtualClock } from "@agboss/core/testing"; import { describe, expect, it } from "vitest"; import { PostgresPausedExecutionRepository } from "../../src/index.js"; @@ -33,7 +39,57 @@ const pauseOf = (pauseId: string, stepKey: string): PauseRecord => ({ askedAt: "2026-01-05T09:00:00.000Z", }); +/** agbossjs/agboss#746: a question role's pause — the role its component, its ask the role's DTO. */ +const questionPauseOf = (pauseId: string, stepKey: string): PauseRecord => ({ + ...pauseOf(pauseId, stepKey), + nodeId: "ship-question", + agent: "ship-question", + channel: "ship-question", + component: { + name: "ship-question", + kind: ComponentKind.Question, + promptHash: undefined, + schemaHash: "ddb0c5dd", + }, + ask: { name: "box-1" }, +}); + describe("PostgresPausedExecutionRepository", () => { + it("agbossjs/agboss#746: a question's pause, whose ask is no tool call, is read back as written", async () => { + const pauses = pausesOn(); + const asked = questionPauseOf("p-question", "step-question"); + + await pauses.findOrCreateByStepKey(asked); + await pauses.resolveByTenantIdAndPauseId( + "default", + "p-question", + { decision: { verdict: "yes" }, answerer: undefined }, + NINE, + ); + + expect(await pauses.findAllByTenantIdAndRootExecutionId("default", "run-1")).toEqual([ + expect.objectContaining({ pauseId: "p-question", ask: { name: "box-1" } }), + ]); + expect(await pauses.findByTenantIdAndPauseId("default", "p-question")).toMatchObject({ + component: { kind: ComponentKind.Question }, + status: PauseStatus.Resolved, + answer: { decision: { verdict: "yes" } }, + }); + }); + + it("an approval's pause whose ask lost its tool call still fails loudly when read", async () => { + const pauses = pausesOn(); + await pauses.findOrCreateByStepKey(pauseOf("p-no-call", "step-no-call")); + await onSchema( + database.url(), + `UPDATE agboss_pauses SET record = record #- '{ask,callId}' WHERE pause_id = 'p-no-call'`, + ); + + await expect(pauses.findByTenantIdAndPauseId("default", "p-no-call")).rejects.toThrow( + /a stored pause is not readable/, + ); + }); + it("markTaken turns a Resolved pause to Taken, and nothing else", async () => { const pauses = pausesOn(); await pauses.findOrCreateByStepKey(pauseOf("p-resolved", "step-resolved")); From 7e08f0d610ff40b086b53ffc72349d772f3d97a5 Mon Sep 17 00:00:00 2001 From: pavelravvich Date: Wed, 4 Nov 2026 10:51:51 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(#746):=20pin=20the=20core=20at=206063?= =?UTF-8?q?70a0=20=E2=80=94=20its=20pausedExecutionContract=20has=20the=20?= =?UTF-8?q?question=20pause=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012Kh8JmysuhZyQRSpNyJdPg --- .agboss-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agboss-version.json b/.agboss-version.json index 0079bb6..09bccea 100644 --- a/.agboss-version.json +++ b/.agboss-version.json @@ -2,6 +2,6 @@ "repository": "agbossjs/agboss", "branch": "main", "source": "commit", - "frameworkSha": "da8148e19697329dd3c4cddc3feaa09acda3daba", + "frameworkSha": "606370a0012178f69352275fc9a12860c524262f", "version": "0.1.0" }