@pinet/broker-core 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/schema.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
2
  import type { PinetMailClass } from "./mail-classification.js";
3
- import type { AgentInfo, ThreadInfo, BrokerMessage, InboxEntry, InboxReadOptions, InboxReadResult, InboxThreadUnreadSummary, DeliveredInboundMessageResult, BacklogEntry, BrokerDBInterface, InboundMessage, ChannelAssignment, TaskAssignmentInfo, TaskAssignmentKind, TaskAssignmentStatus, ScheduledWakeupInfo, ScheduledWakeupDelivery, PortLeaseAcquireInput, PortLeaseInfo, PortLeaseListOptions, PortLeaseReleaseInput, PortLeaseRenewInput, PinetLaneInfo, PinetLaneListOptions, PinetLaneParticipantInfo, PinetLaneParticipantUpsertInput, PinetLaneUpsertInput } from "./types.js";
3
+ import type { AgentInfo, ThreadInfo, BrokerMessage, InboxEntry, InboxReadOptions, InboxReadResult, InboxThreadUnreadSummary, DeliveredInboundMessageResult, BacklogEntry, BrokerDBInterface, InboundMessage, ChannelAssignment, TaskAssignmentInfo, TaskAssignmentKind, TaskAssignmentStatus, ScheduledWakeupInfo, ScheduledWakeupDelivery, PortLeaseAcquireInput, PortLeaseInfo, PortLeaseListOptions, PortLeaseReleaseInput, PortLeaseRenewInput, AgentSessionSearchInfo, AgentSessionSearchOptions, PinetLaneInfo, PinetLaneListOptions, PinetLaneParticipantInfo, PinetLaneParticipantUpsertInput, PinetLaneUpsertInput } from "./types.js";
4
4
  export interface TaskAssignmentAwaitingReplyInfo {
5
5
  id: number;
6
6
  agentId: string;
@@ -40,6 +40,8 @@ export declare class BrokerDB implements BrokerDBInterface {
40
40
  private rowToAgentWithCurrentSessionOutboundCount;
41
41
  getAgents(): AgentInfo[];
42
42
  getAllAgents(): AgentInfo[];
43
+ private getAgentRelatedThreadIds;
44
+ searchAgentSessions(options?: AgentSessionSearchOptions): AgentSessionSearchInfo[];
43
45
  getSetting<T = unknown>(key: string): T | null;
44
46
  setSetting(key: string, value: unknown): void;
45
47
  deleteSetting(key: string): void;
package/dist/schema.js CHANGED
@@ -315,6 +315,101 @@ function getOptionalMetadataString(metadata, keys) {
315
315
  }
316
316
  return null;
317
317
  }
318
+ function getOptionalNestedMetadataString(metadata, keys) {
319
+ const direct = getOptionalMetadataString(metadata ?? undefined, keys);
320
+ if (direct)
321
+ return direct;
322
+ const capabilities = metadata?.capabilities &&
323
+ typeof metadata.capabilities === "object" &&
324
+ !Array.isArray(metadata.capabilities)
325
+ ? metadata.capabilities
326
+ : undefined;
327
+ return getOptionalMetadataString(capabilities, keys);
328
+ }
329
+ function normalizeSessionSearchNeedle(value) {
330
+ const trimmed = value?.trim();
331
+ return trimmed && trimmed.length > 0 ? trimmed.toLowerCase() : null;
332
+ }
333
+ function matchesSessionSearchNeedle(value, needle) {
334
+ if (!needle)
335
+ return true;
336
+ return Boolean(value?.toLowerCase().includes(needle));
337
+ }
338
+ function matchesSessionSearchPrefixOrExact(value, needle) {
339
+ if (!needle)
340
+ return true;
341
+ const normalized = value?.toLowerCase();
342
+ return Boolean(normalized && (normalized === needle || normalized.startsWith(needle)));
343
+ }
344
+ function parseSessionSearchTime(value) {
345
+ if (!value)
346
+ return null;
347
+ const parsed = Date.parse(value);
348
+ return Number.isNaN(parsed) ? null : parsed;
349
+ }
350
+ function normalizeSessionSearchLimit(value) {
351
+ if (typeof value !== "number" || !Number.isFinite(value))
352
+ return 20;
353
+ return Math.max(1, Math.min(100, Math.floor(value)));
354
+ }
355
+ function agentSessionOverlapsRange(agent, sinceMs, untilMs) {
356
+ const connectedMs = Date.parse(agent.connectedAt);
357
+ const lastSeenMs = Date.parse(agent.lastSeen || agent.lastHeartbeat || agent.connectedAt);
358
+ const startMs = Number.isNaN(connectedMs) ? null : connectedMs;
359
+ const endMs = Number.isNaN(lastSeenMs) ? startMs : lastSeenMs;
360
+ if (sinceMs !== null && endMs !== null && endMs < sinceMs)
361
+ return false;
362
+ if (untilMs !== null && startMs !== null && startMs > untilMs)
363
+ return false;
364
+ return true;
365
+ }
366
+ function getAgentSessionMatchedBy(input) {
367
+ const matchedBy = [];
368
+ const agentName = normalizeSessionSearchNeedle(input.options.agentName);
369
+ const agentId = normalizeSessionSearchNeedle(input.options.agentId);
370
+ const threadId = normalizeSessionSearchNeedle(input.options.threadId);
371
+ const repo = normalizeSessionSearchNeedle(input.options.repo);
372
+ const worktreePath = normalizeSessionSearchNeedle(input.options.worktreePath);
373
+ const tmuxSession = normalizeSessionSearchNeedle(input.options.tmuxSession);
374
+ if (agentName && matchesSessionSearchNeedle(input.agent.name, agentName)) {
375
+ matchedBy.push("agent_name");
376
+ }
377
+ if (agentId && matchesSessionSearchPrefixOrExact(input.agent.id, agentId)) {
378
+ matchedBy.push("agent_id");
379
+ }
380
+ if (threadId &&
381
+ input.relatedThreadIds.some((candidate) => candidate.toLowerCase().includes(threadId))) {
382
+ matchedBy.push("thread_id");
383
+ }
384
+ const repoValues = [
385
+ getOptionalNestedMetadataString(input.metadata, ["repo"]),
386
+ getOptionalNestedMetadataString(input.metadata, ["repoRoot"]),
387
+ getOptionalNestedMetadataString(input.metadata, ["cwd"]),
388
+ ];
389
+ if (repo && repoValues.some((value) => matchesSessionSearchNeedle(value, repo))) {
390
+ matchedBy.push("repo");
391
+ }
392
+ const worktreeValues = [
393
+ getOptionalNestedMetadataString(input.metadata, ["worktreePath"]),
394
+ getOptionalNestedMetadataString(input.metadata, ["cwd"]),
395
+ getOptionalNestedMetadataString(input.metadata, ["repoRoot"]),
396
+ ];
397
+ if (worktreePath &&
398
+ worktreeValues.some((value) => matchesSessionSearchNeedle(value, worktreePath))) {
399
+ matchedBy.push("worktree_path");
400
+ }
401
+ const tmux = getOptionalNestedMetadataString(input.metadata, ["tmuxSession", "tmux"]);
402
+ if (tmuxSession && matchesSessionSearchNeedle(tmux, tmuxSession)) {
403
+ matchedBy.push("tmux_session");
404
+ }
405
+ if (input.options.since || input.options.until) {
406
+ matchedBy.push("time_range");
407
+ }
408
+ if (matchedBy.length === 0) {
409
+ matchedBy.push("recent");
410
+ }
411
+ return matchedBy;
412
+ }
318
413
  function rowToPinetLaneParticipant(row) {
319
414
  return {
320
415
  laneId: row.lane_id,
@@ -1470,6 +1565,137 @@ export class BrokerDB {
1470
1565
  .all();
1471
1566
  return rows.map((row) => this.rowToAgentWithCurrentSessionOutboundCount(row));
1472
1567
  }
1568
+ getAgentRelatedThreadIds(agentId, limit = 12) {
1569
+ const db = this.getDb();
1570
+ const rows = db
1571
+ .prepare(`SELECT thread_id, MAX(activity_at) AS activity_at
1572
+ FROM (
1573
+ SELECT thread_id, updated_at AS activity_at
1574
+ FROM threads
1575
+ WHERE owner_agent = ? OR channel = ?
1576
+ UNION ALL
1577
+ SELECT thread_id, created_at AS activity_at
1578
+ FROM messages
1579
+ WHERE sender = ?
1580
+ UNION ALL
1581
+ SELECT m.thread_id, i.created_at AS activity_at
1582
+ FROM inbox i
1583
+ JOIN messages m ON m.id = i.message_id
1584
+ WHERE i.agent_id = ?
1585
+ UNION ALL
1586
+ SELECT thread_id, updated_at AS activity_at
1587
+ FROM threads
1588
+ WHERE thread_id LIKE ? OR thread_id LIKE ?
1589
+ ) related
1590
+ GROUP BY thread_id
1591
+ ORDER BY activity_at DESC
1592
+ LIMIT ?`)
1593
+ .all(agentId, `agent:${agentId}`, agentId, agentId, `a2a:${agentId}:%`, `a2a:%:${agentId}`, limit);
1594
+ return rows.map((row) => row.thread_id);
1595
+ }
1596
+ searchAgentSessions(options = {}) {
1597
+ const agentName = normalizeSessionSearchNeedle(options.agentName);
1598
+ const agentId = normalizeSessionSearchNeedle(options.agentId);
1599
+ const threadId = normalizeSessionSearchNeedle(options.threadId);
1600
+ const repo = normalizeSessionSearchNeedle(options.repo);
1601
+ const worktreePath = normalizeSessionSearchNeedle(options.worktreePath);
1602
+ const tmuxSession = normalizeSessionSearchNeedle(options.tmuxSession);
1603
+ const sinceMs = parseSessionSearchTime(options.since);
1604
+ const untilMs = parseSessionSearchTime(options.until);
1605
+ const limit = normalizeSessionSearchLimit(options.limit);
1606
+ const results = this.getAllAgents()
1607
+ .map((agent) => {
1608
+ const metadata = agent.metadata ?? null;
1609
+ const relatedThreadIds = this.getAgentRelatedThreadIds(agent.id);
1610
+ const matchedBy = getAgentSessionMatchedBy({ agent, metadata, relatedThreadIds, options });
1611
+ return {
1612
+ agent,
1613
+ metadata,
1614
+ relatedThreadIds,
1615
+ matchedBy,
1616
+ lastSeenMs: Date.parse(agent.lastSeen || agent.lastHeartbeat || agent.connectedAt),
1617
+ };
1618
+ })
1619
+ .filter(({ agent, metadata, relatedThreadIds }) => {
1620
+ if (agentName && !matchesSessionSearchNeedle(agent.name, agentName))
1621
+ return false;
1622
+ if (agentId && !matchesSessionSearchPrefixOrExact(agent.id, agentId))
1623
+ return false;
1624
+ if (threadId &&
1625
+ !relatedThreadIds.some((candidate) => candidate.toLowerCase().includes(threadId))) {
1626
+ return false;
1627
+ }
1628
+ if (repo) {
1629
+ const values = [
1630
+ getOptionalNestedMetadataString(metadata, ["repo"]),
1631
+ getOptionalNestedMetadataString(metadata, ["repoRoot"]),
1632
+ getOptionalNestedMetadataString(metadata, ["cwd"]),
1633
+ ];
1634
+ if (!values.some((value) => matchesSessionSearchNeedle(value, repo)))
1635
+ return false;
1636
+ }
1637
+ if (worktreePath) {
1638
+ const values = [
1639
+ getOptionalNestedMetadataString(metadata, ["worktreePath"]),
1640
+ getOptionalNestedMetadataString(metadata, ["cwd"]),
1641
+ getOptionalNestedMetadataString(metadata, ["repoRoot"]),
1642
+ ];
1643
+ if (!values.some((value) => matchesSessionSearchNeedle(value, worktreePath))) {
1644
+ return false;
1645
+ }
1646
+ }
1647
+ if (tmuxSession) {
1648
+ const tmux = getOptionalNestedMetadataString(metadata, ["tmuxSession", "tmux"]);
1649
+ if (!matchesSessionSearchNeedle(tmux, tmuxSession))
1650
+ return false;
1651
+ }
1652
+ return agentSessionOverlapsRange(agent, sinceMs, untilMs);
1653
+ })
1654
+ .sort((left, right) => {
1655
+ const liveDelta = Number(Boolean(left.agent.disconnectedAt)) - Number(Boolean(right.agent.disconnectedAt));
1656
+ if (liveDelta !== 0)
1657
+ return liveDelta;
1658
+ const leftSeen = Number.isNaN(left.lastSeenMs) ? 0 : left.lastSeenMs;
1659
+ const rightSeen = Number.isNaN(right.lastSeenMs) ? 0 : right.lastSeenMs;
1660
+ if (leftSeen !== rightSeen)
1661
+ return rightSeen - leftSeen;
1662
+ return left.agent.name.localeCompare(right.agent.name);
1663
+ })
1664
+ .slice(0, limit)
1665
+ .map(({ agent, metadata, relatedThreadIds, matchedBy }) => ({
1666
+ agentId: agent.id,
1667
+ agentName: agent.name,
1668
+ emoji: agent.emoji,
1669
+ pid: agent.pid,
1670
+ status: agent.status,
1671
+ stableId: agent.stableId ?? null,
1672
+ connectedAt: agent.connectedAt,
1673
+ lastSeen: agent.lastSeen,
1674
+ lastHeartbeat: agent.lastHeartbeat,
1675
+ disconnectedAt: agent.disconnectedAt ?? null,
1676
+ resumableUntil: agent.resumableUntil ?? null,
1677
+ idleSince: agent.idleSince ?? null,
1678
+ lastActivity: agent.lastActivity ?? null,
1679
+ cwd: getOptionalNestedMetadataString(metadata, ["cwd"]),
1680
+ repo: getOptionalNestedMetadataString(metadata, ["repo"]),
1681
+ repoRoot: getOptionalNestedMetadataString(metadata, ["repoRoot"]),
1682
+ worktreePath: getOptionalNestedMetadataString(metadata, ["worktreePath"]),
1683
+ branch: getOptionalNestedMetadataString(metadata, ["branch"]),
1684
+ tmuxSession: getOptionalNestedMetadataString(metadata, ["tmuxSession", "tmux"]),
1685
+ brokerManaged: metadata?.brokerManaged === true,
1686
+ brokerManagedBy: getOptionalNestedMetadataString(metadata, ["brokerManagedBy"]),
1687
+ launchSource: getOptionalNestedMetadataString(metadata, ["launchSource"]),
1688
+ parentAgentId: agent.parentAgentId ?? null,
1689
+ rootAgentId: agent.rootAgentId ?? null,
1690
+ treeDepth: agent.treeDepth ?? 0,
1691
+ supervisionState: agent.supervisionState ?? "root",
1692
+ subtreeRole: agent.subtreeRole ?? null,
1693
+ laneId: agent.laneId ?? null,
1694
+ relatedThreadIds,
1695
+ matchedBy,
1696
+ }));
1697
+ return results;
1698
+ }
1473
1699
  getSetting(key) {
1474
1700
  const db = this.getDb();
1475
1701
  const row = db.prepare("SELECT value FROM settings WHERE key = ?").get(key);
package/dist/types.d.ts CHANGED
@@ -27,7 +27,61 @@ export interface AgentInfo {
27
27
  outboundCount?: number;
28
28
  pendingInboxCount?: number;
29
29
  }
30
- export type ClientAgentInfo = Omit<AgentInfo, "stableId">;
30
+ export type AgentSessionKind = "session" | "leaf" | "cwd" | "broker" | "unknown";
31
+ export interface AgentSessionSummary {
32
+ kind: AgentSessionKind;
33
+ /** Broker-safe, path-free stable session reference such as "session:1a2b3c4d5e6f". */
34
+ ref: string;
35
+ host?: string | null;
36
+ hasPath?: boolean;
37
+ }
38
+ export type ClientAgentInfo = Omit<AgentInfo, "stableId"> & {
39
+ /** Redacted session indicator. Raw stableId/session paths are intentionally not exposed here. */
40
+ session?: AgentSessionSummary | null;
41
+ };
42
+ export interface AgentSessionSearchOptions {
43
+ agentName?: string;
44
+ agentId?: string;
45
+ threadId?: string;
46
+ repo?: string;
47
+ worktreePath?: string;
48
+ tmuxSession?: string;
49
+ since?: string;
50
+ until?: string;
51
+ limit?: number;
52
+ }
53
+ export interface AgentSessionSearchInfo {
54
+ agentId: string;
55
+ agentName: string;
56
+ emoji: string;
57
+ pid: number;
58
+ status: "working" | "idle";
59
+ stableId: string | null;
60
+ connectedAt: string;
61
+ lastSeen: string;
62
+ lastHeartbeat: string;
63
+ disconnectedAt: string | null;
64
+ resumableUntil: string | null;
65
+ idleSince: string | null;
66
+ lastActivity: string | null;
67
+ cwd: string | null;
68
+ repo: string | null;
69
+ repoRoot: string | null;
70
+ worktreePath: string | null;
71
+ branch: string | null;
72
+ tmuxSession: string | null;
73
+ brokerManaged: boolean;
74
+ brokerManagedBy: string | null;
75
+ launchSource: string | null;
76
+ parentAgentId: string | null;
77
+ rootAgentId: string | null;
78
+ treeDepth: number;
79
+ supervisionState: AgentSupervisionState;
80
+ subtreeRole: string | null;
81
+ laneId: string | null;
82
+ relatedThreadIds: string[];
83
+ matchedBy: string[];
84
+ }
31
85
  export interface ThreadInfo {
32
86
  threadId: string;
33
87
  source: string;
@@ -296,6 +350,7 @@ export interface BrokerDBInterface {
296
350
  getAgentById(agentId: string): AgentInfo | null;
297
351
  getAgentByStableId(stableId: string): AgentInfo | null;
298
352
  getAgents(): AgentInfo[];
353
+ searchAgentSessions?(options?: AgentSessionSearchOptions): AgentSessionSearchInfo[];
299
354
  getChannelAssignment(channel: string): ChannelAssignment | null;
300
355
  acquirePortLease?(input: PortLeaseAcquireInput): PortLeaseInfo;
301
356
  renewPortLease?(input: PortLeaseRenewInput): PortLeaseInfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinet/broker-core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Transport-neutral broker kernel primitives for pi transports",
6
6
  "author": "Will Porcellini <5994936+gugu91@users.noreply.github.com>",
@@ -43,7 +43,7 @@
43
43
  "test": "vitest run *.test.ts"
44
44
  },
45
45
  "dependencies": {
46
- "@pinet/transport-core": "0.2.0"
46
+ "@pinet/transport-core": "0.2.2"
47
47
  },
48
48
  "types": "./dist/index.d.ts"
49
49
  }