From 0ff5312744fd73b34a393fcb3392f18c9948596c Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Wed, 22 Jul 2026 18:12:18 -0700 Subject: [PATCH] fix(task): only point follow-up hints at transcripts that exist The aborted-task follow-up hint always references history://, including when no transcript can actually be served. Following that link then fails. Add hasResolvableTranscript beside sessionFilesFromDisk, mirroring the availability half of HistoryProtocolHandler's resolution semantics: a registered ref's live session, a retained session file verified on disk, or a disk-scanned .jsonl under a known artifacts dir (which still serves hard-aborted children whose refs were unregistered). Probing never throws; a stale path or unreadable artifacts subtree reads as unavailable instead of failing delivery of the settled result. Render the transcript clause from that check, independently of the resume affordance, so a still-resumable idle/parked agent keeps its hub resume hint and a disk-backed transcript keeps its link. Idle-completion hints are unchanged. Signed-off-by: Christian Stewart --- packages/coding-agent/CHANGELOG.md | 4 ++ .../src/internal-urls/registry-helpers.ts | 40 +++++++++++++++++++ packages/coding-agent/src/task/index.ts | 18 +++++---- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ca8ad644e..9360da5bc 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed aborted-task follow-up hints pointing at `history://` transcripts that cannot resolve: the hint now reports the transcript as unavailable when the agent ref retains no session file, while still-resumable agents keep their `hub` resume hint. + ## [17.0.8] - 2026-07-22 ### Added diff --git a/packages/coding-agent/src/internal-urls/registry-helpers.ts b/packages/coding-agent/src/internal-urls/registry-helpers.ts index ade597013..25f80ed73 100644 --- a/packages/coding-agent/src/internal-urls/registry-helpers.ts +++ b/packages/coding-agent/src/internal-urls/registry-helpers.ts @@ -90,3 +90,43 @@ export async function sessionFilesFromDisk(): Promise> { for (const dir of artifactsDirsFromRegistry()) await scan(dir, 0); return found; } + +/** + * Availability half of the `history://` resolution semantics: true when a + * transcript for `agentId` can be served from a registered ref's live session + * or retained session file, or from an on-disk `.jsonl` under a known + * artifacts dir. Hint surfaces use this so they only advertise + * `history://` links that `HistoryProtocolHandler` can actually + * resolve. A retained sessionFile path is verified on disk before it counts, + * and probing never throws: a stale path or unreadable artifacts subtree + * reads as unavailable instead of disturbing the caller's delivery path. + */ +export async function hasResolvableTranscript(agentId: string): Promise { + try { + const registry = AgentRegistry.global(); + const lower = agentId.toLowerCase(); + let ref = registry.get(agentId); + if (ref?.kind === "advisor") ref = undefined; + ref ??= registry.list().find(candidate => candidate.kind !== "advisor" && candidate.id.toLowerCase() === lower); + if (ref?.session) return true; + if (ref?.sessionFile && (await isReadableFile(ref.sessionFile))) return true; + const files = await sessionFilesFromDisk(); + for (const id of files.keys()) { + if (id.toLowerCase() === lower) return true; + } + } catch { + // Availability probing is advisory; any filesystem failure means the + // transcript cannot be promised, never that delivery should fail. + } + return false; +} + +/** True when `file` exists and is a regular file; never throws. */ +async function isReadableFile(file: string): Promise { + try { + const stat = await fs.stat(file); + return stat.isFile(); + } catch { + return false; + } +} diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index b1a546310..423229597 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -41,6 +41,7 @@ import { // Import review tools for side effects (registers subagent tool handlers) import "../tools/review"; import type { AsyncJobManager } from "../async"; +import { hasResolvableTranscript } from "../internal-urls/registry-helpers"; import { AgentRegistry } from "../registry/agent-registry"; import { type DiscoveryResult, discoverAgents } from "./discovery"; import { generateTaskName } from "./name-generator"; @@ -1043,14 +1044,17 @@ export class TaskTool implements AgentTool { + const buildFollowUpHint = async (aborted: boolean): Promise => { if (aborted) { - const status = AgentRegistry.global().get(agentId)?.status; - if (status === "idle" || status === "parked") { + const ref = AgentRegistry.global().get(agentId); + const transcript = (await hasResolvableTranscript(agentId)) + ? `transcript at history://${agentId}` + : "transcript unavailable"; + if (ref?.status === "idle" || ref?.status === "parked") { const followUp = ircEnabled ? "message it via `hub` to resume; " : ""; - return `\n\n${agentId} was stopped but is still resumable — ${followUp}transcript at history://${agentId}`; + return `\n\n${agentId} was stopped but is still resumable — ${followUp}${transcript}`; } - return `\n\n${agentId} was aborted — transcript at history://${agentId}`; + return `\n\n${agentId} was aborted — ${transcript}`; } const followUp = ircEnabled ? "message it via `hub` to follow up; " : ""; return `\n\n${agentId} is now idle — ${followUp}transcript at history://${agentId}`; @@ -1157,7 +1161,7 @@ export class TaskTool implements AgentTool); - const deliveryText = `${finalText}${buildFollowUpHint(singleResult?.aborted === true)}`; + const deliveryText = `${finalText}${await buildFollowUpHint(singleResult?.aborted === true)}`; if (resultFailed) { // Mark the job itself failed; the failed agent stays interrogable. throw new TaskJobError(deliveryText); @@ -1173,7 +1177,7 @@ export class TaskTool implements AgentTool); const message = error instanceof Error ? error.message : String(error); - const hint = AgentRegistry.global().get(agentId) ? buildFollowUpHint(false) : ""; + const hint = AgentRegistry.global().get(agentId) ? await buildFollowUpHint(false) : ""; throw new TaskJobError(`${message}${hint}`); } finally { releasePermit();