pum-agent 0.2.11-beta.1 → 0.2.12-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pum-agent",
3
- "version": "0.2.11-beta.1",
3
+ "version": "0.2.12-beta.1",
4
4
  "description": "A compact terminal coding agent powered by pi and OpenTUI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/app.tsx CHANGED
@@ -1037,9 +1037,15 @@ export function App({
1037
1037
  else if (event.type === "main-pending-add") addPending(event.pending);
1038
1038
  else if (event.type === "main-pending-resolve") resolvePending(event.id);
1039
1039
  else if (event.type === "main-pending-drop") dropPending(event.id);
1040
+ else if (event.type === "news-changed") {
1041
+ const loaded = loadNewsItems(session.sessionFile);
1042
+ newsRef.current = loaded;
1043
+ setNews(loaded);
1044
+ setTx((value) => ({ ...value, lines: tagNewsLines(value.lines, loaded) }));
1045
+ }
1040
1046
  setAgentRevision((revision) => revision + 1);
1041
1047
  }),
1042
- [subagentManager],
1048
+ [subagentManager, session],
1043
1049
  );
1044
1050
 
1045
1051
  useEffect(() => {
@@ -1506,29 +1512,47 @@ export function App({
1506
1512
  const jumpFromNews = (target: "answer" | "prompt") => {
1507
1513
  const item = newsRef.current[newsCursorRef.current];
1508
1514
  if (!item) return;
1509
- const lines = txRef.current.lines;
1510
- const answerIndex = lines.findIndex((line) =>
1511
- line.kind === "text" && line.role === "assistant" && line.newsId === item.id,
1512
- );
1515
+ const requesterAgentId = item.completion?.requesterAgentId ?? null;
1516
+ const lines = requesterAgentId === null
1517
+ ? txRef.current.lines
1518
+ : subagentManager.getAgent(requesterAgentId)?.transcript.lines;
1519
+ if (!lines) return;
1520
+ const completionPromptIndex = item.completion
1521
+ ? lines.findIndex((line) =>
1522
+ line.kind === "agent-message" && line.messageId === item.completion?.messageId,
1523
+ )
1524
+ : -1;
1525
+ const answerIndex = item.completion
1526
+ ? lines.findIndex((line, index) =>
1527
+ index > completionPromptIndex
1528
+ && line.kind === "text"
1529
+ && line.role === "assistant"
1530
+ && line.text === item.text,
1531
+ )
1532
+ : lines.findIndex((line) =>
1533
+ line.kind === "text" && line.role === "assistant" && line.newsId === item.id,
1534
+ );
1513
1535
  let targetIndex = answerIndex;
1514
1536
  if (target === "prompt") {
1515
- const promptText = item.prompts?.find((prompt) => !prompt.steer)?.text
1516
- ?? item.prompts?.[0]?.text;
1517
- targetIndex = -1;
1518
- if (promptText) {
1519
- const end = answerIndex >= 0 ? answerIndex : lines.length;
1520
- for (let index = end - 1; index >= 0; index--) {
1521
- const line = lines[index];
1522
- if (line?.kind === "text" && line.role === "user" && line.text === promptText) {
1523
- targetIndex = index;
1524
- break;
1537
+ targetIndex = completionPromptIndex;
1538
+ if (!item.completion) {
1539
+ const promptText = item.prompts?.find((prompt) => !prompt.steer)?.text
1540
+ ?? item.prompts?.[0]?.text;
1541
+ if (promptText) {
1542
+ const end = answerIndex >= 0 ? answerIndex : lines.length;
1543
+ for (let index = end - 1; index >= 0; index--) {
1544
+ const line = lines[index];
1545
+ if (line?.kind === "text" && line.role === "user" && line.text === promptText) {
1546
+ targetIndex = index;
1547
+ break;
1548
+ }
1525
1549
  }
1526
1550
  }
1527
1551
  }
1528
1552
  }
1529
1553
  if (targetIndex < 0) return;
1530
1554
 
1531
- if (activeAgentIdRef.current !== null && !selectAgentView(null)) return;
1555
+ if (activeAgentIdRef.current !== requesterAgentId && !selectAgentView(requesterAgentId)) return;
1532
1556
  newsOpenRef.current = false;
1533
1557
  setNewsOpen(false);
1534
1558
  const scrollToTarget = () => {
@@ -1590,8 +1614,12 @@ export function App({
1590
1614
  const firstLine = item.text.split("\n")[0] ?? item.text;
1591
1615
  const preview = firstLine.length > 240 ? `${firstLine.slice(0, 240)} …` : firstLine;
1592
1616
  const quote = `> ${preview}\n\n`;
1593
- viewDrafts.current.set("main", quote);
1594
- if (activeAgentIdRef.current !== null) selectAgentView(null);
1617
+ const requesterAgentId = item.completion?.requesterAgentId ?? null;
1618
+ const targetAgentId = requesterAgentId && subagentManager.getAgent(requesterAgentId)
1619
+ ? requesterAgentId
1620
+ : null;
1621
+ viewDrafts.current.set(targetAgentId ?? "main", quote);
1622
+ if (activeAgentIdRef.current !== targetAgentId) selectAgentView(targetAgentId);
1595
1623
  else setEditorText(quote);
1596
1624
  queueMicrotask(() => {
1597
1625
  inputRef.current?.focus();
@@ -549,6 +549,12 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
549
549
  let pipeline = 0;
550
550
  let annotations = 0;
551
551
  let truncated = false;
552
+ let commandStart = true;
553
+ const caseContexts: Array<{
554
+ phase: "subject" | "pattern" | "body";
555
+ parenDepth: number;
556
+ braceDepth: number;
557
+ }> = [];
552
558
 
553
559
  const addAnnotation = <T>(list: T[], value: T) => {
554
560
  annotations++;
@@ -607,9 +613,37 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
607
613
  index++;
608
614
  continue;
609
615
  }
610
- if (char === "'" || char === "\"") { quote = char; index++; continue; }
611
- if (char === "(") { parenDepth++; index++; continue; }
612
- if (char === ")") { if (parenDepth === 0) errors.push(`unexpected ) at ${index}`); else parenDepth--; index++; continue; }
616
+ if (char === "'" || char === "\"") { quote = char; commandStart = false; index++; continue; }
617
+
618
+ const word = command.slice(index).match(/^[A-Za-z_][A-Za-z0-9_]*/)?.[0];
619
+ if (word) {
620
+ const next = command[index + word.length];
621
+ const reservedBoundary = next === undefined || /[\s;&|(){}<>]/.test(next);
622
+ const activeCase = caseContexts.at(-1);
623
+ const atCaseLevel = activeCase?.parenDepth === parenDepth && activeCase.braceDepth === braceDepth;
624
+ if (commandStart && word === "case" && reservedBoundary) {
625
+ caseContexts.push({ phase: "subject", parenDepth, braceDepth });
626
+ } else if (activeCase?.phase === "subject" && atCaseLevel && word === "in" && reservedBoundary) {
627
+ activeCase.phase = "pattern";
628
+ } else if (commandStart && activeCase && atCaseLevel && word === "esac" && reservedBoundary) {
629
+ caseContexts.pop();
630
+ }
631
+ commandStart = commandStart && reservedBoundary && new Set(["do", "then", "else", "elif"]).has(word);
632
+ index += word.length;
633
+ continue;
634
+ }
635
+
636
+ if (char === "(") { parenDepth++; commandStart = true; index++; continue; }
637
+ if (char === ")") {
638
+ const activeCase = caseContexts.at(-1);
639
+ if (activeCase?.phase === "pattern" && activeCase.parenDepth === parenDepth && activeCase.braceDepth === braceDepth) {
640
+ activeCase.phase = "body";
641
+ commandStart = true;
642
+ } else if (parenDepth === 0) errors.push(`unexpected ) at ${index}`);
643
+ else parenDepth--;
644
+ index++;
645
+ continue;
646
+ }
613
647
  if (char === "{") { braceDepth++; index++; continue; }
614
648
  if (char === "}") { if (braceDepth === 0) errors.push(`unexpected } at ${index}`); else braceDepth--; index++; continue; }
615
649
 
@@ -632,6 +666,15 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
632
666
  index = newline + 1;
633
667
  continue;
634
668
  }
669
+ const activeCase = caseContexts.at(-1);
670
+ const inCasePattern = activeCase?.phase === "pattern"
671
+ && activeCase.parenDepth === parenDepth
672
+ && activeCase.braceDepth === braceDepth;
673
+ if (char === "|" && inCasePattern && !command.startsWith("||", index) && !command.startsWith("|&", index)) {
674
+ commandStart = false;
675
+ index++;
676
+ continue;
677
+ }
635
678
  const rawOperator = [";;&", "&&", "||", "|&", ";;", ";&", ";", "|", "&", "\n"].find((item) => command.startsWith(item, index));
636
679
  if (rawOperator) {
637
680
  const normalized = rawOperator === "\n" ? "newline" : rawOperator as BashOperator["operator"];
@@ -643,9 +686,15 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
643
686
  stageStart = index + rawOperator.length;
644
687
  operatorBefore = normalized;
645
688
  }
689
+ if (activeCase?.phase === "body" && activeCase.parenDepth === parenDepth && activeCase.braceDepth === braceDepth
690
+ && (rawOperator === ";;" || rawOperator === ";&" || rawOperator === ";;&")) {
691
+ activeCase.phase = "pattern";
692
+ }
693
+ commandStart = true;
646
694
  index += rawOperator.length;
647
695
  continue;
648
696
  }
697
+ if (!/\s/.test(char)) commandStart = false;
649
698
  index++;
650
699
  }
651
700
  addStage(command.length);
@@ -655,6 +704,7 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
655
704
  if (backtickStart !== undefined) errors.push(`unterminated backtick substitution at ${backtickStart}`);
656
705
  if (parenDepth) errors.push("unbalanced parentheses");
657
706
  if (braceDepth) errors.push("unbalanced braces");
707
+ if (caseContexts.length) errors.push("unterminated case statement");
658
708
  if (truncated) errors.push("analysis annotation limit exceeded");
659
709
 
660
710
  const stages: BashStage[] = stageRanges.map((range, index) => {
@@ -69,6 +69,15 @@ export function NewsPopup({
69
69
  <box style={{ flexGrow: 1, width: 1 }} />
70
70
  <text content={formatAge(current.at)} fg={theme.dim} bg={theme.popupBg} wrapMode="none" />
71
71
  </box>
72
+ {current.completion ? (
73
+ <text
74
+ content={`${current.completion.agentName} → ${current.completion.requesterName} · finish_subagent`}
75
+ fg={theme.dim}
76
+ bg={theme.popupBg}
77
+ wrapMode="none"
78
+ style={{ height: 1, flexShrink: 0 }}
79
+ />
80
+ ) : null}
72
81
  <box style={{ height: 1, flexShrink: 0 }} />
73
82
  <scrollbox
74
83
  id="news-scrollbox"
package/src/news.ts CHANGED
@@ -8,6 +8,16 @@ export type NewsPrompt = {
8
8
  steer: boolean;
9
9
  };
10
10
 
11
+ export type NewsCompletion = {
12
+ settlementId: string;
13
+ messageId: string;
14
+ agentId: string;
15
+ agentName: string;
16
+ requesterAgentId: string | null;
17
+ requesterName: string;
18
+ summary: string;
19
+ };
20
+
11
21
  export type NewsItem = {
12
22
  /** Stable identifier used to tag the matching transcript line. */
13
23
  id: string;
@@ -21,6 +31,8 @@ export type NewsItem = {
21
31
  answered: boolean;
22
32
  /** User prompt and steers that produced this answer, oldest first. */
23
33
  prompts?: NewsPrompt[];
34
+ /** Managed completion identity for a finish_subagent-triggered answer. */
35
+ completion?: NewsCompletion;
24
36
  };
25
37
 
26
38
  /** The list never holds more than this many answers. */
@@ -48,10 +60,23 @@ function isNewsItem(value: unknown): value is NewsItem {
48
60
  Boolean(prompt) &&
49
61
  typeof (prompt as Record<string, unknown>).text === "string" &&
50
62
  typeof (prompt as Record<string, unknown>).steer === "boolean",
51
- )))
63
+ ))) &&
64
+ (item.completion === undefined || isNewsCompletion(item.completion))
52
65
  );
53
66
  }
54
67
 
68
+ function isNewsCompletion(value: unknown): value is NewsCompletion {
69
+ if (!value || typeof value !== "object") return false;
70
+ const completion = value as Record<string, unknown>;
71
+ return typeof completion.settlementId === "string"
72
+ && typeof completion.messageId === "string"
73
+ && typeof completion.agentId === "string"
74
+ && typeof completion.agentName === "string"
75
+ && (completion.requesterAgentId === null || typeof completion.requesterAgentId === "string")
76
+ && typeof completion.requesterName === "string"
77
+ && typeof completion.summary === "string";
78
+ }
79
+
55
80
  /** Load the persisted news list for a session. Never throws. */
56
81
  export function loadNewsItems(sessionFile: string | undefined): NewsItem[] {
57
82
  if (!sessionFile) return [];
@@ -81,16 +106,79 @@ export function saveNewsItems(
81
106
  }
82
107
  }
83
108
 
109
+ export type FinishNewsSettlement = {
110
+ id: string;
111
+ messageId: string;
112
+ agentId: string;
113
+ agentName: string;
114
+ parentAgentId: string | null;
115
+ requesterName: string;
116
+ status: "idle" | "completed" | "failed";
117
+ summary?: string;
118
+ content: string;
119
+ createdAt: number;
120
+ response?: string;
121
+ };
122
+
123
+ /** Project one completed finish settlement into the persisted News model. */
124
+ export function newsItemFromFinishSettlement(
125
+ settlement: FinishNewsSettlement,
126
+ ): NewsItem | undefined {
127
+ if (settlement.status !== "completed" || !settlement.response?.trim()) return undefined;
128
+ if (/^(?:ack(?:nowledged)?|got it|noted|ok(?:ay)?|thanks|thank you|understood)[.!\s]*$/i.test(
129
+ settlement.response.trim(),
130
+ )) return undefined;
131
+ return {
132
+ id: `subagent-finish:${settlement.messageId}`,
133
+ text: settlement.response.trim(),
134
+ at: settlement.createdAt,
135
+ read: false,
136
+ answered: false,
137
+ prompts: [{ text: settlement.content, steer: false }],
138
+ completion: {
139
+ settlementId: settlement.id,
140
+ messageId: settlement.messageId,
141
+ agentId: settlement.agentId,
142
+ agentName: settlement.agentName,
143
+ requesterAgentId: settlement.parentAgentId,
144
+ requesterName: settlement.requesterName,
145
+ summary: settlement.summary ?? "",
146
+ },
147
+ };
148
+ }
149
+
150
+ /** Upsert stable News identities while preserving local read and reply state. */
151
+ export function mergeNewsItems(
152
+ existing: readonly NewsItem[],
153
+ incoming: readonly NewsItem[],
154
+ ): NewsItem[] {
155
+ const byId = new Map(existing.map((item) => [item.id, item]));
156
+ for (const item of incoming) {
157
+ const current = byId.get(item.id);
158
+ byId.set(item.id, current
159
+ ? { ...item, read: current.read, answered: current.answered }
160
+ : item);
161
+ }
162
+ return [...byId.values()]
163
+ .sort((a, b) => b.at - a.at)
164
+ .slice(0, NEWS_CAPACITY);
165
+ }
166
+
84
167
  /**
85
168
  * Attach news identifiers to replayed assistant lines that exactly match a
86
169
  * stored answer. Each stored answer claims the first later unmatched line with
87
170
  * the same text, so resumed transcripts keep their circle/checkmark markers.
88
171
  */
89
- export function tagNewsLines<T>(lines: readonly T[], items: readonly NewsItem[]): T[] {
172
+ export function tagNewsLines<T>(
173
+ lines: readonly T[],
174
+ items: readonly NewsItem[],
175
+ requesterAgentId: string | null = null,
176
+ ): T[] {
90
177
  if (items.length === 0 || lines.length === 0) return lines.map((line) => line);
91
178
  const claimed = new Set<number>();
92
179
  const out = lines.map((line) => line);
93
180
  for (const item of items) {
181
+ if ((item.completion?.requesterAgentId ?? null) !== requesterAgentId) continue;
94
182
  const maybe = (value: T) =>
95
183
  value as unknown as { kind?: unknown; role?: unknown; text?: unknown; newsId?: unknown };
96
184
  const index = out.findIndex((line, i) =>
package/src/replay.ts CHANGED
@@ -120,6 +120,7 @@ export function replayEntries(
120
120
  sender: agentMessage.sender,
121
121
  recipient: agentMessage.recipient,
122
122
  text: agentMessage.text,
123
+ messageId: agentMessage.id,
123
124
  });
124
125
  }
125
126
  continue;
@@ -19,6 +19,13 @@ import {
19
19
  usageFromEntries,
20
20
  } from "../agent-usage";
21
21
  import { replayEntries } from "../replay";
22
+ import {
23
+ loadNewsItems,
24
+ mergeNewsItems,
25
+ newsItemFromFinishSettlement,
26
+ saveNewsItems,
27
+ tagNewsLines,
28
+ } from "../news";
22
29
  import { isRejectedToolResult, rejectedToolReason } from "../check-mode";
23
30
  import {
24
31
  observeSearchCalls,
@@ -185,6 +192,8 @@ type RuntimeRecord = {
185
192
  * it to idle and blocking its managed merge.
186
193
  */
187
194
  statusBeforeTurn?: SubagentStatus;
195
+ completionMessageIds?: Set<string>;
196
+ completionResponse?: string;
188
197
  };
189
198
 
190
199
  type IdleOpenReminderState = {
@@ -289,6 +298,8 @@ export class SubagentManager {
289
298
  private mainCwd = process.cwd();
290
299
  private parentSessionId = "detached";
291
300
  private mainRunning = false;
301
+ private readonly mainCompletionMessageIds = new Set<string>();
302
+ private mainCompletionResponse = "";
292
303
  private maxActiveSubagents: number;
293
304
  private worktreeQueue: Promise<void> = Promise.resolve();
294
305
  private readonly messageTimes = new Map<string, number[]>();
@@ -365,6 +376,8 @@ export class SubagentManager {
365
376
  this.mainCwd = cwd;
366
377
  this.parentSessionId = sessionId;
367
378
  this.mainRunning = false;
379
+ this.mainCompletionMessageIds.clear();
380
+ this.mainCompletionResponse = "";
368
381
  this.emit({
369
382
  type: "trigger-target",
370
383
  sessionId,
@@ -374,6 +387,7 @@ export class SubagentManager {
374
387
  });
375
388
 
376
389
  const restored = new Map<string, Omit<SubagentSnapshot, "transcript">>();
390
+ const restoredEntries = new Map<string, readonly any[]>();
377
391
  const restoredActivity = new Map<string, { activityGeneration: number; idleNotifiedGeneration: number }>();
378
392
  const restoredFinish = new Map<string, string>();
379
393
  for (const entry of sessionManager.getEntries()) {
@@ -421,6 +435,7 @@ export class SubagentManager {
421
435
  const childManager = (await import("@earendil-works/pi-coding-agent")).SessionManager.open(
422
436
  snapshot.sessionFile,
423
437
  );
438
+ restoredEntries.set(snapshot.id, childManager.getEntries());
424
439
  transcript = {
425
440
  lines: replayEntries(childManager.buildContextEntries(), snapshot.worktree.path, true),
426
441
  stream: null,
@@ -463,6 +478,8 @@ export class SubagentManager {
463
478
  this.persistActivity(this.records.get(snapshot.id)!);
464
479
  }
465
480
  }
481
+ this.restoreSettlementResponses(sessionManager.getEntries(), restoredEntries);
482
+ this.reconcileFinishNews();
466
483
  await this.retrySettlementsForParent(null);
467
484
  this.emit();
468
485
  }
@@ -733,7 +750,14 @@ export class SubagentManager {
733
750
  if (typeof id === "string") {
734
751
  this.resolvePending(record, id);
735
752
  this.acknowledgeSettlementMessage(id);
753
+ if (message.customType === AGENT_MESSAGE_CUSTOM_TYPE
754
+ && message.details?.kind === "completion") {
755
+ record.completionMessageIds ??= new Set<string>();
756
+ record.completionMessageIds.add(id);
757
+ }
736
758
  }
759
+ } else if (message?.role === "assistant" && record.completionMessageIds?.size) {
760
+ record.completionResponse = "";
737
761
  } else if (message?.role === "user") {
738
762
  const text = typeof message.content === "string"
739
763
  ? message.content
@@ -766,6 +790,9 @@ export class SubagentManager {
766
790
  const update = event.assistantMessageEvent;
767
791
  const kind = update.type === "text_delta" ? "assistant" : update.type === "thinking_delta" ? "thinking" : null;
768
792
  if (!kind) return;
793
+ if (kind === "assistant" && record.completionMessageIds?.size) {
794
+ record.completionResponse = (record.completionResponse ?? "") + update.delta;
795
+ }
769
796
  this.updateTranscript(record, (value) => {
770
797
  if (value.stream?.kind === kind) {
771
798
  return { ...value, stream: { kind, text: value.stream.text + update.delta } };
@@ -833,6 +860,13 @@ export class SubagentManager {
833
860
  case "agent_settled": {
834
861
  this.messageCacheController?.releaseRequester({ kind: "subagent", id: record.snapshot.id });
835
862
  this.updateTranscript(record, flushTranscript);
863
+ if (record.completionMessageIds?.size) {
864
+ for (const messageId of record.completionMessageIds) {
865
+ this.recordSettlementResponse(messageId, record.completionResponse ?? "");
866
+ }
867
+ record.completionMessageIds.clear();
868
+ record.completionResponse = "";
869
+ }
836
870
  if (record.session) {
837
871
  void this.triggerManager?.markTargetSettled(
838
872
  record.session.sessionId,
@@ -1112,6 +1146,13 @@ export class SubagentManager {
1112
1146
  });
1113
1147
  pi.on("agent_settled", () => {
1114
1148
  this.mainRunning = false;
1149
+ if (this.mainCompletionMessageIds.size > 0) {
1150
+ for (const messageId of this.mainCompletionMessageIds) {
1151
+ this.recordSettlementResponse(messageId, this.mainCompletionResponse);
1152
+ }
1153
+ this.mainCompletionMessageIds.clear();
1154
+ this.mainCompletionResponse = "";
1155
+ }
1115
1156
  this.messageCacheController?.releaseRequester({ kind: "main", id: this.parentSessionId });
1116
1157
  void this.triggerManager?.markTargetSettled(this.parentSessionId, null);
1117
1158
  this.emit({
@@ -1126,12 +1167,26 @@ export class SubagentManager {
1126
1167
  pi.on("message_start", (event) => {
1127
1168
  const message = event.message;
1128
1169
  this.acceptIdleOpenReminder(null, message);
1170
+ if (message.role === "assistant" && this.mainCompletionMessageIds.size > 0) {
1171
+ this.mainCompletionResponse = "";
1172
+ return;
1173
+ }
1129
1174
  if (message.role !== "custom"
1130
1175
  || ![AGENT_MESSAGE_CUSTOM_TYPE, TRIGGER_EVENT_CUSTOM_TYPE].includes(message.customType)) return;
1131
- const id = (message.details as AgentMessageData | undefined)?.id;
1176
+ const details = message.details as AgentMessageData | undefined;
1177
+ const id = details?.id;
1132
1178
  if (typeof id === "string") {
1133
1179
  this.acknowledgeSettlementMessage(id);
1134
1180
  this.emit({ type: "main-pending-resolve", id });
1181
+ if (message.customType === AGENT_MESSAGE_CUSTOM_TYPE && details?.kind === "completion") {
1182
+ this.mainCompletionMessageIds.add(id);
1183
+ }
1184
+ }
1185
+ });
1186
+ pi.on("message_update", (event) => {
1187
+ const update = event.assistantMessageEvent;
1188
+ if (this.mainCompletionMessageIds.size > 0 && update.type === "text_delta") {
1189
+ this.mainCompletionResponse += update.delta;
1135
1190
  }
1136
1191
  });
1137
1192
  pi.on("session_start", async (_event, ctx) => {
@@ -1719,6 +1774,7 @@ export class SubagentManager {
1719
1774
  sender: data.sender,
1720
1775
  recipient: data.recipient,
1721
1776
  text: data.text,
1777
+ messageId: data.id,
1722
1778
  };
1723
1779
  }
1724
1780
 
@@ -1893,6 +1949,89 @@ export class SubagentManager {
1893
1949
  if (!delivered) this.emit({ type: "main-pending-drop", id: pending.id });
1894
1950
  }
1895
1951
 
1952
+ private responseAfterSettlement(entries: readonly any[], messageId: string): string {
1953
+ let active = false;
1954
+ let response = "";
1955
+ for (const entry of entries) {
1956
+ const message = entry?.type === "message"
1957
+ ? entry.message
1958
+ : entry?.type === "custom_message"
1959
+ ? { ...entry, role: "custom" }
1960
+ : entry;
1961
+ if (message?.role === "custom") {
1962
+ if (message.customType === AGENT_MESSAGE_CUSTOM_TYPE && message.details?.id === messageId) {
1963
+ active = true;
1964
+ }
1965
+ continue;
1966
+ }
1967
+ if (!active) continue;
1968
+ if (message?.role === "user") break;
1969
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
1970
+ const text = message.content
1971
+ .filter((block: any) => block?.type === "text" && typeof block.text === "string")
1972
+ .map((block: any) => block.text)
1973
+ .join("")
1974
+ .trim();
1975
+ if (text) response = text;
1976
+ }
1977
+ return response;
1978
+ }
1979
+
1980
+ private restoreSettlementResponses(
1981
+ mainEntries: readonly any[],
1982
+ childEntries: ReadonlyMap<string, readonly any[]>,
1983
+ ): void {
1984
+ for (const settlement of this.settlements.values()) {
1985
+ if (settlement.status !== "completed" || settlement.response?.trim()) continue;
1986
+ const entries = settlement.parentAgentId === null
1987
+ ? mainEntries
1988
+ : childEntries.get(settlement.parentAgentId) ?? [];
1989
+ const response = this.responseAfterSettlement(entries, settlement.messageId);
1990
+ if (response) this.recordSettlementResponse(settlement.messageId, response, false);
1991
+ }
1992
+ }
1993
+
1994
+ private recordSettlementResponse(messageId: string, response: string, reconcile = true): void {
1995
+ const settlement = [...this.settlements.values()].find((item) => item.messageId === messageId);
1996
+ if (!settlement || settlement.status !== "completed" || !response.trim()) return;
1997
+ if (settlement.response === response.trim()) return;
1998
+ settlement.response = response.trim();
1999
+ settlement.respondedAt = Date.now();
2000
+ this.persist({
2001
+ event: "settlement",
2002
+ id: settlement.agentId,
2003
+ at: settlement.respondedAt,
2004
+ settlement: { ...settlement },
2005
+ });
2006
+ if (reconcile) this.reconcileFinishNews();
2007
+ }
2008
+
2009
+ private reconcileFinishNews(): void {
2010
+ const sessionFile = this.mainSessionManager?.getSessionFile?.();
2011
+ if (!sessionFile) return;
2012
+ const incoming = [...this.settlements.values()]
2013
+ .map((settlement) => newsItemFromFinishSettlement({
2014
+ ...settlement,
2015
+ agentName: settlement.agentName
2016
+ ?? this.records.get(settlement.agentId)?.snapshot.name
2017
+ ?? settlement.agentId,
2018
+ requesterName: settlement.requesterName
2019
+ ?? (settlement.parentAgentId
2020
+ ? this.records.get(settlement.parentAgentId)?.snapshot.name ?? settlement.parentAgentId
2021
+ : "main"),
2022
+ }))
2023
+ .filter((item): item is NonNullable<typeof item> => item !== undefined);
2024
+ const next = mergeNewsItems(loadNewsItems(sessionFile), incoming);
2025
+ saveNewsItems(sessionFile, next);
2026
+ for (const record of this.records.values()) {
2027
+ record.snapshot.transcript = {
2028
+ ...record.snapshot.transcript,
2029
+ lines: tagNewsLines(record.snapshot.transcript.lines, next, record.snapshot.id),
2030
+ };
2031
+ }
2032
+ this.emit({ type: "news-changed" });
2033
+ }
2034
+
1896
2035
  private settlementId(record: RuntimeRecord, status: "idle" | "completed" | "failed"): string {
1897
2036
  return `${record.snapshot.id}:${record.activityGeneration}:${status}`;
1898
2037
  }
@@ -1912,11 +2051,16 @@ export class SubagentManager {
1912
2051
  `worktree: ${record.snapshot.worktree.path}`,
1913
2052
  summary ? `summary: ${summary}` : "",
1914
2053
  ].filter(Boolean).join("\n");
2054
+ const requester = record.snapshot.parentAgentId
2055
+ ? this.records.get(record.snapshot.parentAgentId)?.snapshot.name ?? record.snapshot.parentAgentId
2056
+ : "main";
1915
2057
  settlement = {
1916
2058
  id,
1917
2059
  messageId: `settlement-${id}`,
1918
2060
  agentId: record.snapshot.id,
2061
+ agentName: record.snapshot.name,
1919
2062
  parentAgentId: record.snapshot.parentAgentId,
2063
+ requesterName: requester,
1920
2064
  status,
1921
2065
  summary,
1922
2066
  activityGeneration: record.activityGeneration,
@@ -60,9 +60,13 @@ export type SubagentSettlement = {
60
60
  parentAgentId: string | null;
61
61
  status: "idle" | "completed" | "failed";
62
62
  summary?: string;
63
+ agentName?: string;
64
+ requesterName?: string;
63
65
  activityGeneration: number;
64
66
  content: string;
65
67
  createdAt: number;
68
+ response?: string;
69
+ respondedAt?: number;
66
70
  acknowledgedAt?: number;
67
71
  };
68
72
 
@@ -96,6 +100,7 @@ export type TriggerEventData = ExternalTriggerEventData & {
96
100
 
97
101
  export type SubagentManagerEvent =
98
102
  | { type: "changed" }
103
+ | { type: "news-changed" }
99
104
  | { type: "main-line"; line: Line }
100
105
  | { type: "main-pending-add"; pending: PendingLine }
101
106
  | { type: "main-pending-resolve"; id: string }
@@ -20,7 +20,7 @@ export type Role = "user" | "assistant" | "thinking" | "system" | "error";
20
20
  export type Line =
21
21
  | { kind: "text"; role: Role; text: string; newsId?: string }
22
22
  | { kind: "tool"; call: ToolCall }
23
- | { kind: "agent-message"; sender: string; recipient: string; text: string };
23
+ | { kind: "agent-message"; sender: string; recipient: string; text: string; messageId?: string };
24
24
 
25
25
  export type PendingLine = {
26
26
  id: string;