@huynle/hub 0.7.4 → 0.8.1

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 (241) hide show
  1. package/CHANGELOG.md +430 -1
  2. package/Dockerfile +7 -2
  3. package/README.md +230 -32
  4. package/package.json +5 -3
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/claude-code/src/daemon.ts +8 -1
  7. package/packages/claude-code/src/launcher.ts +29 -9
  8. package/packages/claude-code/src/paths.ts +3 -3
  9. package/packages/claude-code/src/profile.ts +173 -0
  10. package/packages/claude-code/src/sessions.ts +22 -4
  11. package/packages/cli/dist/hub-cli.js +6871 -3460
  12. package/packages/cli/package.json +1 -1
  13. package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
  14. package/packages/cli/src/artifacts-mcp.ts +160 -0
  15. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +2 -2
  16. package/packages/cli/src/bootstrap/discovery/host.ts +15 -1
  17. package/packages/cli/src/bootstrap/discovery/probe.ts +1 -1
  18. package/packages/cli/src/bootstrap/enrollment/enroll.ts +21 -9
  19. package/packages/cli/src/bootstrap/enrollment/policy.ts +138 -12
  20. package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
  21. package/packages/cli/src/cli.ts +96 -14
  22. package/packages/cli/src/format.ts +48 -10
  23. package/packages/cli/src/http.ts +2 -0
  24. package/packages/cli/src/intents.ts +23 -0
  25. package/packages/cli/src/launches.ts +9 -3
  26. package/packages/cli/src/login.ts +20 -6
  27. package/packages/cli/src/mcp.ts +385 -10
  28. package/packages/cli/src/messaging.ts +7 -3
  29. package/packages/cli/src/profiles-mcp.ts +301 -0
  30. package/packages/cli/src/setup/commands.ts +23 -4
  31. package/packages/cli/src/setup/runtime.ts +3 -2
  32. package/packages/cli/src/spoke-services.ts +1 -1
  33. package/packages/cli/src/team-intake-mcp.ts +158 -0
  34. package/packages/cli/src/teams-mcp.ts +460 -0
  35. package/packages/cli/src/watchlist.ts +135 -0
  36. package/packages/codex/package.json +1 -1
  37. package/packages/codex/src/paths.ts +3 -3
  38. package/packages/codex/src/profile.ts +163 -0
  39. package/packages/codex/src/sessions.ts +12 -0
  40. package/packages/codex/src/spoke.ts +19 -1
  41. package/packages/node/package.json +1 -1
  42. package/packages/node/src/config.ts +104 -2
  43. package/packages/node/src/opencode-warm.ts +344 -0
  44. package/packages/node/src/run.ts +28 -1
  45. package/packages/preset/package.json +1 -1
  46. package/packages/preset/src/profile.ts +34 -0
  47. package/packages/preset/src/spoke.ts +4 -0
  48. package/packages/protocol/package.json +1 -1
  49. package/packages/protocol/src/artifacts.ts +142 -0
  50. package/packages/protocol/src/concierge.ts +366 -0
  51. package/packages/protocol/src/contracts.ts +104 -0
  52. package/packages/protocol/src/graph.ts +70 -0
  53. package/packages/protocol/src/hub-workspace.ts +201 -0
  54. package/packages/protocol/src/index.ts +175 -6
  55. package/packages/protocol/src/launch.ts +36 -0
  56. package/packages/protocol/src/profiles.ts +301 -0
  57. package/packages/protocol/src/teams.ts +204 -0
  58. package/packages/runtime-openshell/package.json +1 -1
  59. package/packages/runtime-sdk/package.json +1 -1
  60. package/packages/sdk/package.json +1 -1
  61. package/packages/sdk/src/address.ts +50 -2
  62. package/packages/sdk/src/artifact-jobs.ts +194 -0
  63. package/packages/sdk/src/artifacts.ts +187 -0
  64. package/packages/sdk/src/client.ts +226 -5
  65. package/packages/sdk/src/concierge.ts +88 -0
  66. package/packages/sdk/src/errors.ts +4 -1
  67. package/packages/sdk/src/index.ts +6 -0
  68. package/packages/sdk/src/team-intake.ts +122 -0
  69. package/packages/sdk/src/teams.ts +346 -0
  70. package/packages/sdk/src/types.ts +568 -1
  71. package/packages/sdk/src/wake.ts +20 -0
  72. package/packages/spoke-sdk/package.json +1 -1
  73. package/packages/spoke-sdk/src/index.ts +1 -0
  74. package/packages/spoke-sdk/src/profile-apply.ts +245 -0
  75. package/src/central/actions.ts +7 -0
  76. package/src/central/agent/acting-routes.ts +2 -1
  77. package/src/central/agent/acting-switch.ts +2 -1
  78. package/src/central/agent/acting.ts +218 -10
  79. package/src/central/agent/app-tokens.ts +3 -2
  80. package/src/central/agent/audit.ts +18 -1
  81. package/src/central/agent/body.ts +163 -0
  82. package/src/central/agent/decisions.ts +125 -2
  83. package/src/central/agent/delivery.ts +194 -40
  84. package/src/central/agent/directory-routes.ts +37 -7
  85. package/src/central/agent/directory.ts +70 -14
  86. package/src/central/agent/errors.ts +7 -0
  87. package/src/central/agent/escalation.ts +10 -2
  88. package/src/central/agent/events-routes.ts +21 -4
  89. package/src/central/agent/events.ts +18 -4
  90. package/src/central/agent/form-check.ts +85 -0
  91. package/src/central/agent/grant-requests.ts +2 -1
  92. package/src/central/agent/grant-routes.ts +4 -3
  93. package/src/central/agent/grants.ts +29 -2
  94. package/src/central/agent/idempotency.ts +133 -0
  95. package/src/central/agent/intent-routes.ts +116 -0
  96. package/src/central/agent/intents.ts +505 -0
  97. package/src/central/agent/interaction-graph-routes.ts +212 -0
  98. package/src/central/agent/interaction-graph.ts +865 -0
  99. package/src/central/agent/launch-engine.ts +189 -32
  100. package/src/central/agent/launch-routes.ts +26 -10
  101. package/src/central/agent/launch-spec.ts +40 -3
  102. package/src/central/agent/launch-store.ts +55 -2
  103. package/src/central/agent/message-limits.ts +89 -11
  104. package/src/central/agent/message-routes.ts +10 -4
  105. package/src/central/agent/messages.ts +28 -2
  106. package/src/central/agent/names.ts +12 -2
  107. package/src/central/agent/placement.ts +93 -18
  108. package/src/central/agent/read-routes.ts +89 -19
  109. package/src/central/agent/routes.ts +178 -5
  110. package/src/central/agent/webhook-routes.ts +2 -1
  111. package/src/central/agent/webhooks.ts +17 -2
  112. package/src/central/agent-data.ts +4 -0
  113. package/src/central/artifacts/access.ts +44 -0
  114. package/src/central/artifacts/blob-store.ts +113 -0
  115. package/src/central/artifacts/blob-support.ts +98 -0
  116. package/src/central/artifacts/events.ts +27 -0
  117. package/src/central/artifacts/host-caps.ts +162 -0
  118. package/src/central/artifacts/host.ts +315 -0
  119. package/src/central/artifacts/idempotency.ts +109 -0
  120. package/src/central/artifacts/jobs-routes.ts +776 -0
  121. package/src/central/artifacts/jobs.ts +344 -0
  122. package/src/central/artifacts/maintenance.ts +103 -0
  123. package/src/central/artifacts/routes.ts +360 -0
  124. package/src/central/artifacts/store.ts +776 -0
  125. package/src/central/artifacts/workspaces.ts +31 -0
  126. package/src/central/backend/file-blobs.ts +377 -0
  127. package/src/central/backend/index.ts +12 -3
  128. package/src/central/backend/memory-blobs.ts +116 -0
  129. package/src/central/backend/postgres-blobs.ts +513 -0
  130. package/src/central/backend/postgres-migrate.ts +33 -0
  131. package/src/central/backend/postgres-state.ts +19 -0
  132. package/src/central/backend/types.ts +8 -0
  133. package/src/central/concierge/activity.ts +142 -0
  134. package/src/central/concierge/arm.ts +63 -0
  135. package/src/central/concierge/board-render.ts +190 -0
  136. package/src/central/concierge/board-results.ts +169 -0
  137. package/src/central/concierge/board-routes.ts +350 -0
  138. package/src/central/concierge/board.ts +249 -0
  139. package/src/central/concierge/context.ts +221 -0
  140. package/src/central/concierge/entitlements.ts +13 -0
  141. package/src/central/concierge/guard.ts +120 -0
  142. package/src/central/concierge/inbound.ts +86 -0
  143. package/src/central/concierge/index.ts +84 -0
  144. package/src/central/concierge/notes.ts +41 -0
  145. package/src/central/concierge/notifier.ts +986 -0
  146. package/src/central/concierge/prompt.ts +131 -0
  147. package/src/central/concierge/proposals.ts +268 -0
  148. package/src/central/concierge/push-remap.ts +49 -0
  149. package/src/central/concierge/routes.ts +122 -0
  150. package/src/central/concierge/schedule.ts +113 -0
  151. package/src/central/concierge/service.ts +243 -0
  152. package/src/central/concierge/session-routes.ts +125 -0
  153. package/src/central/concierge/settings.ts +217 -0
  154. package/src/central/concierge/supervisor.ts +767 -0
  155. package/src/central/concierge/tiers.ts +260 -0
  156. package/src/central/concierge/watchlist-routes.ts +279 -0
  157. package/src/central/concierge/watchlist.ts +273 -0
  158. package/src/central/config.ts +28 -6
  159. package/src/central/http.ts +660 -54
  160. package/src/central/main.ts +1 -1
  161. package/src/central/mcp-http.ts +10 -21
  162. package/src/central/profiles/apply.ts +333 -0
  163. package/src/central/profiles/content.ts +174 -0
  164. package/src/central/profiles/events.ts +40 -0
  165. package/src/central/profiles/idempotency.ts +14 -0
  166. package/src/central/profiles/import.ts +139 -0
  167. package/src/central/profiles/routes.ts +356 -0
  168. package/src/central/profiles/store.ts +597 -0
  169. package/src/central/profiles/usage-ports.ts +120 -0
  170. package/src/central/profiles/usage.ts +364 -0
  171. package/src/central/push.ts +7 -0
  172. package/src/central/registry.ts +69 -7
  173. package/src/central/store.ts +4 -0
  174. package/src/central/teams/collections.ts +34 -0
  175. package/src/central/teams/discovery.ts +346 -0
  176. package/src/central/teams/draft-edits.ts +46 -0
  177. package/src/central/teams/events.ts +46 -0
  178. package/src/central/teams/intake-routes.ts +100 -0
  179. package/src/central/teams/intake.ts +786 -0
  180. package/src/central/teams/policy.ts +231 -0
  181. package/src/central/teams/routes.ts +516 -0
  182. package/src/central/teams/seats.ts +272 -0
  183. package/src/central/teams/staffing.ts +910 -0
  184. package/src/central/teams/store.ts +667 -0
  185. package/src/central/teams/validation.ts +256 -0
  186. package/src/central/workspaces/board.ts +408 -0
  187. package/src/central/workspaces/checkpoints.ts +316 -0
  188. package/src/central/workspaces/coordinator.ts +841 -0
  189. package/src/central/workspaces/events.ts +85 -0
  190. package/src/central/workspaces/lifecycle.ts +93 -0
  191. package/src/central/workspaces/master.ts +490 -0
  192. package/src/central/workspaces/members.ts +632 -0
  193. package/src/central/workspaces/routes.ts +354 -0
  194. package/src/central/workspaces/sidechats-routes.ts +410 -0
  195. package/src/central/workspaces/sidechats.ts +482 -0
  196. package/src/central/workspaces/store.ts +571 -0
  197. package/src/central/workspaces/supervision.ts +355 -0
  198. package/src/index.ts +126 -23
  199. package/src/migrate.ts +109 -3
  200. package/src/spoke/concierge-agent.ts +33 -0
  201. package/src/spoke/deliver.ts +22 -4
  202. package/src/spoke/env-inject.ts +8 -1
  203. package/src/spoke/fork-revert.ts +142 -0
  204. package/src/spoke/hub-rpc.ts +41 -5
  205. package/src/spoke/hub-tool.ts +404 -25
  206. package/src/spoke/launchers.ts +6 -4
  207. package/src/spoke/listing.ts +65 -10
  208. package/src/spoke/localhub.ts +14 -4
  209. package/src/spoke/locations.ts +40 -0
  210. package/src/spoke/profile-apply.ts +298 -0
  211. package/src/spoke/session-context.ts +256 -0
  212. package/src/spoke/spoke.ts +330 -23
  213. package/src/spoke/timeline.ts +6 -1
  214. package/src/spoke/tracker.ts +23 -1
  215. package/src/spoke/who-teams.ts +81 -0
  216. package/src/tui-messaging.ts +32 -18
  217. package/web/dist/assets/ArtifactPage-mNzjlP_U.js +1 -0
  218. package/web/dist/assets/ArtifactPane-CPQcuX7c.js +1 -0
  219. package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
  220. package/web/dist/assets/ArtifactWorkbench-NoFC5Sb2.js +6 -0
  221. package/web/dist/assets/Library-DYsFj_5c.css +1 -0
  222. package/web/dist/assets/Library-LXHsP5Fw.js +2 -0
  223. package/web/dist/assets/ProfilesDestination-DhVVcAep.js +1 -0
  224. package/web/dist/assets/TeamsPage-DBrO5hoL.js +1 -0
  225. package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
  226. package/web/dist/assets/Watchtower-DxaJYQp_.js +2 -0
  227. package/web/dist/assets/index-BabrNDB3.css +1 -0
  228. package/web/dist/assets/index-lIOfSYck.js +34 -0
  229. package/web/dist/assets/live-BDq0AABg.js +5 -0
  230. package/web/dist/assets/live-BUofsQpz.css +1 -0
  231. package/web/dist/assets/pane-layout-DWXfDU-1.js +1 -0
  232. package/web/dist/assets/plural-Bspvoqfq.js +1 -0
  233. package/web/dist/assets/renderer2d-DAr4IHM8.js +1 -0
  234. package/web/dist/assets/renderer3d-DWZj595u.js +5393 -0
  235. package/web/dist/assets/safe-area-JUb6OeI3.js +1 -0
  236. package/web/dist/assets/source-CDU04kYw.js +1 -0
  237. package/web/dist/assets/ui-Dh1-78Cl.js +3 -0
  238. package/web/dist/index.html +5 -2
  239. package/web/dist/sw.js +22 -2
  240. package/web/dist/assets/index-Dm8OmkaO.css +0 -1
  241. package/web/dist/assets/index-FUJbj_Du.js +0 -20
