Merge PR #6328: fix(task): only point follow-up hints at transcripts that exist (@paralin)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user