@openscout/runtime 0.2.37 → 0.2.38

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.
@@ -1,9 +1,10 @@
1
1
  import { createServer } from "node:http";
2
2
  import { hostname } from "node:os";
3
3
  import { join } from "node:path";
4
- import { assertValidCollaborationEvent, assertValidCollaborationRecord, buildRelayReturnAddress, } from "@openscout/protocol";
4
+ import { assertValidCollaborationEvent, assertValidCollaborationRecord, buildRelayReturnAddress, SCOUT_DISPATCHER_AGENT_ID, } from "@openscout/protocol";
5
5
  import { createInMemoryControlRuntime } from "./broker.js";
6
6
  import { FileBackedBrokerJournal } from "./broker-journal.js";
7
+ import { buildDispatchEnvelope, resolveAgentLabel, } from "./scout-dispatcher.js";
7
8
  import { buildCollaborationInvocation } from "./collaboration-invocations.js";
8
9
  import { discoverMeshNodes } from "./mesh-discovery.js";
9
10
  import { buildMeshCollaborationEventBundle, buildMeshCollaborationRecordBundle, buildMeshInvocationBundle, buildMeshMessageBundle, forwardMeshCollaborationEvent, forwardMeshCollaborationRecord, forwardMeshInvocation, fetchPeerAgents, forwardMeshMessage, } from "./mesh-forwarding.js";
@@ -192,13 +193,12 @@ function runDurableWrite(work) {
192
193
  durableWriteQueue = next.then(() => undefined, () => undefined);
193
194
  return next;
194
195
  }
195
- async function appendJournalEntries(entries) {
196
- await journal.appendEntries(entries);
197
- }
198
196
  async function commitDurableEntries(entriesInput, applyRuntime, options = {}) {
199
- const entries = normalizeJournalEntries(entriesInput);
200
- await appendJournalEntries(entries);
201
- await applyRuntime();
197
+ const entries = await journal.appendEntries(normalizeJournalEntries(entriesInput));
198
+ if (entries.length === 0) {
199
+ return [];
200
+ }
201
+ await applyRuntime(entries);
202
202
  if (options.enqueueProjection !== false) {
203
203
  projection.enqueueEntries(entries);
204
204
  }
@@ -223,9 +223,13 @@ async function upsertAgentDurably(agent) {
223
223
  await commitDurableEntries([
224
224
  { kind: "actor.upsert", actor: agent },
225
225
  { kind: "agent.upsert", agent },
226
- ], async () => {
227
- await runtime.upsertActor(agent);
228
- await runtime.upsertAgent(agent);
226
+ ], async (entries) => {
227
+ if (entries.some((entry) => entry.kind === "actor.upsert")) {
228
+ await runtime.upsertActor(agent);
229
+ }
230
+ if (entries.some((entry) => entry.kind === "agent.upsert")) {
231
+ await runtime.upsertAgent(agent);
232
+ }
229
233
  });
230
234
  });
231
235
  }
@@ -284,6 +288,49 @@ async function recordMessageDurably(message, options = {}) {
284
288
  return { deliveries, entries };
285
289
  });
286
290
  }
