pi-sessions 0.8.0 → 0.9.0

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.
Files changed (104) hide show
  1. package/README.md +70 -37
  2. package/extensions/pi-sessions.ts +125 -0
  3. package/extensions/session-ask/agent.ts +34 -14
  4. package/extensions/{session-ask.ts → session-ask/install.ts} +73 -90
  5. package/extensions/session-ask/navigate.ts +3 -1
  6. package/extensions/session-ask/presenter.ts +23 -0
  7. package/extensions/session-ask/renderer.ts +63 -0
  8. package/extensions/session-ask/tool-contract.ts +27 -0
  9. package/extensions/session-ask/view-model.ts +18 -0
  10. package/extensions/session-auto-title/generate.ts +18 -18
  11. package/extensions/{session-auto-title.ts → session-auto-title/install.ts} +60 -53
  12. package/extensions/session-auto-title/model.ts +25 -17
  13. package/extensions/session-auto-title/retitle.ts +17 -18
  14. package/extensions/session-auto-title/wizard.ts +12 -11
  15. package/extensions/session-handoff/board-view-model.ts +181 -0
  16. package/extensions/session-handoff/board.ts +663 -0
  17. package/extensions/session-handoff/bootstrap.ts +174 -0
  18. package/extensions/session-handoff/extract.ts +101 -122
  19. package/extensions/session-handoff/install.ts +225 -0
  20. package/extensions/session-handoff/kickoff.ts +92 -0
  21. package/extensions/session-handoff/launch/backend.ts +16 -0
  22. package/extensions/session-handoff/launch/deferred.ts +20 -0
  23. package/extensions/session-handoff/launch/ghostty.ts +99 -0
  24. package/extensions/session-handoff/launch/resolution.ts +57 -0
  25. package/extensions/session-handoff/launch/shell.ts +7 -0
  26. package/extensions/session-handoff/launch/tmux.ts +31 -0
  27. package/extensions/session-handoff/launch-options.ts +61 -0
  28. package/extensions/session-handoff/launch-target.ts +91 -0
  29. package/extensions/session-handoff/metadata.ts +152 -63
  30. package/extensions/session-handoff/model.ts +58 -0
  31. package/extensions/session-handoff/query.ts +0 -1
  32. package/extensions/session-handoff/receipt.ts +96 -0
  33. package/extensions/session-handoff/review.ts +2 -28
  34. package/extensions/session-handoff/spawn.ts +76 -181
  35. package/extensions/session-handoff/strong-modal.ts +13 -0
  36. package/extensions/session-handoff/tool-contract.ts +12 -0
  37. package/extensions/session-handoff/tool-presenter.ts +35 -0
  38. package/extensions/session-handoff/tool-renderer.ts +66 -0
  39. package/extensions/session-handoff/tool-schema.ts +54 -0
  40. package/extensions/session-handoff/tool-view-model.ts +49 -0
  41. package/extensions/session-handoff/tool.ts +206 -0
  42. package/extensions/session-handoff/ui.ts +66 -0
  43. package/extensions/session-hooks/install.ts +75 -0
  44. package/extensions/{session-index.ts → session-index/install.ts} +9 -7
  45. package/extensions/session-messaging/broker/process.ts +21 -28
  46. package/extensions/session-messaging/install.ts +78 -0
  47. package/extensions/session-messaging/pi/cancel-session-presenter.ts +64 -0
  48. package/extensions/session-messaging/pi/incoming-message-presenter.ts +32 -0
  49. package/extensions/session-messaging/pi/incoming-message-view-model.ts +20 -0
  50. package/extensions/session-messaging/pi/incoming-runtime.ts +27 -23
  51. package/extensions/session-messaging/pi/message-contracts.ts +32 -0
  52. package/extensions/session-messaging/pi/renderer.ts +19 -12
  53. package/extensions/session-messaging/pi/send-message-presenter.ts +39 -0
  54. package/extensions/session-messaging/pi/send-message-view-model.ts +54 -0
  55. package/extensions/session-messaging/pi/service.ts +262 -81
  56. package/extensions/session-messaging/pi/tools.ts +184 -40
  57. package/extensions/session-search/extract.ts +60 -14
  58. package/extensions/session-search/hooks.ts +10 -13
  59. package/extensions/session-search/install.ts +373 -0
  60. package/extensions/session-search/renderer.ts +195 -0
  61. package/extensions/session-search/tool-contract.ts +51 -0
  62. package/extensions/shared/composition.ts +20 -0
  63. package/extensions/shared/errors.ts +3 -0
  64. package/extensions/shared/model-resolution.ts +73 -0
  65. package/extensions/shared/model-runtime.ts +33 -0
  66. package/extensions/shared/model.ts +7 -10
  67. package/extensions/shared/rendering/collapsible-text.ts +89 -0
  68. package/extensions/shared/rendering/expandable-content-layout.ts +84 -0
  69. package/extensions/shared/rendering/theme.ts +5 -0
  70. package/extensions/shared/session-broker/client.ts +30 -29
  71. package/extensions/shared/session-broker/protocol.ts +128 -10
  72. package/extensions/shared/session-index/common.ts +5 -5
  73. package/extensions/shared/session-index/lineage.ts +0 -3
  74. package/extensions/shared/session-index/schema.ts +0 -1
  75. package/extensions/shared/session-index/scoring.ts +1 -2
  76. package/extensions/shared/session-index/search.ts +11 -4
  77. package/extensions/shared/session-index/store.ts +4 -9
  78. package/extensions/shared/settings.ts +76 -38
  79. package/extensions/shared/text.ts +5 -0
  80. package/extensions/shared/thinking-levels.ts +22 -0
  81. package/extensions/shared/tmux.ts +205 -0
  82. package/extensions/subagents/cancel.ts +111 -0
  83. package/extensions/subagents/classify.ts +52 -0
  84. package/extensions/subagents/install.ts +344 -0
  85. package/extensions/subagents/launch-target.ts +75 -0
  86. package/extensions/subagents/ledger.ts +217 -0
  87. package/extensions/subagents/reconcile.ts +386 -0
  88. package/extensions/subagents/report-message-presenter.ts +36 -0
  89. package/extensions/subagents/report-message-renderer.ts +21 -0
  90. package/extensions/subagents/report-message-view-model.ts +29 -0
  91. package/extensions/subagents/report.ts +159 -0
  92. package/extensions/subagents/roster.ts +262 -0
  93. package/extensions/subagents/wake.ts +173 -0
  94. package/images/handoff-board-subagents.png +0 -0
  95. package/images/handoff-board-user-sessions.png +0 -0
  96. package/images/handoff-subagent-report.png +0 -0
  97. package/images/session-handoff-tool.png +0 -0
  98. package/package.json +10 -16
  99. package/extensions/session-handoff.ts +0 -741
  100. package/extensions/session-hooks.ts +0 -75
  101. package/extensions/session-messaging/pi/message-view.ts +0 -131
  102. package/extensions/session-messaging.ts +0 -30
  103. package/extensions/session-search.ts +0 -419
  104. package/extensions/shared/session-broker/active.ts +0 -26
