pi-sessions 0.7.2 → 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 +71 -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} +67 -46
  12. package/extensions/session-auto-title/model.ts +25 -17
  13. package/extensions/session-auto-title/retitle.ts +26 -12
  14. package/extensions/session-auto-title/wizard.ts +12 -6
  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 +80 -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
@@ -0,0 +1,73 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type { Api, Model } from "@earendil-works/pi-ai";
3
+ import { modelsAreEqual } from "@earendil-works/pi-ai";
4
+ import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
5
+ import { resolveCliModel } from "@earendil-works/pi-coding-agent";
6
+
7
+ export type AuthenticatedModelResolution =
8
+ | {
9
+ ok: true;
10
+ model: Model<Api>;
11
+ thinkingLevel: ThinkingLevel | undefined;
12
+ warning: string | undefined;
13
+ }
14
+ | {
15
+ ok: false;
16
+ error: string;
17
+ warning: string | undefined;
18
+ };
19
+
20
+ /**
21
+ * Resolves a model pattern with Pi's CLI grammar (fuzzy aliases, slash ids,
22
+ * `:thinking` suffixes), then narrows the result to a fresh entry from the
23
+ * runtime's available snapshot. resolveCliModel intentionally searches every
24
+ * model for CLI setup flows; nested extension work must only run activated,
25
+ * authenticated models, and the available object crosses that boundary.
26
+ */
27
+ export function resolveAuthenticatedModel(options: {
28
+ modelRuntime: ModelRuntime;
29
+ modelPattern: string;
30
+ thinkingLevel?: ThinkingLevel | undefined;
31
+ }): AuthenticatedModelResolution {
32
+ const resolved = resolveCliModel({
33
+ cliModel: options.modelPattern,
34
+ ...(options.thinkingLevel ? { cliThinking: options.thinkingLevel } : {}),
35
+ modelRuntime: options.modelRuntime,
36
+ });
37
+
38
+ if (resolved.error || !resolved.model) {
39
+ return {
40
+ ok: false,
41
+ error: stripCliGuidance(resolved.error) ?? `Unknown model "${options.modelPattern}".`,
42
+ warning: resolved.warning,
43
+ };
44
+ }
45
+
46
+ const available = options.modelRuntime
47
+ .getAvailableSnapshot()
48
+ .find((model) => modelsAreEqual(model, resolved.model));
49
+ if (!available) {
50
+ return {
51
+ ok: false,
52
+ error: `Model "${resolved.model.provider}/${resolved.model.id}" is not an available authenticated model.`,
53
+ warning: resolved.warning,
54
+ };
55
+ }
56
+
57
+ return {
58
+ ok: true,
59
+ model: available,
60
+ thinkingLevel: options.thinkingLevel ?? resolved.thinkingLevel,
61
+ warning: resolved.warning,
62
+ };
63
+ }
64
+
65
+ // resolveCliModel errors reference --list-models, which does not exist in extension surfaces.
66
+ function stripCliGuidance(error: string | undefined): string | undefined {
67
+ const stripped = error?.replace(/(?:^|\s+)[^.!?\n]*--list-models[^.!?\n]*(?:[.!?]|$)/, "").trim();
68
+ return stripped || undefined;
69
+ }
70
+
71
+ export function formatAvailableModelList(models: readonly Model<Api>[]): string {
72
+ return models.map((model) => `${model.provider}/${model.id}`).join(", ");
73
+ }
@@ -0,0 +1,33 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { type ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
3
+
4
+ export type ModelRuntimeProvider = (modelRegistry: ModelRegistry) => Promise<ModelRuntime>;
5
+
6
+ // Workaround for a missing extension API: resolveCliModel and createAgentSession want a
7
+ // ModelRuntime, but ExtensionContext only exposes the ModelRegistry compatibility facade,
8
+ // which keeps its runtime private. Until pi exposes the runtime to extensions, build a
9
+ // fresh one per use and mirror the registered providers (e.g. pi-claude-bridge) so their
10
+ // configs — including live streamSimple closures — carry over by reference.
11
+ export async function createSessionModelRuntime(
12
+ modelRegistry: ModelRegistry,
13
+ ): Promise<ModelRuntime> {
14
+ const modelRuntime = await ModelRuntime.create({ allowModelNetwork: false });
15
+
16
+ for (const providerId of modelRegistry.getRegisteredProviderIds()) {
17
+ const config = modelRegistry.getRegisteredProviderConfig(providerId);
18
+ if (config) {
19
+ modelRuntime.registerProvider(providerId, config);
20
+ }
21
+ }
22
+
23
+ await modelRuntime.refresh({ allowNetwork: false });
24
+ return modelRuntime;
25
+ }
26
+
27
+ // A model reference carried on the session context is stale relative to the mirrored runtime
28
+ // (it was resolved against the facade's private runtime, not this one). Re-resolve it against
29
+ // the runtime's snapshot so downstream calls use the mirror's live provider config, falling
30
+ // back to the original reference when the runtime has not seen it.
31
+ export function freshenModel(runtime: ModelRuntime, model: Model<Api>): Model<Api> {
32
+ return runtime.getModel(model.provider, model.id) ?? model;
33
+ }
@@ -1,18 +1,15 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
- import type { ModelReference } from "./settings.ts";
2
+
3
+ export interface ExactModelReference {
4
+ provider: string;
5
+ modelId: string;
6
+ }
3
7
 
4
8
  export function findModelByReference(
5
- availableModels: Model<Api>[],
6
- reference: ModelReference,
9
+ availableModels: readonly Model<Api>[],
10
+ reference: ExactModelReference,
7
11
  ): Model<Api> | undefined {
8
12
  return availableModels.find(
9
13
  (model) => model.provider === reference.provider && model.id === reference.modelId,
10
14
  );
11
15
  }
12
-
13
- export function resolveConfiguredModel(
14
- availableModels: Model<Api>[],
15
- configuredModel: ModelReference | undefined,
16
- ): Model<Api> | undefined {
17
- return configuredModel ? findModelByReference(availableModels, configuredModel) : undefined;
18
- }
@@ -0,0 +1,89 @@
1
+ import {
2
+ type Component,
3
+ getKeybindings,
4
+ visibleWidth,
5
+ wrapTextWithAnsi,
6
+ } from "@earendil-works/pi-tui";
7
+ import type { RenderTheme } from "./theme.ts";
8
+
9
+ interface CollapsibleTextKeybindings {
10
+ getKeys(keybinding: "app.tools.expand"): string[];
11
+ }
12
+
13
+ export interface CollapsibleTextOptions {
14
+ collapsedRows: number;
15
+ theme: RenderTheme;
16
+ keybindings?: CollapsibleTextKeybindings | undefined;
17
+ }
18
+
19
+ export class CollapsibleText implements Component {
20
+ private text = "";
21
+ private expanded = false;
22
+ private visibleSupplementalRows = 0;
23
+ private hiddenSupplementalRows = 0;
24
+ private readonly collapsedRows: number;
25
+ private readonly theme: RenderTheme;
26
+ private readonly keybindings: CollapsibleTextKeybindings;
27
+
28
+ constructor(options: CollapsibleTextOptions) {
29
+ this.collapsedRows = Math.max(1, Math.floor(options.collapsedRows));
30
+ this.theme = options.theme;
31
+ this.keybindings = options.keybindings ?? getKeybindings();
32
+ }
33
+
34
+ setText(text: string): void {
35
+ this.text = text;
36
+ }
37
+
38
+ setExpanded(expanded: boolean): void {
39
+ this.expanded = expanded;
40
+ }
41
+
42
+ setSupplementalRowCounts(visibleRows: number, hiddenRows: number): void {
43
+ this.visibleSupplementalRows = Math.max(0, Math.floor(visibleRows));
44
+ this.hiddenSupplementalRows = Math.max(0, Math.floor(hiddenRows));
45
+ }
46
+
47
+ invalidate(): void {}
48
+
49
+ render(width: number): string[] {
50
+ const renderWidth = Math.max(1, width);
51
+ const rows = wrapTextWithAnsi(this.text, renderWidth);
52
+ if (this.expanded) {
53
+ return rows.map((row) => padToWidth(row, renderWidth));
54
+ }
55
+ const hiddenBodyRows = Math.max(0, rows.length - this.collapsedRows);
56
+ if (hiddenBodyRows === 0 && this.hiddenSupplementalRows === 0) {
57
+ return rows.map((row) => padToWidth(row, renderWidth));
58
+ }
59
+
60
+ const remaining = hiddenBodyRows + this.hiddenSupplementalRows;
61
+ const total = rows.length + this.visibleSupplementalRows + this.hiddenSupplementalRows;
62
+ const visibleRows = rows
63
+ .slice(0, this.collapsedRows)
64
+ .map((row) => padToWidth(row, renderWidth));
65
+ const hintRows = wrapTextWithAnsi(this.formatExpandHint(remaining, total), renderWidth).map(
66
+ (row) => padToWidth(row, renderWidth),
67
+ );
68
+ return [...visibleRows, ...hintRows];
69
+ }
70
+
71
+ private formatExpandHint(remaining: number, total: number): string {
72
+ const keys = this.keybindings.getKeys("app.tools.expand").join("/");
73
+ const lineLabel = remaining === 1 ? "line" : "lines";
74
+ if (!keys) {
75
+ return this.theme.fg(
76
+ "muted",
77
+ `(${remaining} more ${lineLabel}, ${total} total, expand key unbound)`,
78
+ );
79
+ }
80
+ return `${this.theme.fg("muted", `(${remaining} more ${lineLabel}, ${total} total, `)}${this.theme.fg(
81
+ "dim",
82
+ keys,
83
+ )}${this.theme.fg("muted", " to expand)")}`;
84
+ }
85
+ }
86
+
87
+ function padToWidth(text: string, width: number): string {
88
+ return `${text}${" ".repeat(Math.max(0, width - visibleWidth(text)))}`;
89
+ }
@@ -0,0 +1,84 @@
1
+ import { type Component, Container, Spacer, Text, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import { CollapsibleText } from "./collapsible-text.ts";
3
+ import type { RenderTheme } from "./theme.ts";
4
+
5
+ export interface ExpandableContentPresentation {
6
+ header: string;
7
+ metadata?: string[] | undefined;
8
+ expandedMetadata?: string[] | undefined;
9
+ body?:
10
+ | {
11
+ text: string;
12
+ collapsedRows: number;
13
+ spacingBefore?: number | undefined;
14
+ }
15
+ | undefined;
16
+ }
17
+
18
+ export class ExpandableContentLayout implements Component {
19
+ private presentation: ExpandableContentPresentation | undefined;
20
+ private expanded = false;
21
+ private body: CollapsibleText | undefined;
22
+ private bodyRows: number | undefined;
23
+
24
+ constructor(private readonly theme: RenderTheme) {}
25
+
26
+ update(presentation: ExpandableContentPresentation, expanded: boolean): void {
27
+ this.presentation = presentation;
28
+ this.expanded = expanded;
29
+
30
+ if (!presentation.body) {
31
+ this.body = undefined;
32
+ this.bodyRows = undefined;
33
+ return;
34
+ }
35
+
36
+ if (!this.body || this.bodyRows !== presentation.body.collapsedRows) {
37
+ this.body = new CollapsibleText({
38
+ collapsedRows: presentation.body.collapsedRows,
39
+ theme: this.theme,
40
+ });
41
+ this.bodyRows = presentation.body.collapsedRows;
42
+ }
43
+ this.body.setText(presentation.body.text);
44
+ this.body.setExpanded(expanded);
45
+ }
46
+
47
+ invalidate(): void {
48
+ this.body?.invalidate();
49
+ }
50
+
51
+ render(width: number): string[] {
52
+ if (!this.presentation) {
53
+ return [];
54
+ }
55
+
56
+ const container = new Container();
57
+ container.addChild(new Text(this.presentation.header, 0, 0));
58
+
59
+ const visibleMetadata = this.presentation.metadata ?? [];
60
+ const expandedMetadata = this.presentation.expandedMetadata ?? [];
61
+ const metadata = [...visibleMetadata, ...(this.expanded ? expandedMetadata : [])];
62
+ if (metadata.length > 0) {
63
+ container.addChild(new Text(metadata.join("\n"), 0, 0));
64
+ }
65
+
66
+ if (this.body && this.presentation.body?.text) {
67
+ this.body.setSupplementalRowCounts(
68
+ countRenderedRows(visibleMetadata, width),
69
+ countRenderedRows(expandedMetadata, width),
70
+ );
71
+ const spacingBefore = this.presentation.body.spacingBefore ?? 1;
72
+ if (spacingBefore > 0) {
73
+ container.addChild(new Spacer(spacingBefore));
74
+ }
75
+ container.addChild(this.body);
76
+ }
77
+ return container.render(width);
78
+ }
79
+ }
80
+
81
+ function countRenderedRows(lines: readonly string[], width: number): number {
82
+ const renderWidth = Math.max(1, width);
83
+ return lines.reduce((total, line) => total + wrapTextWithAnsi(line, renderWidth).length, 0);
84
+ }
@@ -0,0 +1,5 @@
1
+ export interface RenderTheme {
2
+ bold(text: string): string;
3
+ fg(token: string, text: string): string;
4
+ bg(token: string, text: string): string;
5
+ }
@@ -2,10 +2,15 @@ import { randomUUID } from "node:crypto";
2
2
  import { EventEmitter } from "node:events";
3
3
  import net from "node:net";
4
4
  import { readFrames, writeFrame } from "./framing.ts";
5
- import { BROKER_FRAME_SCHEMA, type SessionMessagingBrokerFrame } from "./protocol.ts";
5
+ import {
6
+ BROKER_FRAME_SCHEMA,
7
+ type OutboundSessionEnvelope,
8
+ type SessionEnvelopeSendFailureReason,
9
+ type SessionMessagingBrokerFrame,
10
+ } from "./protocol.ts";
6
11
  import { getSessionMessagingSocketPath } from "./socket-path.ts";
7
12
 
8
- export const INCOMING_SESSION_MESSAGE_EVENT = "session_messaging.incoming_message";
13
+ export const INCOMING_SESSION_ENVELOPE_EVENT = "session_messaging.incoming";
9
14
 
10
15
  const REQUEST_TIMEOUT_MS = 10_000;
11
16
  const SOCKET_PATH = getSessionMessagingSocketPath();
@@ -17,24 +22,22 @@ interface PendingListRequest {
17
22
  }
18
23
 
19
24
  interface PendingSendRequest {
20
- resolve(result: SessionMessageSendResult): void;
25
+ resolve(result: SessionEnvelopeSendResult): void;
21
26
  reject(error: Error): void;
22
27
  timeout: NodeJS.Timeout;
23
28
  }
24
29
 
25
- export interface SessionMessageSendResult {
26
- messageId: string;
27
- delivered: boolean;
28
- error?: string | undefined;
29
- }
30
+ export type SessionEnvelopeSendResult =
31
+ | { delivered: true }
32
+ | {
33
+ delivered: false;
34
+ reason?: SessionEnvelopeSendFailureReason | undefined;
35
+ error?: string | undefined;
36
+ };
30
37
 
31
- export interface SendSessionMessageOptions {
32
- messageId: string;
38
+ export interface SendSessionEnvelopeOptions {
33
39
  target: string;
34
- body: string;
35
- sentAt: string;
36
- requestResponse?: boolean | undefined;
37
- sourceToolCallId?: string | undefined;
40
+ envelope: OutboundSessionEnvelope;
38
41
  }
39
42
 
40
43
  export class SessionMessagingClient extends EventEmitter {
@@ -139,39 +142,33 @@ export class SessionMessagingClient extends EventEmitter {
139
142
  });
140
143
  }
141
144
 
142
- sendMessage(options: SendSessionMessageOptions): Promise<SessionMessageSendResult> {
145
+ sendEnvelope(options: SendSessionEnvelopeOptions): Promise<SessionEnvelopeSendResult> {
143
146
  const socket = this.requireSocket();
144
147
  const requestId = randomUUID();
145
148
 
146
149
  return new Promise((resolve, reject) => {
147
150
  const timeout = setTimeout(() => {
148
151
  this.pendingSends.delete(requestId);
149
- reject(new Error("Session message send timed out."));
152
+ reject(new Error("Session envelope send timed out."));
150
153
  }, REQUEST_TIMEOUT_MS + 1_000);
151
154
  this.pendingSends.set(requestId, { resolve, reject, timeout });
152
155
  writeFrame(socket, {
153
156
  type: "send",
154
157
  requestId,
155
- messageId: options.messageId,
156
158
  target: options.target,
157
- body: options.body,
158
- requestResponse: options.requestResponse,
159
- sourceToolCallId: options.sourceToolCallId,
160
- sentAt: options.sentAt,
159
+ envelope: options.envelope,
161
160
  });
162
161
  });
163
162
  }
164
163
 
165
164
  acknowledgeIncoming(
166
165
  requestId: string,
167
- messageId: string,
168
166
  result: { delivered: true } | { delivered: false; error: string },
169
167
  ): void {
170
168
  const socket = this.requireSocket();
171
169
  writeFrame(socket, {
172
170
  type: "incoming_ack",
173
171
  requestId,
174
- messageId,
175
172
  delivered: result.delivered,
176
173
  error: result.delivered ? undefined : result.error,
177
174
  });
@@ -209,18 +206,22 @@ export class SessionMessagingClient extends EventEmitter {
209
206
  break;
210
207
  }
211
208
  case "incoming":
212
- this.emit(INCOMING_SESSION_MESSAGE_EVENT, frame.requestId, frame.message);
209
+ this.emit(INCOMING_SESSION_ENVELOPE_EVENT, frame.requestId, frame.envelope);
213
210
  break;
214
211
  case "send_result": {
215
212
  const pending = this.pendingSends.get(frame.requestId);
216
213
  if (!pending) return;
217
214
  clearTimeout(pending.timeout);
218
215
  this.pendingSends.delete(frame.requestId);
219
- pending.resolve({
220
- messageId: frame.messageId,
221
- delivered: frame.delivered,
222
- error: frame.error,
223
- });
216
+ pending.resolve(
217
+ frame.delivered
218
+ ? { delivered: true }
219
+ : {
220
+ delivered: false,
221
+ ...(frame.reason === undefined ? {} : { reason: frame.reason }),
222
+ ...(frame.error === undefined ? {} : { error: frame.error }),
223
+ },
224
+ );
224
225
  break;
225
226
  }
226
227
  case "error":
@@ -1,6 +1,87 @@
1
1
  import { type Static, Type } from "typebox";
2
2
 
3
- export const SESSION_MESSAGE_PAYLOAD_SCHEMA = Type.Object({
3
+ export const TASK_REPORT_REFERENCE_SCHEMA = Type.Object({
4
+ reference: Type.String({
5
+ description:
6
+ "A precise reference such as a file path and line range, symbol name, command, URL, session id, or external artifact.",
7
+ }),
8
+ description: Type.Optional(
9
+ Type.String({
10
+ description: "Why this reference matters or what evidence the parent should take from it.",
11
+ }),
12
+ ),
13
+ });
14
+
15
+ export const TASK_REPORT_SCHEMA = Type.Object({
16
+ status: Type.Union(
17
+ [
18
+ Type.Literal("done", {
19
+ description: "The delegated task or requested follow-up is complete.",
20
+ }),
21
+ Type.Literal("blocked", {
22
+ description:
23
+ "The task cannot continue without a specific decision, clarification, permission, dependency, or other action outside the scope of the task.",
24
+ }),
25
+ Type.Literal("incomplete", {
26
+ description:
27
+ "Useful work was performed, but the requested result could not be completed and it's uncertain what should be done to reach completion.",
28
+ }),
29
+ ],
30
+ { description: "The subagent's assessment of the task at the end of this turn." },
31
+ ),
32
+ summary: Type.String({
33
+ description:
34
+ "A concise, 3-4 sentence self-contained account of the outcome and its most important evidence.",
35
+ }),
36
+ details: Type.Optional(
37
+ Type.String({
38
+ description:
39
+ "Optional supporting context such as reasoning, implementation notes, validation performed, limitations, unexpected behavior or roadblocks, or failed approaches.",
40
+ }),
41
+ ),
42
+ references: Type.Optional(
43
+ Type.Array(TASK_REPORT_REFERENCE_SCHEMA, {
44
+ description: "Supporting material for the report.",
45
+ }),
46
+ ),
47
+ nextSteps: Type.Optional(
48
+ Type.Array(Type.String(), {
49
+ description:
50
+ "Optional concrete actions that would extend or build on the state of the work. If blocked or incomplete, call out specifically what is causing the early return.",
51
+ }),
52
+ ),
53
+ });
54
+
55
+ const OUTBOUND_MESSAGE_ENVELOPE_SCHEMA = Type.Object({
56
+ kind: Type.Literal("message"),
57
+ messageId: Type.String(),
58
+ body: Type.String(),
59
+ requestResponse: Type.Optional(Type.Boolean()),
60
+ sentAt: Type.String(),
61
+ sourceToolCallId: Type.Optional(Type.String()),
62
+ });
63
+ const OUTBOUND_CANCEL_ENVELOPE_SCHEMA = Type.Object({
64
+ kind: Type.Literal("cancel"),
65
+ cancelId: Type.String(),
66
+ sentAt: Type.String(),
67
+ });
68
+ const OUTBOUND_SUBAGENT_REPORT_ENVELOPE_SCHEMA = Type.Intersect([
69
+ Type.Object({
70
+ kind: Type.Literal("subagent_report"),
71
+ reportId: Type.String(),
72
+ sentAt: Type.String(),
73
+ }),
74
+ TASK_REPORT_SCHEMA,
75
+ ]);
76
+
77
+ export const OUTBOUND_SESSION_ENVELOPE_SCHEMA = Type.Union([
78
+ OUTBOUND_MESSAGE_ENVELOPE_SCHEMA,
79
+ OUTBOUND_CANCEL_ENVELOPE_SCHEMA,
80
+ OUTBOUND_SUBAGENT_REPORT_ENVELOPE_SCHEMA,
81
+ ]);
82
+
83
+ export const SESSION_MESSAGE_ENVELOPE_SCHEMA = Type.Object({
84
+ kind: Type.Literal("message"),
4
85
  messageId: Type.String(),
5
86
  source: Type.String(),
6
87
  target: Type.String(),
@@ -9,6 +90,29 @@ export const SESSION_MESSAGE_PAYLOAD_SCHEMA = Type.Object({
9
90
  sentAt: Type.String(),
10
91
  sourceToolCallId: Type.Optional(Type.String()),
11
92
  });
93
+ export const SESSION_CANCEL_ENVELOPE_SCHEMA = Type.Object({
94
+ kind: Type.Literal("cancel"),
95
+ cancelId: Type.String(),
96
+ source: Type.String(),
97
+ target: Type.String(),
98
+ sentAt: Type.String(),
99
+ });
100
+ export const SESSION_SUBAGENT_REPORT_ENVELOPE_SCHEMA = Type.Intersect([
101
+ Type.Object({
102
+ kind: Type.Literal("subagent_report"),
103
+ reportId: Type.String(),
104
+ source: Type.String(),
105
+ target: Type.String(),
106
+ sentAt: Type.String(),
107
+ }),
108
+ TASK_REPORT_SCHEMA,
109
+ ]);
110
+
111
+ export const SESSION_ENVELOPE_SCHEMA = Type.Union([
112
+ SESSION_MESSAGE_ENVELOPE_SCHEMA,
113
+ SESSION_CANCEL_ENVELOPE_SCHEMA,
114
+ SESSION_SUBAGENT_REPORT_ENVELOPE_SCHEMA,
115
+ ]);
12
116
 
13
117
  const REGISTER_CLIENT_FRAME_SCHEMA = Type.Object({
14
118
  type: Type.Literal("register"),
@@ -24,17 +128,12 @@ const LIST_CLIENT_FRAME_SCHEMA = Type.Object({
24
128
  const SEND_CLIENT_FRAME_SCHEMA = Type.Object({
25
129
  type: Type.Literal("send"),
26
130
  requestId: Type.String(),
27
- messageId: Type.String(),
28
131
  target: Type.String(),
29
- body: Type.String(),
30
- requestResponse: Type.Optional(Type.Boolean()),
31
- sentAt: Type.String(),
32
- sourceToolCallId: Type.Optional(Type.String()),
132
+ envelope: OUTBOUND_SESSION_ENVELOPE_SCHEMA,
33
133
  });
34
134
  const INCOMING_ACK_CLIENT_FRAME_SCHEMA = Type.Object({
35
135
  type: Type.Literal("incoming_ack"),
36
136
  requestId: Type.String(),
37
- messageId: Type.String(),
38
137
  delivered: Type.Boolean(),
39
138
  error: Type.Optional(Type.String()),
40
139
  });
@@ -55,13 +154,18 @@ const SESSIONS_BROKER_FRAME_SCHEMA = Type.Object({
55
154
  const INCOMING_BROKER_FRAME_SCHEMA = Type.Object({
56
155
  type: Type.Literal("incoming"),
57
156
  requestId: Type.String(),
58
- message: SESSION_MESSAGE_PAYLOAD_SCHEMA,
157
+ envelope: SESSION_ENVELOPE_SCHEMA,
59
158
  });
159
+ export const SESSION_ENVELOPE_SEND_FAILURE_REASON_SCHEMA = Type.Union([
160
+ Type.Literal("no_session"),
161
+ Type.Literal("disconnected"),
162
+ ]);
163
+
60
164
  const SEND_RESULT_BROKER_FRAME_SCHEMA = Type.Object({
61
165
  type: Type.Literal("send_result"),
62
166
  requestId: Type.String(),
63
- messageId: Type.String(),
64
167
  delivered: Type.Boolean(),
168
+ reason: Type.Optional(SESSION_ENVELOPE_SEND_FAILURE_REASON_SCHEMA),
65
169
  error: Type.Optional(Type.String()),
66
170
  });
67
171
  const ERROR_BROKER_FRAME_SCHEMA = Type.Object({
@@ -86,7 +190,21 @@ export const BROKER_FRAME_SCHEMA = Type.Union([
86
190
  ERROR_BROKER_FRAME_SCHEMA,
87
191
  ]);
88
192
 
89
- export type SessionMessagePayload = Static<typeof SESSION_MESSAGE_PAYLOAD_SCHEMA>;
193
+ export type SessionEnvelopeSendFailureReason = Static<
194
+ typeof SESSION_ENVELOPE_SEND_FAILURE_REASON_SCHEMA
195
+ >;
196
+ export type TaskReportReference = Static<typeof TASK_REPORT_REFERENCE_SCHEMA>;
197
+ export type TaskReport = Static<typeof TASK_REPORT_SCHEMA>;
198
+ export type OutboundSessionMessageEnvelope = Static<typeof OUTBOUND_MESSAGE_ENVELOPE_SCHEMA>;
199
+ export type OutboundSessionCancelEnvelope = Static<typeof OUTBOUND_CANCEL_ENVELOPE_SCHEMA>;
200
+ export type OutboundSubagentReportEnvelope = Static<
201
+ typeof OUTBOUND_SUBAGENT_REPORT_ENVELOPE_SCHEMA
202
+ >;
203
+ export type OutboundSessionEnvelope = Static<typeof OUTBOUND_SESSION_ENVELOPE_SCHEMA>;
204
+ export type SessionMessageEnvelope = Static<typeof SESSION_MESSAGE_ENVELOPE_SCHEMA>;
205
+ export type SessionCancelEnvelope = Static<typeof SESSION_CANCEL_ENVELOPE_SCHEMA>;
206
+ export type SessionSubagentReportEnvelope = Static<typeof SESSION_SUBAGENT_REPORT_ENVELOPE_SCHEMA>;
207
+ export type SessionEnvelope = Static<typeof SESSION_ENVELOPE_SCHEMA>;
90
208
  export type SessionMessagingSendClientFrame = Static<typeof SEND_CLIENT_FRAME_SCHEMA>;
91
209
  export type SessionMessagingIncomingAckClientFrame = Static<
92
210
  typeof INCOMING_ACK_CLIENT_FRAME_SCHEMA
@@ -3,9 +3,10 @@ import type { FileTouchOp, FileTouchSource, PathScope } from "../../session-sear
3
3
  import { safeParseTypeBoxJson } from "../typebox.ts";
4
4
  import type { SqliteDatabase } from "./sqlite.ts";
5
5
 
6
- export const INDEX_SCHEMA_VERSION = 12;
6
+ export const INDEX_SCHEMA_VERSION = 14;
7
7
 
8
- export type SessionOrigin = "handoff" | "fork" | "unknown_child";
8
+ export type SessionOrigin = "handoff" | "subagent" | "fork" | "unknown_child";
9
+ export type SessionKind = "user" | "subagent";
9
10
  export type SessionLineageRelation =
10
11
  | "self"
11
12
  | "parent"
@@ -32,7 +33,6 @@ export interface SessionRow {
32
33
  parentSessionId?: string | undefined;
33
34
  sessionOrigin?: SessionOrigin | undefined;
34
35
  handoffGoal?: string | undefined;
35
- handoffNextTask?: string | undefined;
36
36
  indexedFileSize?: number | undefined;
37
37
  indexedFileMtimeMs?: number | undefined;
38
38
  indexedFileAnchor?: string | undefined;
@@ -50,7 +50,6 @@ export interface SessionLineageRow {
50
50
  parentSessionId?: string | undefined;
51
51
  sessionOrigin?: SessionOrigin | undefined;
52
52
  handoffGoal?: string | undefined;
53
- handoffNextTask?: string | undefined;
54
53
  }
55
54
 
56
55
  export interface SessionRelatedSessionRow extends SessionLineageRow {
@@ -93,6 +92,7 @@ export interface SearchSessionsParams {
93
92
  before?: string | undefined;
94
93
  touched?: string[] | undefined;
95
94
  changed?: string[] | undefined;
95
+ kind?: SessionKind | undefined;
96
96
  sort?: SearchSort | undefined;
97
97
  limit?: number | undefined;
98
98
  excludeSessionIds?: string[] | undefined;
@@ -135,7 +135,6 @@ export interface SearchSessionResult {
135
135
  firstUserPrompt?: string | undefined;
136
136
  sessionOrigin?: SessionOrigin | undefined;
137
137
  handoffGoal?: string | undefined;
138
- handoffNextTask?: string | undefined;
139
138
  relation?: SessionLineageRelation | undefined;
140
139
  snippet: string;
141
140
  evidence: SessionSearchEvidence[];
@@ -156,6 +155,7 @@ export type SessionIndexDatabase = SqliteDatabase;
156
155
  export const NULLABLE_STRING_SCHEMA = Type.Union([Type.String(), Type.Null()]);
157
156
  export const SESSION_ORIGIN_SCHEMA = Type.Union([
158
157
  Type.Literal("handoff"),
158
+ Type.Literal("subagent"),
159
159
  Type.Literal("fork"),
160
160
  Type.Literal("unknown_child"),
161
161
  ]);