291
+ async function recordScoutDispatchDurably(envelope, options = {}) {
292
+ const record = {
293
+ id: createRuntimeId("scout-dispatch"),
294
+ invocationId: options.invocationId,
295
+ conversationId: options.conversationId,
296
+ requesterId: options.requesterId,
297
+ ...envelope,
298
+ };
299
+ const dispatchEntries = [
300
+ { kind: "scout.dispatch.record", dispatch: record },
301
+ ];
302
+ let syntheticMessage = null;
303
+ if (options.conversationId) {
304
+ syntheticMessage = {
305
+ id: createRuntimeId("msg-scout"),
306
+ conversationId: options.conversationId,
307
+ actorId: SCOUT_DISPATCHER_AGENT_ID,
308
+ originNodeId: nodeId,
309
+ class: "system",
310
+ body: record.detail,
311
+ visibility: "workspace",
312
+ policy: "best_effort",
313
+ createdAt: record.dispatchedAt,
314
+ metadata: {
315
+ scoutDispatch: record,
316
+ },
317
+ };
318
+ }
319
+ return runDurableWrite(async () => {
320
+ const appended = await commitDurableEntries(dispatchEntries, async () => { });
321
+ if (!syntheticMessage) {
322
+ return { record, message: null, entries: appended };
323
+ }
324
+ const deliveries = runtime.planMessage(syntheticMessage, { localOnly: true });
325
+ const messageEntries = await commitDurableEntries([
326
+ { kind: "message.record", message: syntheticMessage },
327
+ { kind: "deliveries.record", deliveries },
328
+ ], async () => {
329
+ await runtime.commitMessage(syntheticMessage, deliveries);
330
+ });
331
+ return { record, message: syntheticMessage, entries: [...appended, ...messageEntries] };
332
+ });
333
+ }
287
334
  async function recordInvocationDurably(invocation, options = {}) {
288
335
  return runDurableWrite(async () => {
289
336
  const flight = options.flight ?? runtime.planInvocation(invocation);
@@ -388,44 +435,33 @@ async function applyMeshBundleDurably(bundle, options = {}) {
388
435
  assertValidCollaborationEvent(bundle.collaborationEvent, record);
389
436
  }
390
437
  const entries = buildMeshBundleEntries(bundle);
391
- return commitDurableEntries(entries, async () => {
392
- const appliedActorIds = new Set();
393
- const appliedAgentIds = new Set();
394
- const appliedBindingIds = new Set();
395
- await runtime.upsertNode(bundle.originNode);
396
- for (const actor of bundle.actors) {
397
- if (appliedActorIds.has(actor.id)) {
398
- continue;
399
- }
400
- appliedActorIds.add(actor.id);
401
- await runtime.upsertActor(actor);
402
- }
403
- for (const agent of bundle.agents) {
404
- if (appliedAgentIds.has(agent.id)) {
405
- continue;
406
- }
407
- appliedAgentIds.add(agent.id);
408
- if (!appliedActorIds.has(agent.id)) {
409
- appliedActorIds.add(agent.id);
410
- await runtime.upsertActor(agent);
411
- }
412
- await runtime.upsertAgent(agent);
413
- }
414
- if (bundle.conversation) {
415
- await runtime.upsertConversation(bundle.conversation);
416
- }
417
- for (const binding of bundle.bindings ?? []) {
418
- if (appliedBindingIds.has(binding.id)) {
419
- continue;
438
+ return commitDurableEntries(entries, async (retainedEntries) => {
439
+ for (const entry of retainedEntries) {
440
+ switch (entry.kind) {
441
+ case "node.upsert":
442
+ await runtime.upsertNode(entry.node);
443
+ break;
444
+ case "actor.upsert":
445
+ await runtime.upsertActor(entry.actor);
446
+ break;
447
+ case "agent.upsert":
448
+ await runtime.upsertAgent(entry.agent);
449
+ break;
450
+ case "conversation.upsert":
451
+ await runtime.upsertConversation(entry.conversation);
452
+ break;
453
+ case "binding.upsert":
454
+ await runtime.upsertBinding(entry.binding);
455
+ break;
456
+ case "collaboration.record":
457
+ await runtime.upsertCollaboration(entry.record);
458
+ break;
459
+ case "collaboration.event.record":
460
+ await runtime.appendCollaborationEvent(entry.event);
461
+ break;
462
+ default:
463
+ break;
420
464
  }
421
- appliedBindingIds.add(binding.id);
422
- await runtime.upsertBinding(binding);
423
- }
424
- if (bundle.collaborationRecord) {
425
- await runtime.upsertCollaboration(bundle.collaborationRecord);
426
- }
427
- if (bundle.collaborationEvent) {
428
- await runtime.appendCollaborationEvent(bundle.collaborationEvent);
429
465
  }
430
466
  }, options);
431
467
  }
@@ -1817,7 +1853,22 @@ async function routeRequest(request, response) {
1817
1853
  }
1818
1854
  if (method === "POST" && url.pathname === "/v1/invocations") {
1819
1855
  try {
1820
- const invocation = await readRequestBody(request);
1856
+ const payload = await readRequestBody(request);
1857
+ const resolved = resolveInvocationTarget(payload);
1858
+ if (resolved.kind !== "resolved") {
1859
+ const envelope = buildDispatchEnvelope(resolved, payload.targetLabel?.trim() || payload.targetAgentId || "", nodeId, runtime.snapshot(), { homeEndpointFor: homeEndpointForAgent });
1860
+ const { record } = await recordScoutDispatchDurably(envelope, {
1861
+ invocationId: payload.id,
1862
+ conversationId: payload.conversationId,
1863
+ requesterId: payload.requesterId,
1864
+ });
1865
+ json(response, 200, buildScoutDispatchResponse(record, payload));
1866
+ return;
1867
+ }
1868
+ const invocation = {
1869
+ ...payload,
1870
+ targetAgentId: resolved.agent.id,
1871
+ };
1821
1872
  const result = await handleCommand({ kind: "agent.invoke", invocation });
1822
1873
  json(response, 200, result);
1823
1874
  }
@@ -1828,6 +1879,32 @@ async function routeRequest(request, response) {
1828
1879
  }
1829
1880
  notFound(response);
1830
1881
  }
1882
+ function resolveInvocationTarget(payload) {
1883
+ const snapshot = runtime.snapshot();
1884
+ const directId = payload.targetAgentId?.trim();
1885
+ if (directId) {
1886
+ const agent = snapshot.agents[directId];
1887
+ if (agent && !isStaleLocalAgent(agent)) {
1888
+ return { kind: "resolved", agent };
1889
+ }
1890
+ }
1891
+ const label = payload.targetLabel?.trim() || directId || "";
1892
+ if (!label) {
1893
+ return { kind: "unparseable", label: "" };
1894
+ }
1895
+ return resolveAgentLabel(snapshot, label, {
1896
+ preferLocalNodeId: nodeId,
1897
+ helpers: { isStale: isStaleLocalAgent },
1898
+ });
1899
+ }
1900
+ function buildScoutDispatchResponse(record, payload) {
1901
+ return {
1902
+ ok: false,
1903
+ dispatchedTo: SCOUT_DISPATCHER_AGENT_ID,
1904
+ invocationId: payload.id,
1905
+ dispatch: record,
1906
+ };
1907
+ }
1831
1908
  const server = createServer((request, response) => {
1832
1909
  routeRequest(request, response).catch((error) => {
1833
1910
  json(response, 500, {
@@ -1,4 +1,4 @@
1
- import type { ActorIdentity, AgentDefinition, AgentEndpoint, CollaborationEvent, CollaborationRecord, ConversationBinding, ConversationDefinition, DeliveryAttempt, DeliveryIntent, FlightRecord, InvocationRequest, MessageRecord, NodeDefinition } from "@openscout/protocol";
1
+ import type { ActorIdentity, AgentDefinition, AgentEndpoint, CollaborationEvent, CollaborationRecord, ConversationBinding, ConversationDefinition, DeliveryAttempt, DeliveryIntent, FlightRecord, InvocationRequest, MessageRecord, NodeDefinition, ScoutDispatchRecord } from "@openscout/protocol";
2
2
  import { type RuntimeRegistrySnapshot } from "./registry.js";
3
3
  export type BrokerJournalEntry = {
4
4
  kind: "node.upsert";
@@ -46,6 +46,9 @@ export type BrokerJournalEntry = {
46
46
  metadata?: Record<string, unknown>;
47
47
  leaseOwner?: string | null;
48
48
  leaseExpiresAt?: number | null;
49
+ } | {
50
+ kind: "scout.dispatch.record";
51
+ dispatch: ScoutDispatchRecord;
49
52
  };
50
53
  export declare class FileBackedBrokerJournal {
51
54
  private readonly filePath;
@@ -57,7 +60,7 @@ export declare class FileBackedBrokerJournal {
57
60
  readEntries(): Promise<BrokerJournalEntry[]>;
58
61
  replay(visitor: (entry: BrokerJournalEntry) => void | Promise<void>): Promise<void>;
59
62
  snapshot(): RuntimeRegistrySnapshot;
60
- appendEntries(entriesInput: BrokerJournalEntry | BrokerJournalEntry[]): Promise<void>;
63
+ appendEntries(entriesInput: BrokerJournalEntry | BrokerJournalEntry[]): Promise<BrokerJournalEntry[]>;
61
64
  listCollaborationRecords(options?: {
62
65
  limit?: number;
63
66
  kind?: CollaborationRecord["kind"];
@@ -75,5 +78,13 @@ export declare class FileBackedBrokerJournal {
75
78
  limit?: number;
76
79
  }): DeliveryIntent[];
77
80
  listDeliveryAttempts(deliveryId: string): DeliveryAttempt[];
81
+ private rewriteEntries;
82
+ private selectEntriesToAppend;
83
+ private shouldAppendEntry;
84
+ private applyToSnapshot;
78
85
  private apply;
86
+ listScoutDispatches(options?: {
87
+ limit?: number;
88
+ askedLabel?: string;
89
+ }): ScoutDispatchRecord[];
79
90
  }
@@ -1,4 +1,4 @@
1
- import { appendFile, mkdir, readFile } from "node:fs/promises";
1
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
  import { createRuntimeRegistrySnapshot, } from "./registry.js";
4
4
  function cloneSnapshot(snapshot) {
@@ -35,6 +35,61 @@ function parseEntry(rawLine) {
35
35
  return null;
36
36
  }
37
37
  }
38
+ function normalizeComparableValue(value) {
39
+ if (value === undefined) {
40
+ return undefined;
41
+ }
42
+ if (Array.isArray(value)) {
43
+ return value.map((entry) => normalizeComparableValue(entry));
44
+ }
45
+ if (value && typeof value === "object") {
46
+ const normalizedEntries = Object.entries(value)
47
+ .filter(([, entry]) => entry !== undefined)
48
+ .sort(([left], [right]) => left.localeCompare(right))
49
+ .map(([key, entry]) => [key, normalizeComparableValue(entry)]);
50
+ return Object.fromEntries(normalizedEntries);
51
+ }
52
+ return value;
53
+ }
54
+ function sameValue(left, right) {
55
+ return JSON.stringify(normalizeComparableValue(left))
56
+ === JSON.stringify(normalizeComparableValue(right));
57
+ }
58
+ function dedupeKey(entry) {
59
+ switch (entry.kind) {
60
+ case "node.upsert":
61
+ return `${entry.kind}:${entry.node.id}`;
62
+ case "actor.upsert":
63
+ return `${entry.kind}:${entry.actor.id}`;
64
+ case "agent.upsert":
65
+ return `${entry.kind}:${entry.agent.id}`;
66
+ case "agent.endpoint.upsert":
67
+ return `${entry.kind}:${entry.endpoint.id}`;
68
+ case "conversation.upsert":
69
+ return `${entry.kind}:${entry.conversation.id}`;
70
+ case "binding.upsert":
71
+ return `${entry.kind}:${entry.binding.id}`;
72
+ default:
73
+ return null;
74
+ }
75
+ }
76
+ function isDedupableEntry(entry) {
77
+ return dedupeKey(entry) !== null;
78
+ }
79
+ function compactRedundantEntries(entries) {
80
+ const latestIndexByKey = new Map();
81
+ for (const [index, entry] of entries.entries()) {
82
+ const key = dedupeKey(entry);
83
+ if (!key) {
84
+ continue;
85
+ }
86
+ latestIndexByKey.set(key, index);
87
+ }
88
+ return entries.filter((entry, index) => {
89
+ const key = dedupeKey(entry);
90
+ return !key || latestIndexByKey.get(key) === index;
91
+ });
92
+ }
38
93
  export class FileBackedBrokerJournal {
39
94
  filePath;
40
95
  state = {
@@ -42,6 +97,7 @@ export class FileBackedBrokerJournal {
42
97
  collaborationEvents: [],
43
98
  deliveries: new Map(),
44
99
  deliveryAttempts: new Map(),
100
+ scoutDispatches: [],
45
101
  };
46
102
  loaded = false;
47
103
  writeQueue = Promise.resolve();
@@ -52,9 +108,14 @@ export class FileBackedBrokerJournal {
52
108
  if (this.loaded) {
53
109
  return;
54
110
  }
55
- for (const entry of await this.readEntries()) {
111
+ const entries = await this.readEntries();
112
+ for (const entry of entries) {
56
113
  this.apply(entry);
57
114
  }
115
+ const compacted = compactRedundantEntries(entries);
116
+ if (compacted.length < entries.length) {
117
+ await this.rewriteEntries(compacted);
118
+ }
58
119
  this.loaded = true;
59
120
  }
60
121
  async readEntries() {
@@ -89,17 +150,22 @@ export class FileBackedBrokerJournal {
89
150
  async appendEntries(entriesInput) {
90
151
  const entries = Array.isArray(entriesInput) ? entriesInput : [entriesInput];
91
152
  if (entries.length === 0) {
92
- return;
153
+ return [];
93
154
  }
94
- const payload = entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
155
+ const retained = this.selectEntriesToAppend(entries);
95
156
  this.writeQueue = this.writeQueue.then(async () => {
96
157
  await mkdir(dirname(this.filePath), { recursive: true });
158
+ if (retained.length === 0) {
159
+ return;
160
+ }
161
+ const payload = retained.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
97
162
  await appendFile(this.filePath, payload, "utf8");
98
- for (const entry of entries) {
163
+ for (const entry of retained) {
99
164
  this.apply(entry);
100
165
  }
101
166
  });
102
- return this.writeQueue;
167
+ await this.writeQueue;
168
+ return retained;
103
169
  }
104
170
  listCollaborationRecords(options = {}) {
105
171
  const limit = options.limit ?? 200;
@@ -131,6 +197,80 @@ export class FileBackedBrokerJournal {
131
197
  ? left.createdAt - right.createdAt
132
198
  : left.attempt - right.attempt));
133
199
  }
200
+ async rewriteEntries(entries) {
201
+ await mkdir(dirname(this.filePath), { recursive: true });
202
+ const payload = entries.length > 0
203
+ ? `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`
204
+ : "";
205
+ await writeFile(this.filePath, payload, "utf8");
206
+ }
207
+ selectEntriesToAppend(entries) {
208
+ const nextSnapshot = cloneSnapshot(this.state.snapshot);
209
+ const retained = [];
210
+ for (const entry of entries) {
211
+ if (!this.shouldAppendEntry(entry, nextSnapshot)) {
212
+ continue;
213
+ }
214
+ retained.push(entry);
215
+ this.applyToSnapshot(nextSnapshot, entry);
216
+ }
217
+ return retained;
218
+ }
219
+ shouldAppendEntry(entry, snapshot) {
220
+ if (!isDedupableEntry(entry)) {
221
+ return true;
222
+ }
223
+ switch (entry.kind) {
224
+ case "node.upsert":
225
+ return !sameValue(snapshot.nodes[entry.node.id], entry.node);
226
+ case "actor.upsert":
227
+ return !sameValue(snapshot.actors[entry.actor.id], entry.actor);
228
+ case "agent.upsert":
229
+ return !sameValue(snapshot.agents[entry.agent.id], entry.agent);
230
+ case "agent.endpoint.upsert":
231
+ return !sameValue(snapshot.endpoints[entry.endpoint.id], entry.endpoint);
232
+ case "conversation.upsert":
233
+ return !sameValue(snapshot.conversations[entry.conversation.id], entry.conversation);
234
+ case "binding.upsert":
235
+ return !sameValue(snapshot.bindings[entry.binding.id], entry.binding);
236
+ default:
237
+ return true;
238
+ }
239
+ }
240
+ applyToSnapshot(snapshot, entry) {
241
+ switch (entry.kind) {
242
+ case "node.upsert":
243
+ snapshot.nodes[entry.node.id] = entry.node;
244
+ return;
245
+ case "actor.upsert":
246
+ snapshot.actors[entry.actor.id] = entry.actor;
247
+ return;
248
+ case "agent.upsert":
249
+ snapshot.agents[entry.agent.id] = entry.agent;
250
+ if (!snapshot.actors[entry.agent.id]) {
251
+ snapshot.actors[entry.agent.id] = {
252
+ id: entry.agent.id,
253
+ kind: entry.agent.kind,
254
+ displayName: entry.agent.displayName,
255
+ handle: entry.agent.handle,
256
+ labels: entry.agent.labels,
257
+ metadata: entry.agent.metadata,
258
+ };
259
+ }
260
+ return;
261
+ case "agent.endpoint.upsert":
262
+ snapshot.endpoints[entry.endpoint.id] = entry.endpoint;
263
+ return;
264
+ case "conversation.upsert":
265
+ snapshot.conversations[entry.conversation.id] = entry.conversation;
266
+ return;
267
+ case "binding.upsert":
268
+ snapshot.bindings[entry.binding.id] = entry.binding;
269
+ return;
270
+ default:
271
+ return;
272
+ }
273
+ }
134
274
  apply(entry) {
135
275
  switch (entry.kind) {
136
276
  case "node.upsert":
@@ -200,10 +340,20 @@ export class FileBackedBrokerJournal {
200
340
  });
201
341
  return;
202
342
  }
343
+ case "scout.dispatch.record":
344
+ this.state.scoutDispatches.push(entry.dispatch);
345
+ return;
203
346
  default: {
204
347
  const exhaustive = entry;
205
348
  return exhaustive;
206
349
  }
207
350
  }
208
351
  }
352
+ listScoutDispatches(options = {}) {
353
+ const limit = options.limit ?? 200;
354
+ return [...this.state.scoutDispatches]
355
+ .filter((record) => !options.askedLabel || record.askedLabel === options.askedLabel)
356
+ .sort((left, right) => right.dispatchedAt - left.dispatchedAt)
357
+ .slice(0, limit);
358
+ }
209
359
  }
@@ -4,6 +4,10 @@ import { homedir } from "node:os";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { ensureOpenScoutCleanSlateSync, resolveOpenScoutSupportPaths } from "./support-paths.js";
7
+ /** True for paths under /tmp or /private/tmp — transient remote-install dirs. */
8
+ function isTmpPath(p) {
9
+ return /^\/(?:private\/)?tmp\//.test(p);
10
+ }
7
11
  export const DEFAULT_BROKER_HOST = "127.0.0.1";
8
12
  export const DEFAULT_BROKER_PORT = 65535;
9
13
  const BROKER_SERVICE_POLL_INTERVAL_MS = 100;
@@ -34,18 +38,36 @@ function isInstalledRuntimePackageDir(candidate) {
34
38
  && existsSync(join(candidate, "bin", "openscout-runtime.mjs"));
35
39
  }
36
40
  function findGlobalRuntimeDir() {
37
- // bun global: ~/.bun/node_modules/@openscout/runtime
38
- const bunCandidate = join(homedir(), ".bun", "node_modules", "@openscout", "runtime");
39
- if (isInstalledRuntimePackageDir(bunCandidate))
40
- return bunCandidate;
41
- // bun global install (newer layout): ~/.bun/install/global/node_modules/@openscout/runtime
42
- const bunGlobalCandidate = join(homedir(), ".bun", "install", "global", "node_modules", "@openscout", "runtime");
43
- if (isInstalledRuntimePackageDir(bunGlobalCandidate))
44
- return bunGlobalCandidate;
45
- // Also check as a nested dep of @openscout/scout
46
- const bunScoutNested = join(homedir(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime");
47
- if (isInstalledRuntimePackageDir(bunScoutNested))
48
- return bunScoutNested;
41
+ // Static candidates: bun global install layouts
42
+ const candidates = [
43
+ join(homedir(), ".bun", "node_modules", "@openscout", "runtime"),
44
+ join(homedir(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
45
+ join(homedir(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime"),
46
+ ];
47
+ for (const c of candidates) {
48
+ if (isInstalledRuntimePackageDir(c))
49
+ return c;
50
+ }
51
+ // Dynamic: resolve from `which scout` — works regardless of how it was installed
52
+ // (npm -g, bun -g, Homebrew prefix, etc.)
53
+ try {
54
+ const result = spawnSync("which", ["scout"], { encoding: "utf8", timeout: 3000 });
55
+ const scoutBin = result.stdout?.trim();
56
+ if (scoutBin) {
57
+ // scout bin → ../../lib/node_modules/@openscout/scout/node_modules/@openscout/runtime
58
+ const scoutPkg = resolve(scoutBin, "..", "..");
59
+ const nested = join(scoutPkg, "node_modules", "@openscout", "runtime");
60
+ if (isInstalledRuntimePackageDir(nested))
61
+ return nested;
62
+ // or runtime is a sibling: ../../lib/node_modules/@openscout/runtime
63
+ const sibling = resolve(scoutPkg, "..", "runtime");
64
+ if (isInstalledRuntimePackageDir(sibling))
65
+ return sibling;
66
+ }
67
+ }
68
+ catch {
69
+ // which not available or timed out
70
+ }
49
71
  return null;
50
72
  }
51
73
  function findWorkspaceRuntimeDir(startDir) {
@@ -118,15 +140,15 @@ export function resolveBrokerServiceConfig() {
118
140
  const mode = resolveBrokerServiceMode();
119
141
  const label = resolveBrokerServiceLabel(mode);
120
142
  const uid = typeof process.getuid === "function" ? process.getuid() : Number.parseInt(process.env.UID ?? "0", 10);
143
+ // Resolve paths but reject anything under /tmp — remote-install sessions
144
+ // set env vars to transient tmp dirs that don't survive reboots.
121
145
  const supportPaths = resolveOpenScoutSupportPaths();
122
- const supportDirectory = supportPaths.supportDirectory;
123
- const logsDirectory = supportPaths.brokerLogsDirectory;
124
- // Always use the stable home path for the launch agent — never inherit a
125
- // transient tmp dir that a remote-install session may have set.
126
- const rawControlHome = supportPaths.controlHome;
127
- const controlHome = /^\/(?:private\/)?tmp\//.test(rawControlHome)
146
+ const defaultSupportDir = join(homedir(), "Library", "Application Support", "OpenScout");
147
+ const supportDirectory = isTmpPath(supportPaths.supportDirectory) ? defaultSupportDir : supportPaths.supportDirectory;
148
+ const logsDirectory = join(supportDirectory, "logs", "broker");
149
+ const controlHome = isTmpPath(supportPaths.controlHome)
128
150
  ? join(homedir(), ".openscout", "control-plane")
129
- : rawControlHome;
151
+ : supportPaths.controlHome;
130
152
  const brokerHost = process.env.OPENSCOUT_BROKER_HOST ?? DEFAULT_BROKER_HOST;
131
153
  const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(DEFAULT_BROKER_PORT), 10);
132
154
  const brokerUrl = process.env.OPENSCOUT_BROKER_URL ?? buildDefaultBrokerUrl(brokerHost, brokerPort);
@@ -229,7 +251,8 @@ function resolveLaunchAgentPATH() {
229
251
  "/usr/sbin",
230
252
  "/sbin",
231
253
  ];
232
- return Array.from(new Set(entries)).join(":");
254
+ // Strip transient tmp dirs from PATH — remote-install sessions prepend them.
255
+ return Array.from(new Set(entries)).filter((e) => !isTmpPath(e)).join(":");
233
256
  }
234
257
  function xmlEscape(value) {
235
258
  return value
package/dist/schema.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export declare const CONTROL_PLANE_SCHEMA_VERSION = 1;
2
- export declare const CONTROL_PLANE_SQLITE_SCHEMA = "\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n mesh_id TEXT NOT NULL,\n name TEXT NOT NULL,\n host_name TEXT,\n advertise_scope TEXT NOT NULL,\n broker_url TEXT,\n tailnet_name TEXT,\n capabilities_json TEXT,\n labels_json TEXT,\n metadata_json TEXT,\n last_seen_at INTEGER,\n registered_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS actors (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n display_name TEXT NOT NULL,\n handle TEXT,\n labels_json TEXT,\n metadata_json TEXT,\n created_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\nCREATE TABLE IF NOT EXISTS agents (\n id TEXT PRIMARY KEY REFERENCES actors(id) ON DELETE CASCADE,\n definition_id TEXT NOT NULL,\n node_qualifier TEXT,\n workspace_qualifier TEXT,\n selector TEXT,\n default_selector TEXT,\n agent_class TEXT NOT NULL,\n capabilities_json TEXT NOT NULL,\n wake_policy TEXT NOT NULL,\n home_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n authority_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n advertise_scope TEXT NOT NULL,\n owner_id TEXT,\n metadata_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS agent_endpoints (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n harness TEXT NOT NULL,\n transport TEXT NOT NULL,\n state TEXT NOT NULL,\n address TEXT,\n session_id TEXT,\n pane TEXT,\n cwd TEXT,\n project_root TEXT,\n metadata_json TEXT,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\nCREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n title TEXT NOT NULL,\n visibility TEXT NOT NULL,\n share_mode TEXT NOT NULL,\n authority_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n topic TEXT,\n parent_conversation_id TEXT REFERENCES conversations(id) ON DELETE SET NULL,\n message_id TEXT,\n metadata_json TEXT,\n created_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\nCREATE TABLE IF NOT EXISTS conversation_members (\n conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,\n actor_id TEXT NOT NULL REFERENCES actors(id) ON DELETE CASCADE,\n role TEXT,\n PRIMARY KEY (conversation_id, actor_id)\n);\n\nCREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,\n actor_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n origin_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n class TEXT NOT NULL,\n body TEXT NOT NULL,\n reply_to_message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,\n thread_conversation_id TEXT REFERENCES conversations(id) ON DELETE SET NULL,\n speech_json TEXT,\n audience_json TEXT,\n visibility TEXT NOT NULL,\n policy TEXT NOT NULL,\n metadata_json TEXT,\n created_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS message_mentions (\n message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,\n actor_id TEXT NOT NULL REFERENCES actors(id) ON DELETE CASCADE,\n label TEXT,\n PRIMARY KEY (message_id, actor_id)\n);\n\nCREATE TABLE IF NOT EXISTS message_attachments (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,\n media_type TEXT NOT NULL,\n file_name TEXT,\n blob_key TEXT,\n url TEXT,\n metadata_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS invocations (\n id TEXT PRIMARY KEY,\n requester_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n requester_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n target_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,\n target_node_id TEXT REFERENCES nodes(id) ON DELETE SET NULL,\n action TEXT NOT NULL,\n task TEXT NOT NULL,\n conversation_id TEXT REFERENCES conversations(id) ON DELETE SET NULL,\n message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,\n context_json TEXT,\n execution_json TEXT,\n ensure_awake INTEGER NOT NULL DEFAULT 1,\n stream INTEGER NOT NULL DEFAULT 1,\n timeout_ms INTEGER,\n metadata_json TEXT,\n created_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS flights (\n id TEXT PRIMARY KEY,\n invocation_id TEXT NOT NULL REFERENCES invocations(id) ON DELETE CASCADE,\n requester_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n target_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,\n state TEXT NOT NULL,\n summary TEXT,\n output TEXT,\n error TEXT,\n metadata_json TEXT,\n started_at INTEGER,\n completed_at INTEGER\n);\n\nCREATE TABLE IF NOT EXISTS bindings (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,\n platform TEXT NOT NULL,\n mode TEXT NOT NULL,\n external_channel_id TEXT NOT NULL,\n external_thread_id TEXT,\n metadata_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS deliveries (\n id TEXT PRIMARY KEY,\n message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,\n invocation_id TEXT REFERENCES invocations(id) ON DELETE CASCADE,\n target_id TEXT NOT NULL,\n target_node_id TEXT REFERENCES nodes(id) ON DELETE SET NULL,\n target_kind TEXT NOT NULL,\n transport TEXT NOT NULL,\n reason TEXT NOT NULL,\n policy TEXT NOT NULL,\n status TEXT NOT NULL,\n binding_id TEXT REFERENCES bindings(id) ON DELETE SET NULL,\n lease_owner TEXT,\n lease_expires_at INTEGER,\n metadata_json TEXT,\n created_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\nCREATE TABLE IF NOT EXISTS delivery_attempts (\n id TEXT PRIMARY KEY,\n delivery_id TEXT NOT NULL REFERENCES deliveries(id) ON DELETE CASCADE,\n attempt INTEGER NOT NULL,\n status TEXT NOT NULL,\n error TEXT,\n external_ref TEXT,\n metadata_json TEXT,\n created_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS collaboration_records (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n state TEXT NOT NULL,\n acceptance_state TEXT NOT NULL,\n title TEXT NOT NULL,\n summary TEXT,\n created_by_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n owner_id TEXT REFERENCES actors(id) ON DELETE SET NULL,\n next_move_owner_id TEXT REFERENCES actors(id) ON DELETE SET NULL,\n conversation_id TEXT REFERENCES conversations(id) ON DELETE SET NULL,\n parent_id TEXT REFERENCES collaboration_records(id) ON DELETE SET NULL,\n priority TEXT,\n labels_json TEXT,\n relations_json TEXT,\n detail_json TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS collaboration_events (\n id TEXT PRIMARY KEY,\n record_id TEXT NOT NULL REFERENCES collaboration_records(id) ON DELETE CASCADE,\n record_kind TEXT NOT NULL,\n kind TEXT NOT NULL,\n actor_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n summary TEXT,\n metadata_json TEXT,\n created_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n node_id TEXT,\n ts INTEGER NOT NULL,\n payload_json TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS activity_items (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n ts INTEGER NOT NULL,\n conversation_id TEXT REFERENCES conversations(id) ON DELETE CASCADE,\n message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,\n invocation_id TEXT REFERENCES invocations(id) ON DELETE CASCADE,\n flight_id TEXT REFERENCES flights(id) ON DELETE CASCADE,\n record_id TEXT REFERENCES collaboration_records(id) ON DELETE CASCADE,\n actor_id TEXT REFERENCES actors(id) ON DELETE SET NULL,\n counterpart_id TEXT REFERENCES actors(id) ON DELETE SET NULL,\n agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,\n workspace_root TEXT,\n session_id TEXT,\n title TEXT,\n summary TEXT,\n payload_json TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_nodes_mesh_id\n ON nodes (mesh_id);\nCREATE INDEX IF NOT EXISTS idx_agent_endpoints_agent_updated_at\n ON agent_endpoints (agent_id, updated_at DESC);\nCREATE INDEX IF NOT EXISTS idx_messages_conversation_created_at\n ON messages (conversation_id, created_at);\nCREATE INDEX IF NOT EXISTS idx_invocations_target_created_at\n ON invocations (target_agent_id, created_at);\nCREATE INDEX IF NOT EXISTS idx_flights_target_state\n ON flights (target_agent_id, state);\nCREATE INDEX IF NOT EXISTS idx_deliveries_status_transport\n ON deliveries (status, transport);\nCREATE INDEX IF NOT EXISTS idx_collaboration_records_state\n ON collaboration_records (state);\nCREATE INDEX IF NOT EXISTS idx_collaboration_records_updated_at\n ON collaboration_records (updated_at);\nCREATE INDEX IF NOT EXISTS idx_collaboration_events_record_created_at\n ON collaboration_events (record_id, created_at);\nCREATE INDEX IF NOT EXISTS idx_events_kind_ts\n ON events (kind, ts);\nCREATE INDEX IF NOT EXISTS idx_activity_items_agent_ts\n ON activity_items (agent_id, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_actor_ts\n ON activity_items (actor_id, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_conversation_ts\n ON activity_items (conversation_id, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_workspace_ts\n ON activity_items (workspace_root, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_kind_ts\n ON activity_items (kind, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_session_ts\n ON activity_items (session_id, ts DESC);\n";
2
+ export declare const CONTROL_PLANE_SQLITE_SCHEMA = "\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n mesh_id TEXT NOT NULL,\n name TEXT NOT NULL,\n host_name TEXT,\n advertise_scope TEXT NOT NULL,\n broker_url TEXT,\n tailnet_name TEXT,\n capabilities_json TEXT,\n labels_json TEXT,\n metadata_json TEXT,\n last_seen_at INTEGER,\n registered_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS actors (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n display_name TEXT NOT NULL,\n handle TEXT,\n labels_json TEXT,\n metadata_json TEXT,\n created_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\nCREATE TABLE IF NOT EXISTS agents (\n id TEXT PRIMARY KEY REFERENCES actors(id) ON DELETE CASCADE,\n definition_id TEXT NOT NULL,\n node_qualifier TEXT,\n workspace_qualifier TEXT,\n selector TEXT,\n default_selector TEXT,\n agent_class TEXT NOT NULL,\n capabilities_json TEXT NOT NULL,\n wake_policy TEXT NOT NULL,\n home_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n authority_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n advertise_scope TEXT NOT NULL,\n owner_id TEXT,\n metadata_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS agent_endpoints (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n harness TEXT NOT NULL,\n transport TEXT NOT NULL,\n state TEXT NOT NULL,\n address TEXT,\n session_id TEXT,\n pane TEXT,\n cwd TEXT,\n project_root TEXT,\n metadata_json TEXT,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\nCREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n title TEXT NOT NULL,\n visibility TEXT NOT NULL,\n share_mode TEXT NOT NULL,\n authority_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n topic TEXT,\n parent_conversation_id TEXT REFERENCES conversations(id) ON DELETE SET NULL,\n message_id TEXT,\n metadata_json TEXT,\n created_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\nCREATE TABLE IF NOT EXISTS conversation_members (\n conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,\n actor_id TEXT NOT NULL REFERENCES actors(id) ON DELETE CASCADE,\n role TEXT,\n PRIMARY KEY (conversation_id, actor_id)\n);\n\nCREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,\n actor_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n origin_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n class TEXT NOT NULL,\n body TEXT NOT NULL,\n reply_to_message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,\n thread_conversation_id TEXT REFERENCES conversations(id) ON DELETE SET NULL,\n speech_json TEXT,\n audience_json TEXT,\n visibility TEXT NOT NULL,\n policy TEXT NOT NULL,\n metadata_json TEXT,\n created_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS message_mentions (\n message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,\n actor_id TEXT NOT NULL REFERENCES actors(id) ON DELETE CASCADE,\n label TEXT,\n PRIMARY KEY (message_id, actor_id)\n);\n\nCREATE TABLE IF NOT EXISTS message_attachments (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,\n media_type TEXT NOT NULL,\n file_name TEXT,\n blob_key TEXT,\n url TEXT,\n metadata_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS invocations (\n id TEXT PRIMARY KEY,\n requester_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n requester_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n target_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,\n target_node_id TEXT REFERENCES nodes(id) ON DELETE SET NULL,\n action TEXT NOT NULL,\n task TEXT NOT NULL,\n conversation_id TEXT REFERENCES conversations(id) ON DELETE SET NULL,\n message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,\n context_json TEXT,\n execution_json TEXT,\n ensure_awake INTEGER NOT NULL DEFAULT 1,\n stream INTEGER NOT NULL DEFAULT 1,\n timeout_ms INTEGER,\n metadata_json TEXT,\n created_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS flights (\n id TEXT PRIMARY KEY,\n invocation_id TEXT NOT NULL REFERENCES invocations(id) ON DELETE CASCADE,\n requester_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n target_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE RESTRICT,\n state TEXT NOT NULL,\n summary TEXT,\n output TEXT,\n error TEXT,\n metadata_json TEXT,\n started_at INTEGER,\n completed_at INTEGER\n);\n\nCREATE TABLE IF NOT EXISTS bindings (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,\n platform TEXT NOT NULL,\n mode TEXT NOT NULL,\n external_channel_id TEXT NOT NULL,\n external_thread_id TEXT,\n metadata_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS deliveries (\n id TEXT PRIMARY KEY,\n message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,\n invocation_id TEXT REFERENCES invocations(id) ON DELETE CASCADE,\n target_id TEXT NOT NULL,\n target_node_id TEXT REFERENCES nodes(id) ON DELETE SET NULL,\n target_kind TEXT NOT NULL,\n transport TEXT NOT NULL,\n reason TEXT NOT NULL,\n policy TEXT NOT NULL,\n status TEXT NOT NULL,\n binding_id TEXT REFERENCES bindings(id) ON DELETE SET NULL,\n lease_owner TEXT,\n lease_expires_at INTEGER,\n metadata_json TEXT,\n created_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\nCREATE TABLE IF NOT EXISTS delivery_attempts (\n id TEXT PRIMARY KEY,\n delivery_id TEXT NOT NULL REFERENCES deliveries(id) ON DELETE CASCADE,\n attempt INTEGER NOT NULL,\n status TEXT NOT NULL,\n error TEXT,\n external_ref TEXT,\n metadata_json TEXT,\n created_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS collaboration_records (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n state TEXT NOT NULL,\n acceptance_state TEXT NOT NULL,\n title TEXT NOT NULL,\n summary TEXT,\n created_by_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n owner_id TEXT REFERENCES actors(id) ON DELETE SET NULL,\n next_move_owner_id TEXT REFERENCES actors(id) ON DELETE SET NULL,\n conversation_id TEXT REFERENCES conversations(id) ON DELETE SET NULL,\n parent_id TEXT REFERENCES collaboration_records(id) ON DELETE SET NULL,\n priority TEXT,\n labels_json TEXT,\n relations_json TEXT,\n detail_json TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS collaboration_events (\n id TEXT PRIMARY KEY,\n record_id TEXT NOT NULL REFERENCES collaboration_records(id) ON DELETE CASCADE,\n record_kind TEXT NOT NULL,\n kind TEXT NOT NULL,\n actor_id TEXT NOT NULL REFERENCES actors(id) ON DELETE RESTRICT,\n summary TEXT,\n metadata_json TEXT,\n created_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n node_id TEXT,\n ts INTEGER NOT NULL,\n payload_json TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS scout_dispatches (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n asked_label TEXT NOT NULL,\n detail TEXT NOT NULL,\n invocation_id TEXT,\n conversation_id TEXT,\n requester_id TEXT,\n dispatcher_node_id TEXT NOT NULL,\n dispatched_at INTEGER NOT NULL,\n payload_json TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS activity_items (\n id TEXT PRIMARY KEY,\n kind TEXT NOT NULL,\n ts INTEGER NOT NULL,\n conversation_id TEXT REFERENCES conversations(id) ON DELETE CASCADE,\n message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,\n invocation_id TEXT REFERENCES invocations(id) ON DELETE CASCADE,\n flight_id TEXT REFERENCES flights(id) ON DELETE CASCADE,\n record_id TEXT REFERENCES collaboration_records(id) ON DELETE CASCADE,\n actor_id TEXT REFERENCES actors(id) ON DELETE SET NULL,\n counterpart_id TEXT REFERENCES actors(id) ON DELETE SET NULL,\n agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,\n workspace_root TEXT,\n session_id TEXT,\n title TEXT,\n summary TEXT,\n payload_json TEXT\n);\n\nCREATE INDEX IF NOT EXISTS idx_nodes_mesh_id\n ON nodes (mesh_id);\nCREATE INDEX IF NOT EXISTS idx_agent_endpoints_agent_updated_at\n ON agent_endpoints (agent_id, updated_at DESC);\nCREATE INDEX IF NOT EXISTS idx_messages_conversation_created_at\n ON messages (conversation_id, created_at);\nCREATE INDEX IF NOT EXISTS idx_invocations_target_created_at\n ON invocations (target_agent_id, created_at);\nCREATE INDEX IF NOT EXISTS idx_flights_target_state\n ON flights (target_agent_id, state);\nCREATE INDEX IF NOT EXISTS idx_deliveries_status_transport\n ON deliveries (status, transport);\nCREATE INDEX IF NOT EXISTS idx_collaboration_records_state\n ON collaboration_records (state);\nCREATE INDEX IF NOT EXISTS idx_collaboration_records_updated_at\n ON collaboration_records (updated_at);\nCREATE INDEX IF NOT EXISTS idx_collaboration_events_record_created_at\n ON collaboration_events (record_id, created_at);\nCREATE INDEX IF NOT EXISTS idx_events_kind_ts\n ON events (kind, ts);\nCREATE INDEX IF NOT EXISTS idx_activity_items_agent_ts\n ON activity_items (agent_id, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_actor_ts\n ON activity_items (actor_id, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_conversation_ts\n ON activity_items (conversation_id, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_workspace_ts\n ON activity_items (workspace_root, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_kind_ts\n ON activity_items (kind, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_items_session_ts\n ON activity_items (session_id, ts DESC);\nCREATE INDEX IF NOT EXISTS idx_scout_dispatches_dispatched_at\n ON scout_dispatches (dispatched_at DESC);\nCREATE INDEX IF NOT EXISTS idx_scout_dispatches_conversation_ts\n ON scout_dispatches (conversation_id, dispatched_at DESC);\n";
package/dist/schema.js CHANGED
@@ -228,6 +228,19 @@ CREATE TABLE IF NOT EXISTS events (
228
228
  payload_json TEXT NOT NULL
229
229
  );
230
230
 
231
+ CREATE TABLE IF NOT EXISTS scout_dispatches (
232
+ id TEXT PRIMARY KEY,
233
+ kind TEXT NOT NULL,
234
+ asked_label TEXT NOT NULL,
235
+ detail TEXT NOT NULL,
236
+ invocation_id TEXT,
237
+ conversation_id TEXT,
238
+ requester_id TEXT,
239
+ dispatcher_node_id TEXT NOT NULL,
240
+ dispatched_at INTEGER NOT NULL,
241
+ payload_json TEXT NOT NULL
242
+ );
243
+
231
244
  CREATE TABLE IF NOT EXISTS activity_items (
232
245
  id TEXT PRIMARY KEY,
233
246
  kind TEXT NOT NULL,
@@ -279,4 +292,8 @@ CREATE INDEX IF NOT EXISTS idx_activity_items_kind_ts
279
292
  ON activity_items (kind, ts DESC);
280
293
  CREATE INDEX IF NOT EXISTS idx_activity_items_session_ts
281
294
  ON activity_items (session_id, ts DESC);
295
+ CREATE INDEX IF NOT EXISTS idx_scout_dispatches_dispatched_at
296
+ ON scout_dispatches (dispatched_at DESC);
297
+ CREATE INDEX IF NOT EXISTS idx_scout_dispatches_conversation_ts
298
+ ON scout_dispatches (conversation_id, dispatched_at DESC);
282
299
  `;