@@ -1,51 +1,119 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
- import { SEND_MESSAGE_PARAMS, type SendMessageParams } from "./message-contracts.ts";
4
- import { formatSendMessageCall } from "./message-view.ts";
5
- import type { SessionMessagingService } from "./service.ts";
6
-
7
- export function registerSessionMessagingTools(
8
- pi: ExtensionAPI,
9
- service: SessionMessagingService,
10
- ): void {
11
- pi.registerTool({
3
+ import { formatError } from "../../shared/errors.ts";
4
+ import { ExpandableContentLayout } from "../../shared/rendering/expandable-content-layout.ts";
5
+ import { safeParseTypeBoxValue } from "../../shared/typebox.ts";
6
+ import {
7
+ buildCancelSessionModelText,
8
+ buildCancelSessionUserError,
9
+ buildCancelSessionUserText,
10
+ buildDeadSessionError,
11
+ buildUnknownCancellationError,
12
+ isConfirmedManagedCancellation,
13
+ } from "./cancel-session-presenter.ts";
14
+ import {
15
+ CANCEL_SESSION_PARAMS,
16
+ CANCEL_SESSION_TOOL_DETAILS_SCHEMA,
17
+ type CancelSessionParams,
18
+ type CancelSessionToolDetails,
19
+ SEND_MESSAGE_PARAMS,
20
+ SEND_MESSAGE_TOOL_DETAILS_SCHEMA,
21
+ type SendMessageParams,
22
+ type SendMessageToolDetails,
23
+ } from "./message-contracts.ts";
24
+ import { buildSendMessagePresentation } from "./send-message-presenter.ts";
25
+ import { buildDeliveredMessageView, buildSendingMessageView } from "./send-message-view-model.ts";
26
+ import type { CancelSessionResult, SendMessageRequest, SendMessageResult } from "./service.ts";
27
+
28
+ interface SendMessageService {
29
+ sendMessage(request: SendMessageRequest): Promise<SendMessageResult>;
30
+ }
31
+
32
+ interface ManagedCancelSessionResult {
33
+ kind: "managed";
34
+ accepted: true;
35
+ target: {
36
+ sessionId: string;
37
+ sessionName?: string;
38
+ cwd?: string;
39
+ };
40
+ state: string;
41
+ cancelId?: string;
42
+ relation?: string;
43
+ }
44
+
45
+ interface CancelSessionService {
46
+ cancelSession(sessionId: string): Promise<CancelSessionResult | ManagedCancelSessionResult>;
47
+ }
48
+
49
+ export interface SessionSendMessageToolOptions {
50
+ wakeCapable: boolean;
51
+ getCachedRelationTo(sessionId: string | undefined): string | undefined;
52
+ }
53
+
54
+ interface SendMessageRendererState {
55
+ callComponent?: ExpandableContentLayout | undefined;
56
+ }
57
+
58
+ export function createSessionSendMessageTool(
59
+ service: SendMessageService,
60
+ options: SessionSendMessageToolOptions,
61
+ ): ToolDefinition {
62
+ const description = options.wakeCapable
63
+ ? "Send a message to another live pi session or a subagent."
64
+ : "Send a message to another live pi session.";
65
+ const promptSnippet = options.wakeCapable
66
+ ? "Send a message to another pi session or a subagent"
67
+ : "Send a message to another live pi session";
68
+ const promptGuidelines = options.wakeCapable
69
+ ? [
70
+ "Use session_search with live: true to discover live sessions.",
71
+ "It is always possible to session_send_message to an owned subagent",
72
+ ]
73
+ : [
74
+ "Before session_send_message, use session_search with live: true to list all live sessions and find the target.",
75
+ ];
76
+
77
+ return defineTool({
12
78
  name: "session_send_message",
13
79
  label: "Send Message to Session",
14
- description: "Send a message to another live pi session",
15
- promptSnippet: "Send a message to another live pi session",
16
- promptGuidelines: [
17
- "Before session_send_message, use session_search with live: true (no other filters) to list all live sessions and find the target.",
18
- ],
80
+ description,
81
+ promptSnippet,
82
+ promptGuidelines,
19
83
  parameters: SEND_MESSAGE_PARAMS,
20
84
  renderCall(args, theme, context) {
21
- const component = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
22
- component.setText(
23
- formatSendMessageCall(
24
- args,
25
- context.expanded,
26
- "sending",
85
+ const state = context.state as SendMessageRendererState;
86
+ const component = state.callComponent ?? new ExpandableContentLayout(theme);
87
+ state.callComponent = component;
88
+ component.update(
89
+ buildSendMessagePresentation(
90
+ buildSendingMessageView(args, options.getCachedRelationTo(args?.session)),
27
91
  theme,
28
- service.getCachedRelationTo(args?.session),
29
92
  ),
93
+ context.expanded,
30
94
  );
31
95
  return component;
32
96
  },
33
- renderResult(result, _options, theme, context) {
34
- if (!context.isError) {
35
- return new Text("", 0, 0);
97
+ renderResult(result, options, theme, context) {
98
+ if (context.isError) {
99
+ const output = getFirstText(result);
100
+ return new Text(output ? `\n${theme.fg("error", output)}` : "", 0, 0);
36
101
  }
37
102
 
38
- const output = getFirstText(result);
39
- return new Text(output ? `\n${theme.fg("error", output)}` : "", 0, 0);
103
+ const details = safeParseTypeBoxValue(SEND_MESSAGE_TOOL_DETAILS_SCHEMA, result.details);
104
+ const state = context.state as SendMessageRendererState;
105
+ if (!details || !state.callComponent) {
106
+ return new Text(getFirstText(result), 0, 0);
107
+ }
108
+
109
+ state.callComponent.update(
110
+ buildSendMessagePresentation(buildDeliveredMessageView(context.args, details), theme),
111
+ options.expanded,
112
+ );
113
+ return new Text("", 0, 0);
40
114
  },
41
115
  async execute(toolCallId, params: SendMessageParams, _signal, _onUpdate, _ctx) {
42
- const target = params.session.trim();
43
- if (!target) {
44
- throw new Error("session_send_message requires a target session id.");
45
- }
46
- if (target.startsWith("@session:")) {
47
- throw new Error("Use the bare session UUID, not an @session token.");
48
- }
116
+ const target = parseSessionTarget(params.session, "session_send_message");
49
117
 
50
118
  const body = params.message.trim();
51
119
  if (!body) {
@@ -73,8 +141,9 @@ export function registerSessionMessagingTools(
73
141
  details: {
74
142
  delivered: true,
75
143
  messageId: result.messageId,
76
- targetSessionId: target,
77
- },
144
+ target: result.target,
145
+ ...(result.relation === undefined ? {} : { relation: result.relation }),
146
+ } satisfies SendMessageToolDetails,
78
147
  };
79
148
  } catch (error) {
80
149
  throw new Error(formatError(error));
@@ -83,10 +152,85 @@ export function registerSessionMessagingTools(
83
152
  });
84
153
  }
85
154
 
86
- function getFirstText(result: { content: Array<{ type: string; text?: string }> }): string {
87
- return result.content.find((item) => item.type === "text")?.text ?? "";
155
+ export function createSessionCancelTool(service: CancelSessionService): ToolDefinition {
156
+ return defineTool<typeof CANCEL_SESSION_PARAMS, CancelSessionToolDetails>({
157
+ name: "session_cancel",
158
+ label: "Cancel a running session",
159
+ description: "Cancel another running pi session",
160
+ promptSnippet: "Cancel another running pi session",
161
+ promptGuidelines: ["Only cancel a user session when the user directs it."],
162
+ parameters: CANCEL_SESSION_PARAMS,
163
+ renderResult(result, options, theme, context) {
164
+ const output = getFirstText(result);
165
+ if (context.isError) {
166
+ const error = buildCancelSessionUserError(output, options.expanded);
167
+ return new Text(error ? theme.fg("error", error) : "", 0, 0);
168
+ }
169
+
170
+ const details = safeParseTypeBoxValue(CANCEL_SESSION_TOOL_DETAILS_SCHEMA, result.details);
171
+ if (!details) {
172
+ return new Text(output, 0, 0);
173
+ }
174
+
175
+ let text = theme.fg("toolOutput", buildCancelSessionUserText(details));
176
+ if (options.expanded && details.target.sessionName) {
177
+ text += `\n${theme.fg("dim", `${theme.bold("session")} ${details.target.sessionId}`)}`;
178
+ }
179
+ return new Text(text, 0, 0);
180
+ },
181
+ async execute(_toolCallId, params: CancelSessionParams) {
182
+ const target = parseSessionTarget(params.session, "session_cancel");
183
+ const result = await service.cancelSession(target);
184
+ if (result.kind === "managed") {
185
+ const details: CancelSessionToolDetails = {
186
+ kind: "managed",
187
+ accepted: true,
188
+ target: result.target,
189
+ state: result.state,
190
+ ...(result.cancelId === undefined ? {} : { cancelId: result.cancelId }),
191
+ ...(result.relation === undefined ? {} : { relation: result.relation }),
192
+ };
193
+ if (!isConfirmedManagedCancellation(result.state)) {
194
+ throw new Error(buildUnknownCancellationError(result.target));
195
+ }
196
+ return {
197
+ content: [{ type: "text" as const, text: buildCancelSessionModelText(details) }],
198
+ details,
199
+ };
200
+ }
201
+ if (!result.delivered) {
202
+ if (result.reason === "no_session") {
203
+ throw new Error(buildDeadSessionError(target));
204
+ }
205
+ throw new Error(result.error ?? "Cancellation was not delivered.");
206
+ }
207
+
208
+ const details: CancelSessionToolDetails = {
209
+ kind: "transport",
210
+ delivered: true,
211
+ cancelId: result.cancelId,
212
+ target: result.target,
213
+ ...(result.relation === undefined ? {} : { relation: result.relation }),
214
+ };
215
+ return {
216
+ content: [{ type: "text" as const, text: buildCancelSessionModelText(details) }],
217
+ details,
218
+ };
219
+ },
220
+ });
221
+ }
222
+
223
+ function parseSessionTarget(raw: string, toolName: string): string {
224
+ const target = raw.trim();
225
+ if (!target) {
226
+ throw new Error(`${toolName} requires a target session id.`);
227
+ }
228
+ if (target.startsWith("@session:")) {
229
+ throw new Error("Use the bare session UUID, not an @session token.");
230
+ }
231
+ return target;
88
232
  }
89
233
 
90
- function formatError(error: unknown): string {
91
- return error instanceof Error && error.message.trim() ? error.message : String(error);
234
+ function getFirstText(result: { content: Array<{ type: string; text?: string }> }): string {
235
+ return result.content.find((item) => item.type === "text")?.text ?? "";
92
236
  }
@@ -26,6 +26,7 @@ import {
26
26
  import type { SessionOrigin } from "../shared/session-index/index.ts";
27
27
  import { contentToText, isRecord } from "../shared/text.ts";
28
28
  import { safeParseTypeBoxValue } from "../shared/typebox.ts";
29
+ import { SUBAGENT_LAUNCHED_CUSTOM_TYPE, SUBAGENT_LAUNCHED_SCHEMA } from "../subagents/ledger.ts";
29
30
  import {
30
31
  deriveSessionRepoRoots,
31
32
  type FileTouchOp,
@@ -78,7 +79,6 @@ export interface ExtractedSessionRecord {
78
79
  parentSessionId?: string | undefined;
79
80
  sessionOrigin?: SessionOrigin | undefined;
80
81
  handoffGoal?: string | undefined;
81
- handoffNextTask?: string | undefined;
82
82
  indexedFileSize: number;
83
83
  indexedFileMtimeMs: number;
84
84
  indexedFileAnchor: string;
@@ -181,6 +181,9 @@ export function extractSessionRecord(sessionPath: string): ExtractedSessionRecor
181
181
  appendDurableHandoffMetadataChunks(chunks, scan.handoffMetadata);
182
182
 
183
183
  const parentSessionPath = normalizeParentSessionPath(parsed.header.parentSession);
184
+ const parentSession = parentSessionPath
185
+ ? inspectParentSession(parentSessionPath, parsed.header.id, sessionPath)
186
+ : undefined;
184
187
 
185
188
  return {
186
189
  sessionId: parsed.header.id,
@@ -194,10 +197,13 @@ export function extractSessionRecord(sessionPath: string): ExtractedSessionRecor
194
197
  messageCount: scan.messageCount,
195
198
  entryCount: scan.entryCount,
196
199
  parentSessionPath,
197
- parentSessionId: parentSessionPath ? readSessionIdFromPath(parentSessionPath) : undefined,
198
- sessionOrigin: inferSessionOrigin(parentSessionPath, scan.handoffMetadata?.metadata),
200
+ parentSessionId: parentSession?.sessionId,
201
+ sessionOrigin: deriveSessionOrigin(
202
+ parentSessionPath,
203
+ parentSession?.launchedSubagent ?? false,
204
+ scan.handoffMetadata?.metadata,
205
+ ),
199
206
  handoffGoal: scan.handoffMetadata?.metadata.goal,
200
- handoffNextTask: scan.handoffMetadata?.metadata.nextTask,
201
207
  indexedFileSize: slice.consumedBytes,
202
208
  indexedFileMtimeMs,
203
209
  indexedFileAnchor: buildFileAnchor(buffer, slice.consumedBytes),
@@ -429,7 +435,7 @@ export function parseSessionContent(raw: string): ParsedSessionFile | undefined
429
435
  }
430
436
 
431
437
  const header = fileEntries[0];
432
- if (!header || header.type !== "session") {
438
+ if (header?.type !== "session") {
433
439
  return undefined;
434
440
  }
435
441
 
@@ -458,23 +464,66 @@ function normalizeParentSessionPath(parentSession: string | undefined): string |
458
464
  return trimmed.length > 0 ? trimmed : undefined;
459
465
  }
460
466
 
461
- function readSessionIdFromPath(sessionPath: string): string | undefined {
467
+ interface ParentSessionInspection {
468
+ sessionId: string;
469
+ launchedSubagent: boolean;
470
+ }
471
+
472
+ export function inferSessionOrigin(
473
+ sessionId: string,
474
+ sessionPath: string,
475
+ parentSessionPath: string | undefined,
476
+ handoffMetadata: HandoffSessionMetadata | undefined,
477
+ ): SessionOrigin | undefined {
478
+ const parentSession = parentSessionPath
479
+ ? inspectParentSession(parentSessionPath, sessionId, sessionPath)
480
+ : undefined;
481
+ return deriveSessionOrigin(
482
+ parentSessionPath,
483
+ parentSession?.launchedSubagent ?? false,
484
+ handoffMetadata,
485
+ );
486
+ }
487
+
488
+ function inspectParentSession(
489
+ parentSessionPath: string,
490
+ childSessionId: string,
491
+ childSessionPath: string,
492
+ ): ParentSessionInspection | undefined {
462
493
  try {
463
- const parsed = parseSessionFile(sessionPath);
464
- return parsed?.header.id;
494
+ const parent = parseSessionFile(parentSessionPath);
495
+ if (!parent) {
496
+ return undefined;
497
+ }
498
+
499
+ const launchedSubagent = parent.entries.some((entry) => {
500
+ if (entry.type !== "custom" || entry.customType !== SUBAGENT_LAUNCHED_CUSTOM_TYPE) {
501
+ return false;
502
+ }
503
+ const launch = safeParseTypeBoxValue(SUBAGENT_LAUNCHED_SCHEMA, entry.data);
504
+ return (
505
+ launch?.writerSessionId === parent.header.id &&
506
+ launch.childSessionId === childSessionId &&
507
+ launch.childSessionFile === childSessionPath
508
+ );
509
+ });
510
+ return { sessionId: parent.header.id, launchedSubagent };
465
511
  } catch {
466
512
  return undefined;
467
513
  }
468
514
  }
469
515
 
470
- function inferSessionOrigin(
516
+ function deriveSessionOrigin(
471
517
  parentSessionPath: string | undefined,
518
+ launchedSubagent: boolean,
472
519
  handoffMetadata: HandoffSessionMetadata | undefined,
473
520
  ): SessionOrigin | undefined {
474
521
  if (!parentSessionPath) {
475
522
  return undefined;
476
523
  }
477
-
524
+ if (launchedSubagent) {
525
+ return "subagent";
526
+ }
478
527
  return handoffMetadata?.origin === "handoff" ? "handoff" : "unknown_child";
479
528
  }
480
529
 
@@ -507,10 +556,7 @@ function appendDurableHandoffMetadataChunks(
507
556
  }
508
557
 
509
558
  const { entryId, ts, metadata } = durableHandoffMetadata;
510
- chunks.push(
511
- createMetadataChunk(entryId, ts, "handoff_goal", metadata.goal),
512
- createMetadataChunk(entryId, ts, "handoff_next_task", metadata.nextTask),
513
- );
559
+ chunks.push(createMetadataChunk(entryId, ts, "handoff_goal", metadata.goal));
514
560
  }
515
561
 
516
562
  function createMetadataChunk(
@@ -27,6 +27,7 @@ import {
27
27
  type ExtractedSessionTail,
28
28
  extractSessionRecord,
29
29
  extractSessionTail,
30
+ inferSessionOrigin,
30
31
  type SessionFileTouch,
31
32
  } from "./extract.ts";
32
33
  import { deriveSessionRepoRoots } from "./normalize.ts";
@@ -361,14 +362,6 @@ function applyTailSync(
361
362
  sourceKind: "handoff_goal",
362
363
  text: metadata.goal,
363
364
  });
364
- insertTextChunk(db, {
365
- sessionId: baseline.sessionId,
366
- entryId,
367
- entryType: "custom",
368
- ts,
369
- sourceKind: "handoff_next_task",
370
- text: metadata.nextTask,
371
- });
372
365
  }
373
366
 
374
367
  for (const chunk of scan.chunks) {
@@ -392,10 +385,15 @@ function buildTailSessionRow(
392
385
  ): SessionRow {
393
386
  const scan = tail.scan;
394
387
  const tailHandoffMetadata = baseline.handoffGoal ? undefined : scan.handoffMetadata?.metadata;
395
- const tailOrigin =
396
- baseline.parentSessionPath && tailHandoffMetadata?.origin === "handoff"
397
- ? ("handoff" as const)
398
- : undefined;
388
+ let tailOrigin: SessionOrigin | undefined;
389
+ if (baseline.sessionOrigin !== "subagent" && baseline.sessionOrigin !== "fork") {
390
+ tailOrigin = inferSessionOrigin(
391
+ baseline.sessionId,
392
+ baseline.sessionPath,
393
+ baseline.parentSessionPath,
394
+ tailHandoffMetadata,
395
+ );
396
+ }
399
397
  const nextOrigin = resolveSessionOrigin(sessionOrigin, tailOrigin, baseline.sessionOrigin);
400
398
 
401
399
  return {
@@ -416,7 +414,6 @@ function buildTailSessionRow(
416
414
  parentSessionId: baseline.parentSessionId,
417
415
  sessionOrigin: baseline.parentSessionPath ? (nextOrigin ?? "unknown_child") : undefined,
418
416
  handoffGoal: baseline.handoffGoal ?? tailHandoffMetadata?.goal,
419
- handoffNextTask: baseline.handoffNextTask ?? tailHandoffMetadata?.nextTask,
420
417
  indexedFileSize: tail.indexedFileSize,
421
418
  indexedFileMtimeMs: tail.indexedFileMtimeMs,
422
419
  indexedFileAnchor: tail.indexedFileAnchor,