@openscout/protocol 0.2.64 → 0.2.83

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 (214) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +103 -52
  3. package/dist/a2a.d.ts +224 -0
  4. package/dist/a2a.d.ts.map +1 -0
  5. package/dist/a2a.js +225 -0
  6. package/dist/activity.d.ts +70 -0
  7. package/dist/activity.d.ts.map +1 -0
  8. package/dist/activity.js +1 -0
  9. package/dist/actors.d.ts +7 -3
  10. package/dist/actors.d.ts.map +1 -0
  11. package/dist/actors.js +15 -1
  12. package/dist/agent-broker-feed.d.ts +84 -0
  13. package/dist/agent-broker-feed.d.ts.map +1 -0
  14. package/dist/agent-broker-feed.js +1 -0
  15. package/dist/agent-identity.d.ts +23 -0
  16. package/dist/agent-identity.d.ts.map +1 -0
  17. package/dist/agent-identity.js +122 -8
  18. package/dist/agent-runs.d.ts +191 -0
  19. package/dist/agent-runs.d.ts.map +1 -0
  20. package/dist/agent-runs.js +365 -0
  21. package/dist/assigned-roles.d.ts +205 -0
  22. package/dist/assigned-roles.d.ts.map +1 -0
  23. package/dist/assigned-roles.js +228 -0
  24. package/dist/broker-routes.d.ts +40 -0
  25. package/dist/broker-routes.d.ts.map +1 -0
  26. package/dist/broker-routes.js +56 -0
  27. package/dist/budget.d.ts +65 -0
  28. package/dist/budget.d.ts.map +1 -0
  29. package/dist/budget.js +19 -0
  30. package/dist/capability-matrix.d.ts +230 -0
  31. package/dist/capability-matrix.d.ts.map +1 -0
  32. package/dist/capability-matrix.js +304 -0
  33. package/dist/channel-identity.d.ts +40 -0
  34. package/dist/channel-identity.d.ts.map +1 -0
  35. package/dist/channel-identity.js +108 -0
  36. package/dist/collaboration.d.ts +45 -16
  37. package/dist/collaboration.d.ts.map +1 -0
  38. package/dist/collaboration.js +39 -23
  39. package/dist/common.d.ts +4 -3
  40. package/dist/common.d.ts.map +1 -0
  41. package/dist/conversations.d.ts +1 -0
  42. package/dist/conversations.d.ts.map +1 -0
  43. package/dist/cursor-transport.d.ts +22 -0
  44. package/dist/cursor-transport.d.ts.map +1 -0
  45. package/dist/cursor-transport.js +10 -0
  46. package/dist/debug-attach.d.ts +29 -0
  47. package/dist/debug-attach.d.ts.map +1 -0
  48. package/dist/debug-attach.js +6 -0
  49. package/dist/deliveries.d.ts +16 -2
  50. package/dist/deliveries.d.ts.map +1 -0
  51. package/dist/durable-actions.d.ts +94 -0
  52. package/dist/durable-actions.d.ts.map +1 -0
  53. package/dist/durable-actions.js +1 -0
  54. package/dist/events.d.ts +27 -1
  55. package/dist/events.d.ts.map +1 -0
  56. package/dist/events.js +14 -1
  57. package/dist/harness-feature-support.d.ts +76 -0
  58. package/dist/harness-feature-support.d.ts.map +1 -0
  59. package/dist/harness-feature-support.js +29 -0
  60. package/dist/host-integrations.d.ts +22 -0
  61. package/dist/host-integrations.d.ts.map +1 -0
  62. package/dist/host-integrations.js +64 -0
  63. package/dist/inbox.d.ts +50 -0
  64. package/dist/inbox.d.ts.map +1 -0
  65. package/dist/inbox.js +9 -0
  66. package/dist/index.d.ts +35 -2
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +34 -2
  69. package/dist/invocations.d.ts +119 -4
  70. package/dist/invocations.d.ts.map +1 -0
  71. package/dist/invocations.js +201 -1
  72. package/dist/mesh.d.ts +98 -0
  73. package/dist/mesh.d.ts.map +1 -0
  74. package/dist/mesh.js +18 -1
  75. package/dist/messages.d.ts +3 -0
  76. package/dist/messages.d.ts.map +1 -0
  77. package/dist/native-interactions.d.ts +30 -0
  78. package/dist/native-interactions.d.ts.map +1 -0
  79. package/dist/native-interactions.js +51 -0
  80. package/dist/observed-status.d.ts +33 -0
  81. package/dist/observed-status.d.ts.map +1 -0
  82. package/dist/observed-status.js +6 -0
  83. package/dist/orchestrator-prompt.d.ts +10 -0
  84. package/dist/orchestrator-prompt.d.ts.map +1 -0
  85. package/dist/orchestrator-prompt.js +36 -0
  86. package/dist/permission-policy.d.ts +5 -0
  87. package/dist/permission-policy.d.ts.map +1 -0
  88. package/dist/permission-policy.js +15 -0
  89. package/dist/provisional-agent-names.d.ts +30 -0
  90. package/dist/provisional-agent-names.d.ts.map +1 -0
  91. package/dist/provisional-agent-names.js +224 -0
  92. package/dist/read-receipts.d.ts +12 -0
  93. package/dist/read-receipts.d.ts.map +1 -0
  94. package/dist/read-receipts.js +1 -0
  95. package/dist/relay-agent-card.d.ts +10 -0
  96. package/dist/relay-agent-card.d.ts.map +1 -0
  97. package/dist/rendezvous.d.ts +97 -0
  98. package/dist/rendezvous.d.ts.map +1 -0
  99. package/dist/rendezvous.js +35 -0
  100. package/dist/reserved-vocabulary.d.ts +21 -0
  101. package/dist/reserved-vocabulary.d.ts.map +1 -0
  102. package/dist/reserved-vocabulary.js +101 -0
  103. package/dist/route-aliases.d.ts +112 -0
  104. package/dist/route-aliases.d.ts.map +1 -0
  105. package/dist/route-aliases.js +1 -0
  106. package/dist/runtime-catalog-contract.d.ts +39 -0
  107. package/dist/runtime-catalog-contract.d.ts.map +1 -0
  108. package/dist/runtime-catalog-contract.js +17 -0
  109. package/dist/runtime-catalog.d.ts +11 -0
  110. package/dist/runtime-catalog.d.ts.map +1 -0
  111. package/dist/runtime-catalog.generated.d.ts +230 -0
  112. package/dist/runtime-catalog.generated.d.ts.map +1 -0
  113. package/dist/runtime-catalog.generated.js +299 -0
  114. package/dist/runtime-catalog.js +192 -0
  115. package/dist/runtime-execution.d.ts +161 -0
  116. package/dist/runtime-execution.d.ts.map +1 -0
  117. package/dist/runtime-execution.js +327 -0
  118. package/dist/runtime-profiles.d.ts +12 -0
  119. package/dist/runtime-profiles.d.ts.map +1 -0
  120. package/dist/runtime-profiles.js +20 -0
  121. package/dist/scout-agent-card.d.ts +10 -0
  122. package/dist/scout-agent-card.d.ts.map +1 -0
  123. package/dist/scout-composer.d.ts +27 -0
  124. package/dist/scout-composer.d.ts.map +1 -0
  125. package/dist/scout-composer.js +192 -0
  126. package/dist/scout-delivery.d.ts +99 -4
  127. package/dist/scout-delivery.d.ts.map +1 -0
  128. package/dist/scout-dispatch.d.ts +40 -3
  129. package/dist/scout-dispatch.d.ts.map +1 -0
  130. package/dist/scout-reply-context.d.ts +1 -0
  131. package/dist/scout-reply-context.d.ts.map +1 -0
  132. package/dist/session-display-state.d.ts +132 -0
  133. package/dist/session-display-state.d.ts.map +1 -0
  134. package/dist/session-display-state.js +94 -0
  135. package/dist/terminal-host-topology.d.ts +119 -0
  136. package/dist/terminal-host-topology.d.ts.map +1 -0
  137. package/dist/terminal-host-topology.js +21 -0
  138. package/dist/terminal-sessions.d.ts +142 -0
  139. package/dist/terminal-sessions.d.ts.map +1 -0
  140. package/dist/terminal-sessions.js +81 -0
  141. package/dist/terminal-surface-id.d.ts +73 -0
  142. package/dist/terminal-surface-id.d.ts.map +1 -0
  143. package/dist/terminal-surface-id.js +162 -0
  144. package/dist/terminal-workspaces.d.ts +199 -0
  145. package/dist/terminal-workspaces.d.ts.map +1 -0
  146. package/dist/terminal-workspaces.js +287 -0
  147. package/dist/thread-events.d.ts +1 -0
  148. package/dist/thread-events.d.ts.map +1 -0
  149. package/dist/time.d.ts +16 -0
  150. package/dist/time.d.ts.map +1 -0
  151. package/dist/time.js +33 -0
  152. package/dist/transports.d.ts +1 -0
  153. package/dist/transports.d.ts.map +1 -0
  154. package/package.json +37 -9
  155. package/src/a2a.ts +471 -0
  156. package/src/activity.ts +113 -0
  157. package/src/actors.ts +42 -8
  158. package/src/agent-broker-feed.ts +102 -0
  159. package/src/agent-identity.ts +143 -8
  160. package/src/agent-runs.ts +675 -0
  161. package/src/assigned-roles.ts +437 -0
  162. package/src/broker-routes.ts +62 -0
  163. package/src/budget.ts +88 -0
  164. package/src/capability-matrix.ts +648 -0
  165. package/src/channel-identity.ts +147 -0
  166. package/src/collaboration.ts +78 -48
  167. package/src/common.ts +24 -5
  168. package/src/cursor-transport.ts +34 -0
  169. package/src/debug-attach.ts +38 -0
  170. package/src/deliveries.ts +16 -1
  171. package/src/durable-actions.ts +125 -0
  172. package/src/events.ts +31 -0
  173. package/src/harness-feature-support.ts +116 -0
  174. package/src/host-integrations.ts +104 -0
  175. package/src/inbox.ts +68 -0
  176. package/src/index.ts +34 -2
  177. package/src/invocations.ts +351 -4
  178. package/src/mesh.ts +130 -0
  179. package/src/messages.ts +2 -0
  180. package/src/native-interactions.ts +100 -0
  181. package/src/observed-status.ts +90 -0
  182. package/src/orchestrator-prompt.ts +47 -0
  183. package/src/permission-policy.ts +21 -0
  184. package/src/provisional-agent-names.ts +272 -0
  185. package/src/read-receipts.ts +12 -0
  186. package/src/relay-agent-card.ts +73 -63
  187. package/src/rendezvous.ts +154 -0
  188. package/src/reserved-vocabulary.ts +135 -0
  189. package/src/route-aliases.ts +130 -0
  190. package/src/runtime-catalog-contract.ts +60 -0
  191. package/src/runtime-catalog.generated.ts +300 -0
  192. package/src/runtime-catalog.md +21 -0
  193. package/src/runtime-catalog.ts +193 -0
  194. package/src/runtime-catalog.v1.json +81 -0
  195. package/src/runtime-execution.ts +545 -0
  196. package/src/runtime-profiles.ts +37 -0
  197. package/src/scout-agent-card.ts +10 -0
  198. package/src/scout-composer.ts +249 -0
  199. package/src/scout-delivery.ts +113 -3
  200. package/src/scout-dispatch.ts +33 -2
  201. package/src/session-display-state.ts +202 -0
  202. package/src/terminal-host-topology.ts +132 -0
  203. package/src/terminal-sessions.ts +187 -0
  204. package/src/terminal-surface-id.ts +189 -0
  205. package/src/terminal-workspaces.ts +455 -0
  206. package/src/time.ts +49 -0
  207. package/dist/agent-address.d.ts +0 -1
  208. package/dist/agent-address.js +0 -1
  209. package/dist/agent-selectors.d.ts +0 -1
  210. package/dist/agent-selectors.js +0 -1
  211. package/src/agent-address.ts +0 -1
  212. package/src/agent-identity.test.ts +0 -435
  213. package/src/agent-selectors.ts +0 -1
  214. package/src/scout-agent-card.test.ts +0 -54
