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
60 changes: 60 additions & 0 deletions .github/releases/v1.0.69.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## opencode {VERSION}

{Prerelease/Stable} release from `{branch}` branch. DAG workers deliver complete final answers once, while hook guidance now explains how async hooks can resume an idle session.

---

### 🐛 Bug Fixes

- **Capture the worker's complete final answer**: New workflows use the persisted `final_response` protocol instead of requiring a separate `submit_result` report. The runtime collects all ordinary text parts of the final assistant answer, or validates one final JSON value when an output schema is present. Valid `null`, `false`, and zero results retain their original shape. Historical workflows without a protocol field continue using `submit_result` (#735).
- **Deliver the final report once**: An explicit delivery node or a unique terminal synthesis node supplies the report shown verbatim in the parent conversation. Durable receipts and wake markers commit together, preventing duplicate delivery after restart. Normal completed reports no longer start a parent model turn to repeat the answer; pause, failure, timeout, and clarification still use the existing decision path. Ambiguous outputs produce a deterministic result index (#735).
- **Wait for CLI report rendering before exit**: The CLI checks the durable delivery receipt and renders each new answer before exiting. Idle polling also renders reports committed by another process when no local answer event arrives, while source and answer identities prevent duplicate output from later events. Regressions cover multiple workflows, old idle events, equal-time message ordering, resumed sessions, and mixed report delivery with pending parent decisions (#735, #736).
- **Retry CLI receipt baselines without masking network errors**: Transient failures while reading the initial receipt state now retry within a bound. A network error is no longer treated as proof that a receipt is absent, so the CLI cannot start from an unknown delivery baseline (#736).
- **Commit EventV2 reports, receipts, and wakes together**: Report events, durable receipts, and wake markers are persisted in the same transaction; listeners are notified only after the transaction commits (#736).
- **Continue after stale final-response validation**: When validation of a saved `final_response` fails after new parent input arrives, the run becomes a continuation instead of a terminal failure (#736).
- **Settle pending user input only with its complete answer**: A real pending user message now requires its corresponding successful, complete assistant answer before delivery can be considered complete (#736).
- **Preserve structured JSON results**: Final JSON output retains its complete structured value through validation and delivery, including values used by downstream conditions and mappings. JSON strings keep their quotes, empty strings remain valid JSON, and objects shaped like file references retain every field (#736).
- **Invalidate stale extend and replan selections**: Extend/replan invalidates selections when their chosen point is no longer valid, and recovery remaps the selection to the current workflow state (#736).
- **Preserve fresh input during recovery**: A recovered answer without an associated snapshot rechecks the mailbox and guards failure by its accepted revision, so a new message continues the node instead of being discarded (#736).
- **Reject start-only fields on replans**: Replan fragments now reject `result_protocol` and `delivery_node` with field-specific diagnostics instead of silently ignoring them (#736).
- **Recognize completed local commands**: Completed `/memory`, `/trust`, `/goal`, and `/subgoal` responses unblock final report delivery without requiring a model-generated assistant message (#736).
- **Align CLI receipt detection with the runtime**: Invalid persisted workflow configurations use the same legacy fallback in the CLI and delivery runtime, avoiding a wait for a receipt the runtime cannot produce (#736).
- **Recognize failed local command responses**: Historical `/goal` and `/subgoal` failures stored as ended synthetic, ignored responses count as completed local turns. Ordinary or unfinished lookalike text still leaves the user request pending (#736).
- **Defer large report artifacts until admission**: Pending user requests and stale completion episodes are checked before large reports create managed files, preventing artifacts from delivery attempts that are already ineligible (#736).
- **Explain async hook continuation in built-in guidance**: Built-in hook guidance and prompts now describe `asyncRewake`, completion output, timeout and lifecycle constraints, one-shot behavior, and anti-loop handling. Review corrections cover environment-forced trust, HTTP failure feedback, worktree bootstrap timing, headless permission decisions, matcher restrictions, custom tool translation, and asynchronous plugin event observers. This is a guidance update; the asyncRewake runtime mechanism is unchanged (#737).

---

### 🧪 Test Summary

```text
DAG final delivery regressions: 47 passed
CLI receipt regressions: 23 passed
Latest delivery / CLI regressions: 80 passed, 298 assertions
Earlier four-file review regressions: 89 passed, 334 assertions
CLI process and hook guidance tests: 45 passed, 98 assertions
DAG mutation and stale-input tests: 16 passed, 49 assertions (2 test files)
Expanded DAG regression set: 101 passed, 451 assertions
Core mailbox regressions: 30 passed, 296 assertions
Hook and related regressions: 372 passed (core 12, opencode 360)
Package typechecks: core and opencode passed
Full DAG gate: core 153, runtime 969, schema 3, TUI 63 passed
DAG gate opt-in tests: 2 skipped; all coverage floors passed
Workspace typecheck: 31/31 tasks passed
Lint: 4739 warnings, 0 errors (cap 4750)
App Chromium / TUI report rendering: 1 passed each
```

---

### 🔍 Verification

The latest delivery, CLI receipt, and CLI process regressions, workspace typecheck, lint, and complete DAG gate passed after the final three delivery fixes. The earlier four-file review, mutation, expanded regression, core mailbox, hook guidance, and app Chromium results preceded these fixes; the repeated DAG gate includes TUI rendering and preserves all coverage floors. Both affected package typechecks passed during integration. Final review results are recorded in the pull request before merge.

Before the final three delivery fixes, integrated-source DAG acceptance passed independently with configured Qwen, GLM and DeepSeek: 39 assertions per model, two completed nodes, zero `submit_result` calls, one parent answer, and unchanged durable receipts after restart. A local test binary built and passed its version smoke test at that stage. Installation over `/usr/local/bin/opencode` was blocked by local administrator permissions, so these live runs used development source and are not claimed as installed-binary acceptance. The release pipeline independently verifies stable artifacts.

Hook self-authoring acceptance was repeated on the integrated source before the final three delivery fixes. An isolated Qwen session received one natural-language user prompt and loaded the built-in hook skill; the test driver registered no hooks, and the model authored the hook configuration and script. After 2.266907 seconds observably idle, a fresh nonce resumed the same session and appeared in its completed reply; no repeat was observed over the next 6 seconds. Cleanup confirmed the user's configuration was unchanged and the temporary credentials and task-owned processes were removed. The earlier GLM self-authoring attempt timed out before reaching idle and is not counted as a pass.

---

**Full changelog:** [`{previous_tag}`...`{current_tag}`](https://github.com/LeXwDeX/OpenCode-GraphAgent/compare/{previous_tag}...{current_tag})
112 changes: 112 additions & 0 deletions packages/app/e2e/regression/dag-delivery-render.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { expect, test } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"

const directory = "C:/OpenCode/DagDeliveryRender"
const projectID = "proj_dag_delivery_render"
const sessionID = "ses_dag_delivery_render"
const requestID = "msg_dag_delivery_request"
const deliveryID = "msg_dag_delivery_generated"
const followupID = "msg_dag_delivery_followup"
const sourceID = "prt_dag_delivery_source"
const answerID = "prt_dag_delivery_answer"
const followupTextID = "prt_dag_delivery_followup_text"
const title = "DAG delivery render"
const sourceText = "DAG · Release workflow · final-report"
const answerText = "DAG DELIVERY BODY: the release report is ready."
const followupText = "FOLLOWUP USER TURN remains visible."

const messages = [
{
info: {
id: requestID,
sessionID,
role: "user",
time: { created: 10 },
},
parts: [{ id: "prt_dag_delivery_request", sessionID, messageID: requestID, type: "text", text: "run workflow" }],
},
{
info: {
id: deliveryID,
sessionID,
parentID: requestID,
role: "assistant",
agent: "build",
mode: "build",
modelID: "test-model",
providerID: "opencode",
path: { cwd: directory, root: directory },
time: { created: 20, completed: 20 },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
finish: "stop",
},
parts: [
{
id: sourceID,
sessionID,
messageID: deliveryID,
type: "text",
text: sourceText,
time: { start: 20, end: 20 },
metadata: { dag_delivery: { kind: "source", workflow_id: "wf_release", node_id: "final-report" } },
},
{
id: answerID,
sessionID,
messageID: deliveryID,
type: "text",
text: answerText,
time: { start: 20, end: 20 },
metadata: { dag_delivery: { kind: "answer", workflow_id: "wf_release", node_id: "final-report" } },
},
],
},
{
info: {
id: followupID,
sessionID,
role: "user",
time: { created: 30 },
},
parts: [{ id: followupTextID, sessionID, messageID: followupID, type: "text", text: followupText }],
},
]

test("renders DAG delivery source and answer once, then renders the next user message", async ({ page }) => {
await mockOpenCodeServer(page, {
sessions: [
{
id: sessionID,
slug: "dag-delivery-render",
projectID,
title,
directory,
version: "test",
time: { created: 10, updated: 30 },
},
],
provider: { all: [], default: {}, connected: [] },
directory,
project: { id: projectID, worktree: directory, sandboxes: [] },
pageMessages: () => ({ items: messages }),
})

await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expectSessionTitle(page, title)

const source = page.locator(`[data-timeline-part-id="${sourceID}"]`)
const answer = page.locator(`[data-timeline-part-id="${answerID}"]`)
const followup = page.locator(`[data-timeline-part-id="${followupTextID}"]`)
await expect(source).toHaveCount(1)
await expect(source).toContainText(sourceText)
await expect(source).toBeVisible()
await expect(answer).toHaveCount(1)
await expect(answer).toContainText(answerText)
await expect(answer).toBeVisible()
await expect(followup).toHaveCount(1)
await expect(followup).toContainText(followupText)
await expect(followup).toBeVisible()
})
15 changes: 11 additions & 4 deletions packages/core/src/dag/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ export interface Guard {
close?: boolean
/** Conditional failure applies only to this accepted revision, checked in the settlement transaction. */
expectedAcceptedRevision?: number
/** Failure/cancellation outranks pending input unless expectedAcceptedRevision makes it conditional. */
/** Response-derived failure must still own its original associated input snapshot. */
requireCurrentInput?: boolean
/** Failure/cancellation outranks pending input unless explicitly made conditional. */
failureReason?: string
}
export interface Metadata {
Expand Down Expand Up @@ -673,10 +675,15 @@ export const layer = Layer.effect(
sql`SELECT * FROM agent_input_snapshot WHERE id = ${input.snapshotID} AND mailbox_id = ${box.id}`,
)
: undefined
if (!input.failureReason && box.revision > 0 && (!frozen || frozen.revision !== box.revision))
const checkSnapshot = !input.failureReason || input.requireCurrentInput === true
if (
checkSnapshot &&
((!frozen && (box.revision > 0 || input.snapshotID !== undefined)) ||
(frozen && frozen.revision !== box.revision))
)
return reject("stale_input")
if (!input.failureReason && frozen && !frozen.associated) return reject("unassociated")
if (!input.failureReason && frozen?.stop_reason && frozen.stop_reason !== "budget_exhausted")
if (checkSnapshot && frozen && !frozen.associated) return reject("unassociated")
if (checkSnapshot && frozen?.stop_reason && frozen.stop_reason !== "budget_exhausted")
return reject("stopped")
const claimed = yield* tx.get(
sql`UPDATE agent_mailbox SET closed_reason = ${input.close === false ? null : (input.failureReason ?? "completed")} WHERE id = ${box.id} AND revision = ${box.revision} AND closed_reason IS NULL RETURNING id`,
Expand Down
Loading
Loading