@@ -0,0 +1,212 @@
1
+ // The Watchtower's read API (HWA-31) over interaction-graph.ts, and the adapters that feed it from the hub's
2
+ // stores (one scan per source per request; no per-edge reads):
3
+ //
4
+ // GET /api/agent/graph?since=&until=&machine=&harness=&team=&tag=&project=&session=&workspace=&kind=&state=&limit=
5
+ // GET /api/agent/graph/edges/:id?since=&until=&cursor=&limit= message summaries of one edge (no text)
6
+ // GET /api/agent/graph/nodes/:id?since=&until=&cursor=&limit= message summaries of one node (no text)
7
+ //
8
+ // Users always; session tokens; app tokens with sessions:read (the route class) and messages:read (viewerOf).
9
+ // Full text stays behind GET /api/agent/messages/:id.
10
+ import { LIST_LIMIT_MAX } from "../../protocol"
11
+ import type { Store } from "../store"
12
+ import type { TeamStore } from "../teams/store"
13
+ import type { TeamSeatStore } from "../teams/seats"
14
+ import type { MembershipStore } from "../workspaces/members"
15
+ import type { WorkspaceStore } from "../workspaces/store"
16
+ import type { SupervisionStore } from "../workspaces/supervision"
17
+ import type { AgentAudit } from "./audit"
18
+ import { AUDIT_PAGE_MAX } from "./audit"
19
+ import type { Decisions } from "./decisions"
20
+ import type { Directory } from "./directory"
21
+ import { buildGraph, edgeDetail, type GraphDenial, type GraphLaunch, type GraphMembership, type GraphSeat, type GraphSources, type GraphTeam, nodeDetail, parseGraphQuery, viewerOf } from "./interaction-graph"
22
+ import type { Messages } from "./messages"
23
+ import type { ReadRegistry } from "./read-routes"
24
+ import { AGENT_PREFIX, type AgentRoute } from "./routes"
25
+
26
+ /** Pages read from a paged store list before giving up (bounded: the stores cap their records). */
27
+ const PAGES_MAX = 50
28
+
29
+ export interface GraphRouteDeps {
30
+ readonly store: Pick<Store, "machines" | "agentData">
31
+ readonly registry: Pick<ReadRegistry, "catalog" | "sessionTenant">
32
+ readonly directory: Pick<Directory, "who">
33
+ readonly messages: Pick<Messages, "all" | "retention">
34
+ readonly decisions?: Pick<Decisions, "list">
35
+ readonly launches?: { list(userID: string): Promise<readonly GraphLaunch[]> }
36
+ readonly teams?: { readonly teams: Pick<TeamStore, "list">; readonly seats: Pick<TeamSeatStore, "list"> }
37
+ readonly workspaces?: { readonly workspaces: Pick<WorkspaceStore, "list">; readonly members: Pick<MembershipStore, "members">; readonly supervision: Pick<SupervisionStore, "graph"> }
38
+ readonly audit?: Pick<AgentAudit, "list">
39
+ readonly now?: () => number
40
+ /** (CON-04) The user's Concierge sessions: their nodes carry `concierge: true`. */
41
+ readonly concierge?: (userID: string) => Promise<readonly string[]>
42
+ /** (CON-09a) The user's current Concierge session: labelled @@concierge; the others "@@concierge (previous)", ended. */
43
+ readonly conciergeCurrent?: (userID: string) => Promise<string | undefined>
44
+ }
45
+
46
+ interface ConciergeMarks {
47
+ readonly node: <N extends { id: string; kind: string }>(node: N) => N
48
+ /** (CON-10) Previous (retired) Concierge sessions: ended for good. */
49
+ readonly previous: ReadonlySet<string>
50
+ }
51
+
52
+ /** Moves undelivered counts (pending, queued) to "dropped": they never reach an ended session. */
53
+ function dropUndelivered<D extends Partial<Record<string, number>>>(delivery: D | undefined): D | undefined {
54
+ if (!delivery) return delivery
55
+ const { pending = 0, queued = 0, ...rest } = delivery as Record<string, number>
56
+ const dropped = (rest.dropped ?? 0) + pending + queued
57
+ return (dropped ? { ...rest, dropped } : rest) as unknown as D
58
+ }
59
+
60
+ /**
61
+ * (CON-09a) How Watchtower shows a Concierge session: no auto @name, previous ones ended (not idle sessions).
62
+ * (CON-10) An ended one has no "last seen" (its spoke may still report it) and is not online, and messages to it
63
+ * that never reached it (queued, pending) are dropped, never waiting.
64
+ */
65
+ async function conciergeMarker(deps: GraphRouteDeps, userID: string): Promise<ConciergeMarks> {
66
+ const concierge = new Set(deps.concierge ? await deps.concierge(userID) : [])
67
+ if (!concierge.size) return { node: (node) => node, previous: new Set() }
68
+ const current = deps.conciergeCurrent ? await deps.conciergeCurrent(userID) : undefined
69
+ const previous = new Set(deps.conciergeCurrent === undefined ? [] : [...concierge].filter((id) => id !== current))
70
+ return {
71
+ previous,
72
+ node: (node) => {
73
+ if (node.kind !== "session" || !concierge.has(node.id)) return node
74
+ if (deps.conciergeCurrent === undefined) return { ...node, concierge: true as const }
75
+ if (node.id === current) return { ...node, concierge: true as const, label: "@@concierge" }
76
+ const { lastSeen: _lastSeen, ...rest } = node as typeof node & { lastSeen?: number }
77
+ return { ...rest, concierge: true as const, label: "@@concierge (previous)", status: "ended", online: false } as unknown as typeof node
78
+ },
79
+ }
80
+ }
81
+
82
+ /** (CON-10) Edges into an ended Concierge session: their undelivered messages are dropped. */
83
+ const markEdge = (previous: ReadonlySet<string>) => <E extends { to: string; delivery?: Partial<Record<string, number>> }>(edge: E): E =>
84
+ previous.has(edge.to) && edge.delivery ? { ...edge, delivery: dropUndelivered(edge.delivery) } : edge
85
+
86
+ /** (CON-10) A message to an ended Concierge session that never reached it reads "dropped". */
87
+ const markMessage = (previous: ReadonlySet<string>) => <M extends { to: string; delivery?: string }>(m: M): M =>
88
+ previous.has(m.to) && (m.delivery === "pending" || m.delivery === "queued") ? { ...m, delivery: "dropped" } : m
89
+
90
+ /** Every page of a cursor-paged list. */
91
+ async function allPages<T>(list: (cursor?: string) => Promise<{ readonly items: readonly T[]; readonly cursor?: string }>): Promise<T[]> {
92
+ const out: T[] = []
93
+ let cursor: string | undefined
94
+ for (let i = 0; i < PAGES_MAX; i++) {
95
+ const page = await list(cursor)
96
+ out.push(...page.items)
97
+ if (!page.cursor) break
98
+ cursor = page.cursor
99
+ }
100
+ return out
101
+ }
102
+
103
+ /** The projection's sources over the hub's stores. */
104
+ export function graphSources(deps: GraphRouteDeps): GraphSources {
105
+ const { teams, workspaces, audit, launches } = deps
106
+ return {
107
+ now: deps.now ?? Date.now,
108
+ machines: (userID) => deps.store.machines().filter((m) => !m.revoked && m.ownerID === userID),
109
+ registry: deps.registry,
110
+ sessions: (userID) => deps.store.agentData(userID).sessions ?? [],
111
+ directory: deps.directory,
112
+ messages: deps.messages,
113
+ ...(deps.decisions ? { decisions: deps.decisions } : {}),
114
+ ...(launches ? { launches: (userID: string) => launches.list(userID) } : {}),
115
+ ...(teams
116
+ ? {
117
+ teams: async (userID: string) => {
118
+ const list = (await allPages((cursor) => teams.teams.list(userID, { state: "active", limit: LIST_LIMIT_MAX, ...(cursor ? { cursor } : {}) }))) as GraphTeam[]
119
+ const seats: GraphSeat[] = []
120
+ for (const team of list) {
121
+ try {
122
+ for (const seat of await teams.seats.list(userID, team.id, { current: true })) seats.push({ teamID: seat.teamID, role: seat.role, sessionID: seat.sessionID })
123
+ } catch {
124
+ // A team this build cannot read shows without seats.
125
+ }
126
+ }
127
+ return { teams: list, seats }
128
+ },
129
+ }
130
+ : {}),
131
+ ...(workspaces
132
+ ? {
133
+ supervision: async (userID: string) => {
134
+ const out: GraphMembership[] = []
135
+ for (const workspace of await allPages((cursor) => workspaces.workspaces.list(userID, { state: "active", limit: LIST_LIMIT_MAX, ...(cursor ? { cursor } : {}) }))) {
136
+ const [members, graph] = await Promise.all([workspaces.members.members(userID, workspace.id), workspaces.supervision.graph(userID, workspace.id)])
137
+ const sessionOf = new Map(members.filter((m) => m.current).map((m) => [m.member.id, m.member.sessionID]))
138
+ const supervisorOf = new Map<string, string>()
139
+ for (const { link, member, supervisor } of graph.links) {
140
+ const sessionID = sessionOf.get(link.supervisorMemberID)
141
+ if (member === "current" && supervisor === "current" && sessionID !== undefined) supervisorOf.set(link.memberID, sessionID)
142
+ }
143
+ for (const [memberID, sessionID] of sessionOf) {
144
+ const supervisorSessionID = supervisorOf.get(memberID)
145
+ out.push({ workspaceID: workspace.id, sessionID, ...(supervisorSessionID !== undefined ? { supervisorSessionID } : {}) })
146
+ }
147
+ }
148
+ return out
149
+ },
150
+ }
151
+ : {}),
152
+ ...(audit
153
+ ? {
154
+ denials: async (userID: string) => {
155
+ const out: GraphDenial[] = []
156
+ let before: number | undefined
157
+ for (let i = 0; i < PAGES_MAX; i++) {
158
+ const page = await audit.list(userID, { limit: AUDIT_PAGE_MAX, ...(before !== undefined ? { before } : {}) })
159
+ for (const entry of page.entries) {
160
+ if (entry.verb !== "team.denied" || entry.actor.kind !== "session") continue
161
+ out.push({ at: entry.at, senderID: entry.actor.sessionID, ...(entry.target.sessionID !== undefined ? { recipientID: entry.target.sessionID } : {}) })
162
+ }
163
+ if (page.next === undefined) break
164
+ before = page.next
165
+ }
166
+ return out
167
+ },
168
+ }
169
+ : {}),
170
+ }
171
+ }
172
+
173
+ const NO_STORE = { "cache-control": "no-store" }
174
+ const ok = (body: unknown) => Response.json(body, { headers: NO_STORE })
175
+ const READ = { session: true, scope: "sessions:read" } as const
176
+
177
+ export function graphRoutes(deps: GraphRouteDeps): AgentRoute[] {
178
+ const sources = graphSources(deps)
179
+ return [
180
+ {
181
+ method: "GET",
182
+ path: `${AGENT_PREFIX}/graph`,
183
+ allow: READ,
184
+ handle: async ({ user, actor, url }) => {
185
+ const viewer = viewerOf(actor)
186
+ const graph = await buildGraph(sources, user.id, viewer, parseGraphQuery(url.searchParams, sources.now()))
187
+ const mark = await conciergeMarker(deps, user.id)
188
+ return ok({ ...graph, nodes: graph.nodes.map(mark.node), edges: graph.edges.map(markEdge(mark.previous)) })
189
+ },
190
+ },
191
+ {
192
+ method: "GET",
193
+ path: `${AGENT_PREFIX}/graph/edges/:id`,
194
+ allow: READ,
195
+ handle: async ({ user, actor, url, params }) => {
196
+ const detail = await edgeDetail(sources, user.id, viewerOf(actor), params.id!, url.searchParams, sources.now())
197
+ const { previous } = await conciergeMarker(deps, user.id)
198
+ return ok({ ...detail, edge: markEdge(previous)(detail.edge), messages: detail.messages.map(markMessage(previous)) })
199
+ },
200
+ },
201
+ {
202
+ method: "GET",
203
+ path: `${AGENT_PREFIX}/graph/nodes/:id`,
204
+ allow: READ,
205
+ handle: async ({ user, actor, url, params }) => {
206
+ const detail = await nodeDetail(sources, user.id, viewerOf(actor), params.id!, url.searchParams, sources.now())
207
+ const mark = await conciergeMarker(deps, user.id)
208
+ return ok({ ...detail, node: mark.node(detail.node), messages: detail.messages.map(markMessage(mark.previous)) })
209
+ },
210
+ },
211
+ ]
212
+ }