Skip to content

Commit 861d65f

Browse files
jom-sqampcode-com
andcommitted
fix(server): extract handoff children from tool_use blocks in messages
The listThreads API returns empty relationships, and newer Amp versions store handoff parent-child links as handoff tool_use/tool_result blocks in message content rather than a top-level relationships[] array. enrichRelationships now checks both sources: 1. relationships[] array (older format) 2. handoff tool blocks in messages - finds tool_use with name='handoff', then extracts child thread IDs from the corresponding tool_result Also sets the reverse link: when a parent's messages contain handoff results, the child thread gets its handoffParentId set automatically. Co-authored-by: Amp <amp@ampcode.com>
1 parent 9c9953b commit 861d65f

File tree

1 file changed

+65
-5
lines changed

1 file changed

+65
-5
lines changed

server/lib/threadProvider.ts

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
import { readFile, readdir, access } from 'fs/promises';
1010
import { join } from 'path';
1111
import type { Thread, ThreadVisibility } from '../../shared/types.js';
12-
import { THREADS_DIR, isHandoffRelationship, type ThreadFile } from './threadTypes.js';
12+
import {
13+
THREADS_DIR,
14+
isHandoffRelationship,
15+
isToolUseContent,
16+
type ThreadFile,
17+
type ThreadMessage,
18+
} from './threadTypes.js';
1319
import { listThreads, type AmpThreadSummary } from './amp-api.js';
1420
import { formatRelativeTime, parseFileUri, runAmp } from './utils.js';
1521

@@ -56,8 +62,9 @@ export async function listAllThreads(): Promise<Thread[]> {
5662

5763
/**
5864
* Scan local thread files for handoff relationships and merge them into
59-
* the thread list. This is fast (~5ms for 25 local files) and only reads
60-
* the `relationships` field from each file.
65+
* the thread list. Checks two sources:
66+
* 1. `relationships[]` array (older Amp format)
67+
* 2. `handoff` tool_use/tool_result blocks in messages (newer Amp format)
6168
*/
6269
async function enrichRelationships(threads: Thread[]): Promise<void> {
6370
const threadMap = new Map(threads.map((t) => [t.id, t]));
@@ -80,9 +87,9 @@ async function enrichRelationships(threads: Thread[]): Promise<void> {
8087
try {
8188
const content = await readFile(join(THREADS_DIR, file), 'utf-8');
8289
const data = JSON.parse(content) as ThreadFile;
83-
const relationships = data.relationships || [];
8490

85-
for (const rel of relationships) {
91+
// Source 1: explicit relationships array (older format)
92+
for (const rel of data.relationships || []) {
8693
if (isHandoffRelationship(rel)) {
8794
if (rel.role === 'child') {
8895
thread.handoffParentId = rel.threadID;
@@ -93,6 +100,20 @@ async function enrichRelationships(threads: Thread[]): Promise<void> {
93100
}
94101
}
95102

103+
// Source 2: handoff tool blocks in messages (newer format)
104+
const childIds = extractHandoffChildIds(data.messages || [], threadId);
105+
if (childIds.length > 0) {
106+
thread.handoffChildIds = thread.handoffChildIds || [];
107+
thread.handoffChildIds.push(...childIds);
108+
// Set the reverse link: mark each child's parent
109+
for (const childId of childIds) {
110+
const child = threadMap.get(childId);
111+
if (child && !child.handoffParentId) {
112+
child.handoffParentId = threadId;
113+
}
114+
}
115+
}
116+
96117
if (thread.handoffChildIds?.length) {
97118
thread.handoffChildIds = [...new Set(thread.handoffChildIds)];
98119
}
@@ -103,6 +124,45 @@ async function enrichRelationships(threads: Thread[]): Promise<void> {
103124
);
104125
}
105126

127+
const THREAD_ID_RE = /T-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
128+
129+
/**
130+
* Extract child thread IDs from handoff tool_result blocks.
131+
* When a thread uses the `handoff` tool, the tool_result contains the new thread ID.
132+
*/
133+
function extractHandoffChildIds(messages: ThreadMessage[], selfId: string): string[] {
134+
const handoffToolUseIds = new Set<string>();
135+
const childIds: string[] = [];
136+
137+
for (const msg of messages) {
138+
if (!Array.isArray(msg.content)) continue;
139+
140+
for (const block of msg.content) {
141+
// Collect tool_use IDs for handoff tools
142+
if (isToolUseContent(block) && block.name === 'handoff') {
143+
const id = (block as unknown as Record<string, unknown>).id as string | undefined;
144+
if (id) handoffToolUseIds.add(id);
145+
}
146+
147+
// Check tool_result blocks that reference a handoff tool_use
148+
const b = block as Record<string, unknown>;
149+
if (b.type === 'tool_result' && typeof b.toolUseID === 'string') {
150+
if (handoffToolUseIds.has(b.toolUseID)) {
151+
// Extract thread IDs from the result content
152+
const resultText =
153+
typeof b.content === 'string' ? b.content : JSON.stringify(b.content ?? b.run ?? '');
154+
const matches = resultText.match(THREAD_ID_RE) || [];
155+
for (const tid of matches) {
156+
if (tid !== selfId) childIds.push(tid);
157+
}
158+
}
159+
}
160+
}
161+
}
162+
163+
return [...new Set(childIds)];
164+
}
165+
106166
function toThread(s: AmpThreadSummary): Thread {
107167
const tree = s.env?.initial?.trees?.[0];
108168
const repoUrl = tree?.repository?.url;

0 commit comments

Comments
 (0)