@@ -0,0 +1,202 @@
1
+ import type { MetadataMap, ScoutId } from "./common.js";
2
+
3
+ export type ScoutSessionDisplayPhase =
4
+ | "idle"
5
+ | "running"
6
+ | "waiting"
7
+ | "failed"
8
+ | "completed";
9
+
10
+ export interface ScoutDisplayMessage {
11
+ id: ScoutId;
12
+ role: "user" | "assistant" | "system";
13
+ text?: string;
14
+ updatedAt: number;
15
+ metadata?: MetadataMap;
16
+ }
17
+
18
+ export interface ScoutDisplayTool {
19
+ id: ScoutId;
20
+ name: string;
21
+ status: "running" | "completed" | "failed";
22
+ summary?: string;
23
+ updatedAt: number;
24
+ metadata?: MetadataMap;
25
+ }
26
+
27
+ export interface ScoutDisplayAttentionItem {
28
+ id: ScoutId;
29
+ kind: "question" | "approval" | "permission" | "waiting";
30
+ title: string;
31
+ summary?: string;
32
+ sourceRef?: string;
33
+ updatedAt: number;
34
+ metadata?: MetadataMap;
35
+ }
36
+
37
+ export interface ScoutDisplaySubagent {
38
+ id: ScoutId;
39
+ agentType?: string;
40
+ title: string;
41
+ status: "running" | "completed" | "failed";
42
+ summary?: string;
43
+ updatedAt: number;
44
+ metadata?: MetadataMap;
45
+ }
46
+
47
+ export interface ScoutDisplayTask {
48
+ id: ScoutId;
49
+ title: string;
50
+ status: "pending" | "in_progress" | "completed";
51
+ updatedAt: number;
52
+ metadata?: MetadataMap;
53
+ }
54
+
55
+ export interface ScoutDisplayUsage {
56
+ inputTokens?: number;
57
+ outputTokens?: number;
58
+ totalTokens?: number;
59
+ source?: "provider_exact" | "tokenizer_estimate" | "char_heuristic" | "manual_estimate";
60
+ }
61
+
62
+ export interface ScoutDisplayTurn {
63
+ id: ScoutId;
64
+ status: "streaming" | "completed" | "interrupted" | "error";
65
+ startedAt: number;
66
+ endedAt?: number;
67
+ blockCount: number;
68
+ messageCount: number;
69
+ toolCount: number;
70
+ attentionCount: number;
71
+ summary?: string;
72
+ updatedAt: number;
73
+ metadata?: MetadataMap;
74
+ }
75
+
76
+ export interface ScoutSessionDisplayState {
77
+ sessionId: ScoutId;
78
+ phase: ScoutSessionDisplayPhase;
79
+ currentMessage: ScoutDisplayMessage | null;
80
+ activeTools: Record<ScoutId, ScoutDisplayTool>;
81
+ attention: Record<ScoutId, ScoutDisplayAttentionItem>;
82
+ activeSubagents: Record<ScoutId, ScoutDisplaySubagent>;
83
+ turns: ScoutDisplayTurn[];
84
+ tasks: ScoutDisplayTask[];
85
+ usage?: ScoutDisplayUsage;
86
+ updatedAt: number;
87
+ metadata?: MetadataMap;
88
+ }
89
+
90
+ export type ScoutSessionDisplayEvent =
91
+ | { type: "phase_changed"; phase: ScoutSessionDisplayPhase; at: number }
92
+ | { type: "message_updated"; message: ScoutDisplayMessage; at?: number }
93
+ | { type: "tool_started"; tool: ScoutDisplayTool; at?: number }
94
+ | { type: "tool_finished"; toolId: ScoutId; failed?: boolean; summary?: string; at: number }
95
+ | { type: "attention_opened"; item: ScoutDisplayAttentionItem; at?: number }
96
+ | { type: "attention_closed"; itemId: ScoutId; at: number }
97
+ | { type: "subagent_started"; subagent: ScoutDisplaySubagent; at?: number }
98
+ | { type: "subagent_finished"; subagentId: ScoutId; failed?: boolean; summary?: string; at: number }
99
+ | { type: "turns_snapshot"; turns: ScoutDisplayTurn[]; at: number }
100
+ | { type: "tasks_snapshot"; tasks: ScoutDisplayTask[]; at: number }
101
+ | { type: "usage_updated"; usage: ScoutDisplayUsage; at: number }
102
+ | { type: "reset_thread"; at: number };
103
+
104
+ export function createScoutSessionDisplayState(input: {
105
+ sessionId: ScoutId;
106
+ now?: number;
107
+ }): ScoutSessionDisplayState {
108
+ return {
109
+ sessionId: input.sessionId,
110
+ phase: "idle",
111
+ currentMessage: null,
112
+ activeTools: {},
113
+ attention: {},
114
+ activeSubagents: {},
115
+ turns: [],
116
+ tasks: [],
117
+ updatedAt: input.now ?? 0,
118
+ };
119
+ }
120
+
121
+ export function reduceScoutSessionDisplayState(
122
+ state: ScoutSessionDisplayState,
123
+ event: ScoutSessionDisplayEvent,
124
+ ): ScoutSessionDisplayState {
125
+ switch (event.type) {
126
+ case "phase_changed":
127
+ return { ...state, phase: event.phase, updatedAt: event.at };
128
+ case "message_updated":
129
+ return { ...state, currentMessage: event.message, updatedAt: event.at ?? event.message.updatedAt };
130
+ case "tool_started":
131
+ return {
132
+ ...state,
133
+ activeTools: { ...state.activeTools, [event.tool.id]: event.tool },
134
+ updatedAt: event.at ?? event.tool.updatedAt,
135
+ };
136
+ case "tool_finished": {
137
+ const current = state.activeTools[event.toolId];
138
+ if (!current) return { ...state, updatedAt: event.at };
139
+ return {
140
+ ...state,
141
+ activeTools: {
142
+ ...state.activeTools,
143
+ [event.toolId]: {
144
+ ...current,
145
+ status: event.failed ? "failed" : "completed",
146
+ summary: event.summary ?? current.summary,
147
+ updatedAt: event.at,
148
+ },
149
+ },
150
+ updatedAt: event.at,
151
+ };
152
+ }
153
+ case "attention_opened":
154
+ return {
155
+ ...state,
156
+ phase: state.phase === "idle" ? "waiting" : state.phase,
157
+ attention: { ...state.attention, [event.item.id]: event.item },
158
+ updatedAt: event.at ?? event.item.updatedAt,
159
+ };
160
+ case "attention_closed": {
161
+ const next = { ...state.attention };
162
+ delete next[event.itemId];
163
+ return {
164
+ ...state,
165
+ attention: next,
166
+ phase: state.phase === "waiting" && Object.keys(next).length === 0 ? "idle" : state.phase,
167
+ updatedAt: event.at,
168
+ };
169
+ }
170
+ case "subagent_started":
171
+ return {
172
+ ...state,
173
+ activeSubagents: { ...state.activeSubagents, [event.subagent.id]: event.subagent },
174
+ updatedAt: event.at ?? event.subagent.updatedAt,
175
+ };
176
+ case "subagent_finished": {
177
+ const current = state.activeSubagents[event.subagentId];
178
+ if (!current) return { ...state, updatedAt: event.at };
179
+ return {
180
+ ...state,
181
+ activeSubagents: {
182
+ ...state.activeSubagents,
183
+ [event.subagentId]: {
184
+ ...current,
185
+ status: event.failed ? "failed" : "completed",
186
+ summary: event.summary ?? current.summary,
187
+ updatedAt: event.at,
188
+ },
189
+ },
190
+ updatedAt: event.at,
191
+ };
192
+ }
193
+ case "turns_snapshot":
194
+ return { ...state, turns: [...event.turns], updatedAt: event.at };
195
+ case "tasks_snapshot":
196
+ return { ...state, tasks: [...event.tasks], updatedAt: event.at };
197
+ case "usage_updated":
198
+ return { ...state, usage: event.usage, updatedAt: event.at };
199
+ case "reset_thread":
200
+ return createScoutSessionDisplayState({ sessionId: state.sessionId, now: event.at });
201
+ }
202
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Herdr session topology, projected for Scout clients.
3
+ *
4
+ * Herdr owns the real thing: workspaces, tabs, panes, layouts, and the agent
5
+ * records inside them. This type is a READ-ONLY PROJECTION of that topology —
6
+ * what Scout's terminals view needs to represent a herdr session faithfully
7
+ * without becoming a second layout manager. Layout coordinates and split
8
+ * ratios are included read-only so a client can render the arrangement
9
+ * faithfully; there are deliberately no mutation verbs here: if a client wants
10
+ * to change the topology, the handoff is the herdr client itself (`herdr
11
+ * session attach`, `herdr agent focus`), not a Scout control.
12
+ *
13
+ * The server builds this from `herdr --session <n> workspace list`,
14
+ * `tab list`, and `pane list` JSON, plus the per-tab layout geometry from
15
+ * `herdr --session <n> api snapshot`. A session whose server is stopped
16
+ * projects as `{ running: false, workspaces: [] }` — an ordinary state, not
17
+ * an error.
18
+ */
19
+
20
+ /** Agent state as herdr reports it, never as Scout infers it. */
21
+ export type HerdrAgentStatus = "idle" | "working" | "blocked" | "done" | "unknown";
22
+
23
+ /**
24
+ * The harness-native session reference herdr reports for a pane (e.g. a Claude
25
+ * session id). Opaque to Scout: carried so clients can correlate, never parsed.
26
+ */
27
+ export type HerdrAgentSessionRef = {
28
+ agent: string;
29
+ kind: string;
30
+ source: string;
31
+ value: string;
32
+ };
33
+
34
+ export type HerdrPaneProjection = {
35
+ paneId: string;
36
+ /** Herdr's stable terminal id; `herdr agent <verb>` accepts it as a target. */
37
+ terminalId: string | null;
38
+ tabId: string;
39
+ workspaceId: string;
40
+ label: string | null;
41
+ /** Detected/reported agent label, e.g. "claude". Null for plain shells. */
42
+ agent: string | null;
43
+ agentStatus: HerdrAgentStatus;
44
+ agentSession: HerdrAgentSessionRef | null;
45
+ cwd: string | null;
46
+ foregroundCwd: string | null;
47
+ focused: boolean;
48
+ /**
49
+ * Scrollback position as herdr reports it. `maxOffsetFromBottom` is the
50
+ * pane's backlog depth — the cheapest activity signal the projection has.
51
+ * Null when the host does not report scroll state (older herdr).
52
+ */
53
+ scroll: HerdrPaneScroll | null;
54
+ };
55
+
56
+ export type HerdrPaneScroll = {
57
+ maxOffsetFromBottom: number;
58
+ offsetFromBottom: number;
59
+ viewportRows: number;
60
+ };
61
+
62
+ /**
63
+ * Layout geometry for one tab, read from `herdr api snapshot`. Rects are
64
+ * absolute terminal cell coordinates as herdr reports them (the area includes
65
+ * herdr's chrome offset); clients normalize against `area` to render a
66
+ * proportional replica.
67
+ */
68
+ export type HerdrLayoutRect = {
69
+ x: number;
70
+ y: number;
71
+ width: number;
72
+ height: number;
73
+ };
74
+
75
+ export type HerdrLayoutPane = {
76
+ paneId: string;
77
+ focused: boolean;
78
+ rect: HerdrLayoutRect;
79
+ };
80
+
81
+ export type HerdrLayoutSplit = {
82
+ id: string | null;
83
+ direction: "right" | "down" | null;
84
+ ratio: number | null;
85
+ rect: HerdrLayoutRect | null;
86
+ };
87
+
88
+ export type HerdrTabLayout = {
89
+ tabId: string;
90
+ workspaceId: string;
91
+ area: HerdrLayoutRect;
92
+ focusedPaneId: string | null;
93
+ zoomed: boolean;
94
+ panes: HerdrLayoutPane[];
95
+ splits: HerdrLayoutSplit[];
96
+ };
97
+
98
+ export type HerdrTabProjection = {
99
+ tabId: string;
100
+ workspaceId: string;
101
+ label: string | null;
102
+ number: number | null;
103
+ focused: boolean;
104
+ agentStatus: HerdrAgentStatus;
105
+ panes: HerdrPaneProjection[];
106
+ /** Per-tab pane geometry; null when the snapshot did not cover this tab. */
107
+ layout: HerdrTabLayout | null;
108
+ };
109
+
110
+ export type HerdrWorkspaceProjection = {
111
+ workspaceId: string;
112
+ label: string | null;
113
+ number: number | null;
114
+ focused: boolean;
115
+ activeTabId: string | null;
116
+ agentStatus: HerdrAgentStatus;
117
+ tabs: HerdrTabProjection[];
118
+ };
119
+
120
+ export type HerdrSessionTopology = {
121
+ /** Herdr session name, e.g. "openscout". */
122
+ session: string;
123
+ /** Whether the session's herdr server answered. */
124
+ running: boolean;
125
+ workspaces: HerdrWorkspaceProjection[];
126
+ /** Server wall-clock when the projection was observed. */
127
+ observedAt: number;
128
+ };
129
+
130
+ export function emptyHerdrSessionTopology(session: string, running: boolean): HerdrSessionTopology {
131
+ return { session, running, workspaces: [], observedAt: Date.now() };
132
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Terminal session registry.
3
+ *
4
+ * The durable noun is a HARNESS SESSION — a stable agent session identified by
5
+ * its harness-native source id and resumable via a resume command. A harness
6
+ * session is *materialized* through one or more disposable TERMINAL SURFACES
7
+ * (tmux, zellij, future ssh/host-control). Backends are interchangeable: the
8
+ * source session id is the stable key; the surface is the throwaway shell.
9
+ *
10
+ * This module defines the shared contract used by `scout session intake`
11
+ * (which writes the record), the runtime store (which persists it), the
12
+ * web/runtime APIs (which list it + materialize surfaces), and the app
13
+ * terminal UI (which reads it to attach/observe/take over).
14
+ *
15
+ * Terminal scrollback is never imported as Scout messages — a surface is a
16
+ * relay target, not a message source.
17
+ */
18
+
19
+ import { formatTerminalSurfaceId, parseTerminalSurfaceId } from "./terminal-surface-id.js";
20
+ import type { TerminalHostId, TerminalSurfaceId } from "./terminal-surface-id.js";
21
+
22
+ /** Lifecycle of a single materialized surface. */
23
+ export type TerminalSurfaceState = "live" | "detached" | "exited";
24
+
25
+ /**
26
+ * Relay descriptor for one surface.
27
+ *
28
+ * The per-backend optional fields are legacy: they are what a relay built
29
+ * before host adapters expects to find, and they are only populated for the
30
+ * hosts that predate the registry. A new host adds an adapter, not a field —
31
+ * `backend` plus `sessionName` is the whole contract.
32
+ */
33
+ export type TerminalSurfaceRelay = {
34
+ backend: TerminalHostId;
35
+ sessionName: string;
36
+ tmuxSession?: string;
37
+ zellijSession?: string;
38
+ zellijPaneId?: string;
39
+ };
40
+
41
+ /** One disposable terminal surface a harness session has been materialized through. */
42
+ export type TerminalSurface = {
43
+ /**
44
+ * Opaque durable handle for this surface. Absent on records written before
45
+ * surface ids existed; derive one with `terminalSurfaceIdForSurface`.
46
+ */
47
+ surfaceId?: TerminalSurfaceId;
48
+ /** Host that owns the surface. Registered adapters may add hosts the union does not name. */
49
+ backend: TerminalHostId;
50
+ /** Backend session name (e.g. tmux target, zellij session). Secondary metadata. */
51
+ sessionName: string;
52
+ paneId: string | null;
53
+ attachCommand: string[];
54
+ observeCommand: string[] | null;
55
+ relay: TerminalSurfaceRelay;
56
+ /** Lifecycle state; absent means unknown / not yet observed. */
57
+ state?: TerminalSurfaceState;
58
+ /**
59
+ * Zellij requires a short socket dir on macOS (the default $TMPDIR exceeds the
60
+ * Unix socket-path length limit). Any relay attaching to a Scout-created zellij
61
+ * surface must preserve this, or it lands in a different server namespace.
62
+ */
63
+ socketDir?: string;
64
+ };
65
+
66
+ /**
67
+ * Durable registry record: a stable harness session plus the disposable
68
+ * terminal surfaces it owns. Re-materializing in another backend appends a
69
+ * surface; it never changes the record identity.
70
+ */
71
+ export type TerminalSessionRecord = {
72
+ /** Stable Scout record id (derived from harness + sourceSessionId by default). */
73
+ id: string;
74
+ harness: string;
75
+ /** Harness-native session id — the stable identity across backends. */
76
+ sourceSessionId: string;
77
+ cwd: string;
78
+ resumeCommand: string;
79
+ /**
80
+ * Where the record came from. `registry` records are Scout-owned harness
81
+ * sessions with a real harness and resume command; `discovered` records are
82
+ * live multiplexer sessions observed on the host, for which both are unknown
83
+ * and must be left empty rather than filled with the backend and attach argv.
84
+ * Absent means `registry`.
85
+ */
86
+ origin?: "registry" | "discovered";
87
+ surfaces: TerminalSurface[];
88
+ createdAt: number;
89
+ updatedAt: number;
90
+ metadata?: Record<string, unknown>;
91
+ };
92
+
93
+ /** Upsert input for a registry record. Timestamps + id are assigned by the store. */
94
+ export type TerminalSessionRecordInput = {
95
+ /** Optional explicit id; defaults to a deterministic id from harness + sourceSessionId. */
96
+ id?: string;
97
+ harness: string;
98
+ sourceSessionId: string;
99
+ cwd: string;
100
+ resumeCommand: string;
101
+ surfaces?: TerminalSurface[];
102
+ metadata?: Record<string, unknown>;
103
+ };
104
+
105
+ /**
106
+ * The durable handle for a surface. Prefers the stored id and derives the same
107
+ * value from the surface's address otherwise, so a record written before
108
+ * surface ids existed resolves to the id it would be issued today.
109
+ */
110
+ export function terminalSurfaceIdForSurface(
111
+ surface: Pick<TerminalSurface, "surfaceId" | "backend" | "sessionName" | "paneId">,
112
+ options: { nodeId?: string | null } = {},
113
+ ): TerminalSurfaceId {
114
+ if (surface.surfaceId) return surface.surfaceId;
115
+ return formatTerminalSurfaceId({
116
+ backend: surface.backend,
117
+ hostSession: surface.sessionName,
118
+ paneId: surface.paneId,
119
+ nodeId: options.nodeId ?? null,
120
+ });
121
+ }
122
+
123
+ /** Where a match is being evaluated, for the parts of matching that are node-scoped. */
124
+ export type TerminalSurfaceMatchScope = {
125
+ /**
126
+ * This host's own Scout node id, when the caller knows it. A surface observed
127
+ * with no node in its id was observed HERE, so this is the only node whose
128
+ * handles may claim it.
129
+ */
130
+ localNodeId?: string | null;
131
+ };
132
+
133
+ /**
134
+ * Whether a handle scoped to `handleNodeId` may address a surface scoped to
135
+ * `surfaceNodeId`.
136
+ *
137
+ * Three cases, and the third is the one a review reproduced:
138
+ *
139
+ * - Both sides name a node: they must name the same one. Without this a handle
140
+ * for one node matched an identically named session on another.
141
+ * - The handle names none: it is a legacy key, which names no machine and must
142
+ * keep matching whatever it finds.
143
+ * - The handle names a node and the OBSERVATION does not. A node-less
144
+ * observation is by definition local — it is a session this host just listed
145
+ * — so only the local node's handle can be the one it belongs to. Letting any
146
+ * node-scoped handle match it meant handles for `node-a` and `node-b` both
147
+ * bound to one discovered local session, and both workspaces reported it
148
+ * Running: one session proving two machines live. A foreign handle never
149
+ * matches, and when this host does not know its own node id it cannot prove
150
+ * the handle is local either, so it does not claim it.
151
+ */
152
+ export function terminalSurfaceNodeScopeMatches(input: {
153
+ handleNodeId: string | null;
154
+ surfaceNodeId: string | null;
155
+ localNodeId?: string | null;
156
+ }): boolean {
157
+ if (input.handleNodeId === null) return true;
158
+ if (input.surfaceNodeId !== null) return input.handleNodeId === input.surfaceNodeId;
159
+ const localNodeId = input.localNodeId?.trim() || null;
160
+ return localNodeId !== null && input.handleNodeId === localNodeId;
161
+ }
162
+
163
+ /** The node a surface belongs to, which lives inside its own opaque id. */
164
+ export function terminalSurfaceNodeId(
165
+ surface: Pick<TerminalSurface, "surfaceId">,
166
+ ): string | null {
167
+ return parseTerminalSurfaceId(surface.surfaceId)?.nodeId ?? null;
168
+ }
169
+
170
+ /** True when a handle addresses this surface, in either the opaque or legacy form. */
171
+ export function terminalSurfaceMatchesId(
172
+ surface: Pick<TerminalSurface, "surfaceId" | "backend" | "sessionName" | "paneId">,
173
+ handle: string | null | undefined,
174
+ scope: TerminalSurfaceMatchScope = {},
175
+ ): boolean {
176
+ const address = parseTerminalSurfaceId(handle);
177
+ if (!address) return false;
178
+ if (address.backend !== surface.backend || address.hostSession !== surface.sessionName) return false;
179
+ // A legacy key carries no pane, so it matches any pane on the session; an
180
+ // opaque id that names a pane must match it exactly.
181
+ if (address.paneId !== null && address.paneId !== (surface.paneId ?? null)) return false;
182
+ return terminalSurfaceNodeScopeMatches({
183
+ handleNodeId: address.nodeId ?? null,
184
+ surfaceNodeId: terminalSurfaceNodeId(surface),
185
+ localNodeId: scope.localNodeId,
186
+ });
187
+ }