Merge PR #6328: fix(task): only point follow-up hints at transcripts that exist (@paralin)

This commit is contained in:
can1357
2026-07-23 11:37:12 +02:00
3 changed files with 54 additions and 7 deletions
+3
View File
@@ -22,6 +22,9 @@
### Fixed
- Fixed the Hindsight API token setting being absent from the Memory tab, so authenticated servers can be configured entirely in the TUI.
### 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
@@ -90,3 +90,43 @@ export async function sessionFilesFromDisk(): Promise<Map<string, string>> {
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://<agentId>` 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<boolean> {
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<boolean> {
try {
const stat = await fs.stat(file);
return stat.isFile();
} catch {
return false;
}
}
+11 -7
View File
@@ -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";
@@ -1076,14 +1077,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
}): string {
const { manager, toolCallId, spawnParams, agentId, progress, ircEnabled, buildDetails, onUpdate, onSettled } =
options;
const buildFollowUpHint = (aborted: boolean): string => {
const buildFollowUpHint = async (aborted: boolean): Promise<string> => {
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}`;
@@ -1190,7 +1194,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
? `Background task ${agentId} failed.`
: `Background task ${agentId} complete.`;
await reportProgress(statusText, buildDetails() as unknown as Record<string, unknown>);
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);
@@ -1206,7 +1210,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
const statusText = `Background task ${agentId} failed.`;
await reportProgress(statusText, buildDetails() as unknown as Record<string, unknown>);
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();