@vercel/factory 0.0.16 → 0.0.18

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 (186) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/api-contracts.d.mts +665 -715
  3. package/dist/api-contracts.mjs +91 -4
  4. package/dist/api-contracts.mjs.map +1 -1
  5. package/dist/api-task-graph.mjs +14 -5
  6. package/dist/api-task-graph.mjs.map +1 -1
  7. package/dist/api.d.mts +58 -11
  8. package/dist/api.mjs +715 -399
  9. package/dist/api.mjs.map +1 -1
  10. package/dist/approval-access.d.mts +1 -1
  11. package/dist/approval-contracts.d.mts +3 -4
  12. package/dist/approval-contracts.mjs +1 -2
  13. package/dist/approval-contracts.mjs.map +1 -1
  14. package/dist/blob/index.d.mts +1 -1
  15. package/dist/budget.mjs +20 -3
  16. package/dist/budget.mjs.map +1 -1
  17. package/dist/change-verification/brief.d.mts +1 -1
  18. package/dist/change-verification/dispatch.mjs +8 -1
  19. package/dist/change-verification/dispatch.mjs.map +1 -1
  20. package/dist/changes/eve-record-change.mjs +4 -1
  21. package/dist/changes/eve-record-change.mjs.map +1 -1
  22. package/dist/client-stream.d.mts +19 -2
  23. package/dist/client-stream.mjs +115 -53
  24. package/dist/client-stream.mjs.map +1 -1
  25. package/dist/client-transcript.d.mts +25 -2
  26. package/dist/client-transcript.mjs +95 -13
  27. package/dist/client-transcript.mjs.map +1 -1
  28. package/dist/client.d.mts +145 -19
  29. package/dist/client.mjs +89 -16
  30. package/dist/client.mjs.map +1 -1
  31. package/dist/code-review/contracts.d.mts +1 -0
  32. package/dist/code-review/eve-post-review.d.mts +4 -2
  33. package/dist/code-review/eve-post-review.mjs +8 -0
  34. package/dist/code-review/eve-post-review.mjs.map +1 -1
  35. package/dist/code-review/github-reporter.d.mts +6 -2
  36. package/dist/code-review/github-reporter.mjs +86 -35
  37. package/dist/code-review/github-reporter.mjs.map +1 -1
  38. package/dist/code-review/preparation.d.mts +2 -1
  39. package/dist/code-review/preparation.mjs +2 -1
  40. package/dist/code-review/preparation.mjs.map +1 -1
  41. package/dist/code-review/stall.mjs +11 -2
  42. package/dist/code-review/stall.mjs.map +1 -1
  43. package/dist/connectors/github.d.mts +1 -1
  44. package/dist/connectors/index.d.mts +1 -1
  45. package/dist/connectors/slack.d.mts +1 -1
  46. package/dist/delivery-metrics/metrics.d.mts +1 -1
  47. package/dist/dispatch.mjs +3 -3
  48. package/dist/dispatch.mjs.map +1 -1
  49. package/dist/eve/index.d.mts +1 -1
  50. package/dist/eve/index.mjs +8 -1
  51. package/dist/eve/index.mjs.map +1 -1
  52. package/dist/eve/task-execution.d.mts +4 -4
  53. package/dist/eve/transcript.d.mts +3 -1
  54. package/dist/eve/transcript.mjs.map +1 -1
  55. package/dist/finding-remediation/admission.d.mts +1 -1
  56. package/dist/finding-remediation/brief.d.mts +1 -1
  57. package/dist/github-app.d.mts +1 -1
  58. package/dist/github-publication.d.mts +1 -1
  59. package/dist/github.d.mts +9 -1
  60. package/dist/github.mjs +29 -11
  61. package/dist/github.mjs.map +1 -1
  62. package/dist/index.d.mts +1 -1
  63. package/dist/intake-contracts.d.mts +1 -0
  64. package/dist/intake-contracts.mjs +2 -1
  65. package/dist/intake-contracts.mjs.map +1 -1
  66. package/dist/integrations/github-issues.d.mts +53 -0
  67. package/dist/integrations/github-issues.mjs +170 -0
  68. package/dist/integrations/github-issues.mjs.map +1 -0
  69. package/dist/integrations/github.d.mts +2 -1
  70. package/dist/integrations/github.mjs +2 -1
  71. package/dist/multi-repository-changes/coordination.mjs +1 -1
  72. package/dist/planning/reconcile.mjs +15 -2
  73. package/dist/planning/reconcile.mjs.map +1 -1
  74. package/dist/postgres/index.d.mts +2 -1
  75. package/dist/postgres/index.mjs +11 -5
  76. package/dist/postgres/index.mjs.map +1 -1
  77. package/dist/postgres/indexed-codec.mjs +137 -0
  78. package/dist/postgres/indexed-codec.mjs.map +1 -0
  79. package/dist/postgres/indexed-schema.mjs +99 -0
  80. package/dist/postgres/indexed-schema.mjs.map +1 -0
  81. package/dist/postgres/indexed.d.mts +28 -0
  82. package/dist/postgres/indexed.mjs +277 -0
  83. package/dist/postgres/indexed.mjs.map +1 -0
  84. package/dist/postgres/record-predicates.mjs +23 -0
  85. package/dist/postgres/record-predicates.mjs.map +1 -0
  86. package/dist/presets/software-development/deepsec-brief.d.mts +1 -1
  87. package/dist/presets/software-development/dispatch.mjs +1 -1
  88. package/dist/presets/software-development/dispatch.mjs.map +1 -1
  89. package/dist/presets/software-development/recovery.mjs +9 -2
  90. package/dist/presets/software-development/recovery.mjs.map +1 -1
  91. package/dist/pull-requests/eve-fetch.d.mts +4 -1
  92. package/dist/pull-requests/eve-fetch.mjs +9 -0
  93. package/dist/pull-requests/eve-fetch.mjs.map +1 -1
  94. package/dist/pull-requests/github-publisher.d.mts +1 -1
  95. package/dist/pull-requests/github-publisher.mjs +19 -7
  96. package/dist/pull-requests/github-publisher.mjs.map +1 -1
  97. package/dist/repositories.mjs +20 -13
  98. package/dist/repositories.mjs.map +1 -1
  99. package/dist/sandbox/execution-run-ledger.d.mts +2 -2
  100. package/dist/sandbox/execution-run-ledger.mjs.map +1 -1
  101. package/dist/sandbox/execution.d.mts +2 -2
  102. package/dist/sandbox/execution.mjs.map +1 -1
  103. package/dist/schema/catalog.mjs +1 -1
  104. package/dist/schema/change.d.mts +1 -1
  105. package/dist/schema/communication.d.mts +2 -2
  106. package/dist/schema/factory-config.d.mts +1 -1
  107. package/dist/schema/outbox.d.mts +6 -6
  108. package/dist/schema/session-summary.d.mts +218 -0
  109. package/dist/schema/session-summary.mjs +68 -0
  110. package/dist/schema/session-summary.mjs.map +1 -0
  111. package/dist/schema/session.d.mts +39 -59
  112. package/dist/schema/session.mjs +6 -6
  113. package/dist/schema/session.mjs.map +1 -1
  114. package/dist/schema/task-graph.d.mts +24 -24
  115. package/dist/schema/transcript.d.mts +141 -3
  116. package/dist/schema/transcript.mjs +19 -3
  117. package/dist/schema/transcript.mjs.map +1 -1
  118. package/dist/schema/transitions.d.mts +1 -1
  119. package/dist/schema/transitions.mjs +1 -1
  120. package/dist/schema/transitions.mjs.map +1 -1
  121. package/dist/signals.d.mts +1 -1
  122. package/dist/stall.mjs +10 -2
  123. package/dist/stall.mjs.map +1 -1
  124. package/dist/storage.d.mts +10 -1
  125. package/dist/storage.mjs +7 -1
  126. package/dist/store/approvals.d.mts +14 -0
  127. package/dist/store/approvals.mjs +78 -0
  128. package/dist/store/approvals.mjs.map +1 -0
  129. package/dist/store/conversations.d.mts +59 -0
  130. package/dist/store/conversations.mjs +549 -0
  131. package/dist/store/conversations.mjs.map +1 -0
  132. package/dist/store/driver.d.mts +1 -1
  133. package/dist/store/driver.mjs.map +1 -1
  134. package/dist/store/engine.d.mts +38 -16
  135. package/dist/store/engine.mjs +229 -106
  136. package/dist/store/engine.mjs.map +1 -1
  137. package/dist/store/indexed-engine.d.mts +57 -0
  138. package/dist/store/indexed-engine.mjs +155 -0
  139. package/dist/store/indexed-engine.mjs.map +1 -0
  140. package/dist/store/indexed-memory.d.mts +7 -0
  141. package/dist/store/indexed-memory.mjs +200 -0
  142. package/dist/store/indexed-memory.mjs.map +1 -0
  143. package/dist/store/indexed-projections.d.mts +280 -0
  144. package/dist/store/indexed-projections.mjs +481 -0
  145. package/dist/store/indexed-projections.mjs.map +1 -0
  146. package/dist/store/indexed.d.mts +101 -0
  147. package/dist/store/indexed.mjs +19 -0
  148. package/dist/store/indexed.mjs.map +1 -0
  149. package/dist/store/iterate.d.mts +9 -0
  150. package/dist/store/iterate.mjs +23 -0
  151. package/dist/store/iterate.mjs.map +1 -0
  152. package/dist/store/query-scope.mjs +62 -0
  153. package/dist/store/query-scope.mjs.map +1 -0
  154. package/dist/store/query.d.mts +32 -0
  155. package/dist/store/query.mjs +103 -0
  156. package/dist/store/query.mjs.map +1 -0
  157. package/dist/store/session-history.mjs +24 -0
  158. package/dist/store/session-history.mjs.map +1 -0
  159. package/dist/store/slack-pr-notifications.d.mts +2 -2
  160. package/dist/store/slack-pr-notifications.mjs.map +1 -1
  161. package/dist/store/state.d.mts +26 -0
  162. package/dist/store/state.mjs +95 -0
  163. package/dist/store/state.mjs.map +1 -0
  164. package/dist/store/task-effects.d.mts +1 -1
  165. package/dist/store/task-graphs.d.mts +1 -1
  166. package/dist/store/task-messages.d.mts +1 -1
  167. package/dist/store/task-work.d.mts +1 -1
  168. package/dist/store/task-work.mjs +1 -1
  169. package/dist/store/transcripts.mjs +308 -0
  170. package/dist/store/transcripts.mjs.map +1 -0
  171. package/dist/sweep.d.mts +1 -1
  172. package/dist/sweep.mjs +12 -7
  173. package/dist/sweep.mjs.map +1 -1
  174. package/dist/task-communication.mjs +15 -1
  175. package/dist/task-communication.mjs.map +1 -1
  176. package/dist/task-protocols.d.mts +3 -3
  177. package/dist/tasks.d.mts +1 -1
  178. package/dist/vercel.d.mts +1 -1
  179. package/dist/work-triage/find-existing-work.d.mts +1 -1
  180. package/dist/work-triage/find-existing-work.mjs +12 -2
  181. package/dist/work-triage/find-existing-work.mjs.map +1 -1
  182. package/docs/index.md +4 -0
  183. package/docs/recipes/persistence-recovery.md +15 -9
  184. package/docs/recipes/postgres-indexed-engine.md +108 -0
  185. package/docs/recipes/postgres-indexed-storage.md +145 -0
  186. package/package.json +6 -1
package/dist/api.mjs CHANGED
@@ -1,24 +1,26 @@
1
+ import { collectFactoryRecords } from "./store/iterate.mjs";
1
2
  import { repositoryIdSchema, signalIdSchema, taskIdSchema } from "./schema/id.mjs";
2
3
  import { repositorySlugSchema } from "./schema/repository.mjs";
3
4
  import { parseAgentRouteBinding } from "./schema/agent-route.mjs";
4
5
  import { taskStateSchema } from "./schema/task.mjs";
6
+ import { RecordNotFoundError, VersionConflictError } from "./store/driver.mjs";
5
7
  import { InvalidTransitionError } from "./schema/transitions.mjs";
6
- import { readEvents } from "./client-events.mjs";
7
8
  import { MAX_IMAGE_BYTES, imageIdSchema, imagePartFilename, imageUploadSchema } from "./image-input.mjs";
8
9
  import { Transcript } from "./client-transcript.mjs";
9
10
  import { routeTaskWork } from "./agent-routes.mjs";
10
- import { RecordNotFoundError, VersionConflictError } from "./store/driver.mjs";
11
11
  import { BudgetExhaustedError, DispatchRefusedError, dispatchTask } from "./dispatch.mjs";
12
+ import { findingSchema } from "./schema/finding.mjs";
12
13
  import { ApprovalAccessError, createApprovalAccess } from "./approval-access.mjs";
13
14
  import { agentSessionIdSchema, agentSessionSchema } from "./schema/session.mjs";
14
15
  import { ApprovalError, ImageInputError, SessionConflictError } from "./store/engine.mjs";
16
+ import { InvalidStoreQueryError } from "./store/indexed.mjs";
17
+ import { answerSessionQuestionRequestSchema, apiAgentSchema, controlSessionRequestSchema, createSessionRequestSchema, repositoryDetailsSchema, repositorySummarySchema, sendSessionMessageRequestSchema, sessionInputResponseSchema, sessionPageSchema, sessionPullRequestSchema, sessionPullRequestStateSchema, sessionViewSchema, sessionWorkGroupSchema, sessionWorkResultSchema, sessionWorkTaskSchema, submitSessionInputRequestSchema } from "./api-contracts.mjs";
15
18
  import { inboxActivityItemSchema, inboxActivitySchema } from "./inbox-activity.mjs";
16
19
  import { agentModelConfigSchema, modelSettingsSchema, updateModelDefaultsSchema } from "./model-settings.mjs";
17
20
  import { sessionPreviewSchema, sessionPreviewsSchema } from "./session-previews.mjs";
18
21
  import { sessionReviewCheckSchema, sessionReviewIssueSchema, sessionReviewSchema, sessionReviewScreenshotSchema } from "./session-review.mjs";
19
22
  import { workspaceFileContentSchema, workspaceFileEntrySchema, workspaceFilesRequestSchema, workspaceFilesSchema, workspacePathSchema } from "./workspace-files.mjs";
20
23
  import { intakeViewSchema, retryIntakeSchema } from "./intake-contracts.mjs";
21
- import { answerSessionQuestionRequestSchema, apiAgentSchema, controlSessionRequestSchema, createSessionRequestSchema, repositoryDetailsSchema, repositorySummarySchema, sendSessionMessageRequestSchema, sessionInputResponseSchema, sessionPageSchema, sessionViewSchema, submitSessionInputRequestSchema } from "./api-contracts.mjs";
22
24
  import { unavailableWorkspaceDiff, workspaceDiffFileSchema, workspaceDiffSchema } from "./workspace-diff.mjs";
23
25
  import { taskGraphViewSchema } from "./task-graph-view.mjs";
24
26
  import { workspaceGitFilesScript } from "./workspace-files-git.mjs";
@@ -39,14 +41,18 @@ const json = (value, status = 200) => Response.json(value, {
39
41
  status,
40
42
  headers: { "cache-control": "no-store" }
41
43
  });
42
- function attention(task, change) {
43
- if (task?.state === "needs_human") return "question";
44
- if (task?.state === "failed" || task?.state === "stalled") return task.state;
45
- if (change?.state === "ready") return "review";
46
- }
44
+ const pullRequestStatusObservationSchema = z.strictObject({
45
+ changeId: sessionPullRequestSchema.shape.changeId,
46
+ state: sessionPullRequestStateSchema.nullable(),
47
+ sync: sessionPullRequestSchema.shape.sync,
48
+ checkedAt: sessionPullRequestSchema.shape.checkedAt,
49
+ observedAt: sessionPullRequestSchema.shape.observedAt,
50
+ syncMessage: sessionPullRequestSchema.shape.syncMessage
51
+ });
47
52
  /** Creates the authenticated request handler for the public Factory HTTP API. */
48
53
  function createFactoryApi(options) {
49
54
  const { stores } = options;
55
+ const maxSessionPullRequests = z.number().int().min(1).max(100).parse(options.maxSessionPullRequests ?? 50);
50
56
  const agents = options.agents.map((agent) => apiAgentSchema.parse(agent));
51
57
  const agentModels = (options.agentModels ?? []).map((entry) => agentModelConfigSchema.parse(entry));
52
58
  if (new Set(agentModels.map((entry) => entry.agent)).size !== agentModels.length) throw new Error("Duplicate agent model catalog");
@@ -59,64 +65,64 @@ function createFactoryApi(options) {
59
65
  if (new Set(agents.map((agent) => agent.id)).size !== agents.length) throw new Error("Duplicate API agent");
60
66
  if (options.continuationAgent !== void 0 && !agents.some((agent) => agent.id === options.continuationAgent && agent.route === void 0 && agent.canStart)) throw new Error("continuationAgent must be a directly startable conversation agent");
61
67
  for (const agent of agents) if (agent.route !== void 0 && options.routes.get(agent.route)?.agent !== agent.id) throw new Error(`API agent ${agent.id} must select one of its registered routes`);
68
+ async function configuredRepositories() {
69
+ return mapConcurrent(options.repositories, async (slug) => {
70
+ return (await stores.repositories.list({
71
+ limit: 1,
72
+ match: { slug }
73
+ })).items[0] ?? options.ensureRepository(slug);
74
+ });
75
+ }
62
76
  const approvals = createApprovalAccess({
63
77
  stores,
64
78
  operators: options.operators ?? [],
65
79
  repositories: options.repositories
66
80
  });
67
- const approvalView = approvals.view;
68
81
  async function approvalPage(url, actor) {
69
82
  const input = z.object({
70
83
  status: z.enum([
71
84
  "pending",
72
85
  "decided",
73
86
  "all"
74
- ]).default("pending").describe("Pending approvals, recorded decisions, or both."),
75
- repository: repositorySlugSchema.optional().describe("Configured repository slug to filter."),
76
- query: z.string().trim().max(200).optional().describe("Case-insensitive text in the finding, repository, Task ID, or decision actor/reason."),
87
+ ]).default("pending"),
88
+ repository: repositorySlugSchema.optional(),
89
+ query: z.string().trim().max(200).optional(),
77
90
  sort: z.enum([
78
91
  "created",
79
92
  "priority",
80
93
  "decided"
81
- ]).default("created").describe("Oldest creation first, highest finding priority first, or newest decision first; ties use creation time and Task ID."),
82
- limit: z.coerce.number().int().min(1).max(100).default(30).describe("Maximum authorized approvals per page."),
83
- cursor: taskIdSchema.optional().describe("Last Task ID from the previous page with the same filters.")
94
+ ]).default("created"),
95
+ limit: z.coerce.number().int().min(1).max(100).default(30),
96
+ cursor: z.string().max(8192).optional()
84
97
  }).parse(Object.fromEntries(url.searchParams));
85
- if (input.repository && !options.repositories.includes(input.repository)) throw new ApiError(403, "Repository is not configured");
86
- const repos = (await stores.repositories.list()).filter((repo) => repo.enabled && options.repositories.includes(repo.slug));
87
- const ids = new Set(repos.map((repo) => repo.id));
88
- const selected = input.repository ? repos.find((repo) => repo.slug === input.repository)?.id : void 0;
89
- const tasks = (await stores.tasks.list()).filter((task) => task.approval !== void 0 && task.repositoryIds.length > 0 && task.repositoryIds.every((id) => ids.has(id)) && (!input.repository || selected && task.repositoryIds.includes(selected)) && (input.status === "all" || (input.status === "pending" ? task.approval === "required" && task.state === "queued" : task.approval !== "required"))).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
90
- let views;
91
- if (input.query || input.sort !== "created") {
92
- views = await mapConcurrent(tasks, approvalView);
93
- const query = input.query?.toLowerCase();
94
- if (query) views = views.filter((view) => [
95
- view.task.id,
96
- ...view.repositories,
97
- view.finding?.title,
98
- view.finding?.body,
99
- view.finding?.category,
100
- view.finding?.path,
101
- view.decision?.decidedBy,
102
- view.decision?.reason
103
- ].filter(Boolean).join(" ").toLowerCase().includes(query));
104
- if (input.sort === "priority") views.sort((a, b) => (a.finding?.priority ?? "P4").localeCompare(b.finding?.priority ?? "P4") || a.task.createdAt.localeCompare(b.task.createdAt) || a.task.id.localeCompare(b.task.id));
105
- if (input.sort === "decided") views.sort((a, b) => (b.decision?.recordedAt ?? b.task.createdAt).localeCompare(a.decision?.recordedAt ?? a.task.createdAt) || a.task.createdAt.localeCompare(b.task.createdAt) || a.task.id.localeCompare(b.task.id));
106
- }
107
- const ordered = views?.map((view) => view.task) ?? tasks;
108
- const start = input.cursor ? ordered.findIndex((task) => task.id === input.cursor) + 1 : 0;
109
- if (input.cursor && start === 0) throw new ApiError(409, "Approval list changed; refresh from the first page");
110
- const page = ordered.slice(start, start + input.limit);
98
+ const repos = (await configuredRepositories()).filter((repo) => repo.enabled);
99
+ const selected = input.repository ? repos.find((repo) => repo.slug === input.repository) : void 0;
100
+ if (input.repository && !selected) throw new ApiError(403, "Repository is not configured");
101
+ const page = await stores.approvals.list({
102
+ limit: input.limit,
103
+ cursor: input.cursor,
104
+ sort: input.sort,
105
+ search: input.query || void 0,
106
+ includeTotal: true,
107
+ match: {
108
+ ...selected ? { repositoryIds: [selected.id] } : {},
109
+ ...input.status === "all" ? {} : { status: input.status }
110
+ },
111
+ fields: [{
112
+ path: ["repositoryIds"],
113
+ test: "subset",
114
+ value: repos.map((repo) => repo.id)
115
+ }]
116
+ });
111
117
  return json({
112
118
  actor,
113
- items: views ? views.slice(start, start + input.limit) : await mapConcurrent(page, approvalView),
114
- totalCount: ordered.length,
115
- nextCursor: start + input.limit < ordered.length ? page.at(-1).id : null
119
+ items: await mapConcurrent(page.items, (id) => approvals.get(actor, id)),
120
+ nextCursor: page.nextCursor,
121
+ totalCount: page.totalCount
116
122
  });
117
123
  }
118
124
  async function start(session, task, message, operationId) {
119
- const continuation = task ? (await stores.sessions.get(session.id))?.continuations?.find((item) => item.taskId === task.id) : void 0;
125
+ const continuation = task ? await stores.sessions.getContinuation(session.id, task.id) : void 0;
120
126
  const attachments = continuation ? continuation.contextAttachments ?? continuation.attachments : session.attachments;
121
127
  return options.start({
122
128
  session,
@@ -130,14 +136,109 @@ function createFactoryApi(options) {
130
136
  const binding = task.work.route;
131
137
  return binding ? options.routes.get(binding) : void 0;
132
138
  }
133
- function taskAgent(session, task) {
139
+ async function taskAgent(session, task) {
134
140
  const route = taskRoute(task);
135
141
  if (route) return agents.some((agent) => agent.id === route.agent) ? route.agent : void 0;
136
- const continuation = session.continuations?.find((item) => item.taskId === task.id);
142
+ const continuation = await stores.sessions.getContinuation(session.id, task.id);
137
143
  return task.work.route?.id === "conversation" && task.work.route.version === 1 && continuation?.agent === options.continuationAgent ? options.continuationAgent : void 0;
138
144
  }
139
- function executionSession(session, task, agent) {
140
- const activeAgent = agent ?? (task ? taskAgent(session, task) : session.agent);
145
+ const terminalWorkStates = /* @__PURE__ */ new Set([
146
+ "succeeded",
147
+ "failed",
148
+ "cancelled"
149
+ ]);
150
+ function aggregateWorkState(tasks, root) {
151
+ const states = new Set(tasks.map((task) => task.state));
152
+ for (const state of [
153
+ "needs_human",
154
+ "verifying",
155
+ "running",
156
+ "queued",
157
+ "stalled"
158
+ ]) if (states.has(state)) return state;
159
+ if (root && terminalWorkStates.has(root.state)) return root.state;
160
+ for (const state of [
161
+ "failed",
162
+ "cancelled",
163
+ "succeeded"
164
+ ]) if (states.has(state)) return state;
165
+ return "queued";
166
+ }
167
+ async function sessionWorkGroups(session, selectedTask, relatedTasks, pullRequests) {
168
+ const knownTasks = /* @__PURE__ */ new Map();
169
+ for (const task of relatedTasks) knownTasks.set(task.id, task);
170
+ if (selectedTask) knownTasks.set(selectedTask.id, selectedTask);
171
+ async function ancestor(id) {
172
+ const cached = knownTasks.get(id);
173
+ if (cached) return cached;
174
+ const fetched = await stores.tasks.get(id);
175
+ if (fetched) knownTasks.set(id, fetched);
176
+ return fetched;
177
+ }
178
+ async function requestRootId(task) {
179
+ let current = task;
180
+ const visited = /* @__PURE__ */ new Set([task.id]);
181
+ for (;;) {
182
+ if (current.parentTaskId === void 0 || visited.has(current.parentTaskId)) return current.id;
183
+ const parent = await ancestor(current.parentTaskId);
184
+ if (!parent || parent.work.route?.id === "conversation") return current.id;
185
+ visited.add(parent.id);
186
+ current = parent;
187
+ }
188
+ }
189
+ const tasks = [...new Map([...relatedTasks, ...selectedTask ? [selectedTask] : []].filter((task) => task.work.route?.id !== "conversation").map((task) => [task.id, task])).values()];
190
+ const grouped = /* @__PURE__ */ new Map();
191
+ for (const task of tasks) {
192
+ if (!task.work.route) continue;
193
+ const requestId = await requestRootId(task);
194
+ const group = grouped.get(requestId) ?? [];
195
+ group.push(task);
196
+ grouped.set(requestId, group);
197
+ }
198
+ return await mapConcurrent([...grouped], async ([requestId, groupedTasks]) => {
199
+ let root = groupedTasks.find((task) => task.id === requestId) ?? null;
200
+ if (!root) {
201
+ const candidate = await ancestor(requestId);
202
+ root = candidate ? await authorizedRelatedTask(session, candidate) : null;
203
+ if (root && root.work.route?.id === "conversation") root = null;
204
+ }
205
+ const orderedTasks = [...root ? [root] : [], ...groupedTasks.filter((task) => task.id !== root?.id)];
206
+ const anchor = root ?? orderedTasks[0];
207
+ const changeIds = [...new Set(orderedTasks.map((task) => task.changeId).filter((value) => value !== void 0))];
208
+ const state = aggregateWorkState(orderedTasks, root);
209
+ let result;
210
+ if (root && options.presentSessionWork && orderedTasks.every((task) => terminalWorkStates.has(task.state))) try {
211
+ const presented = await options.presentSessionWork({
212
+ root,
213
+ tasks: orderedTasks,
214
+ pullRequests: pullRequests.filter((pullRequest) => changeIds.includes(pullRequest.changeId))
215
+ });
216
+ if (presented) result = sessionWorkResultSchema.parse(presented);
217
+ } catch (error) {
218
+ options.reportError?.(error, {
219
+ method: "GET",
220
+ path: "session-work-presentation"
221
+ });
222
+ }
223
+ return sessionWorkGroupSchema.parse({
224
+ requestId,
225
+ title: anchor.work.title,
226
+ agent: taskRoute(anchor)?.agent ?? anchor.work.route?.id ?? "Workflow",
227
+ state,
228
+ tasks: orderedTasks.map((task) => ({
229
+ taskId: task.id,
230
+ title: task.work.title,
231
+ agent: taskRoute(task)?.agent ?? task.work.route?.id ?? "Workflow",
232
+ state: task.state,
233
+ attempt: task.attempt
234
+ })),
235
+ changeIds,
236
+ result
237
+ });
238
+ });
239
+ }
240
+ async function executionSession(session, task, agent) {
241
+ const activeAgent = agent ?? (task ? await taskAgent(session, task) : session.agent);
141
242
  if (!activeAgent) throw new ApiError(409, "Task has no configured agent route");
142
243
  return agentSessionSchema.parse({
143
244
  ...session,
@@ -147,282 +248,369 @@ function createFactoryApi(options) {
147
248
  external: task ? void 0 : session.external
148
249
  });
149
250
  }
150
- function sessionTasks(session, byId, candidates = byId.values()) {
151
- const root = session.taskId === void 0 ? null : byId.get(session.taskId) ?? null;
152
- function depth(task) {
153
- const seen = /* @__PURE__ */ new Set();
154
- let current = task;
155
- while (current?.parentTaskId && !seen.has(current.id)) {
156
- seen.add(current.id);
157
- current = byId.get(current.parentTaskId);
158
- }
159
- return seen.size;
160
- }
161
- if (root === null) return {
162
- root,
163
- relatedTasks: [],
164
- tasks: []
165
- };
166
- const relatedTasks = [...candidates].filter((task) => root !== null && task.id !== root.id && task.replyTo.channel === "api" && task.replyTo.address === session.id && task.repositoryIds.length === 1 && task.repositoryIds[0] === session.repositoryId && (task.work.route !== void 0 || session.continuations?.some((item) => item.taskId === task.id))).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || depth(a) - depth(b) || a.id.localeCompare(b.id));
251
+ function workflowTasks(session, tasks) {
252
+ return [...tasks].filter((task) => task.work.route === void 0 && !task.question && task.replyTo.channel === "api" && task.replyTo.address === session.id && task.repositoryIds.length === 1 && task.repositoryIds[0] === session.repositoryId);
253
+ }
254
+ async function mayAccessTask(owner, task) {
255
+ return options.canAccessTask ? options.canAccessTask({ id: owner }, task) : options.sharedWorkspaceOwner === owner;
256
+ }
257
+ async function sessionFinding(session, task) {
258
+ if (!task?.origin.signalId) return null;
259
+ const repositoryIds = session.external?.repositoryIds ?? (session.repositoryId ? [session.repositoryId] : []);
260
+ if (!task.repositoryIds.every((id) => repositoryIds.includes(id)) || (session.attached || session.external) && !await mayAccessTask(session.owner, task)) return null;
261
+ const signal = await stores.signals.get(task.origin.signalId);
262
+ if (!signal || !task.repositoryIds.includes(signal.repositoryId)) return null;
263
+ const finding = findingSchema.safeParse(signal.payload);
264
+ return finding.success ? finding.data : null;
265
+ }
266
+ async function findingSummary(summary) {
267
+ if (!summary.task?.approval) return summary;
268
+ const task = await stores.tasks.get(summary.task.id);
269
+ const finding = await sessionFinding(summary.session, task);
167
270
  return {
168
- root,
169
- relatedTasks,
170
- tasks: [...root ? [root] : [], ...relatedTasks]
271
+ ...summary,
272
+ finding: finding ? {
273
+ priority: finding.priority,
274
+ category: finding.category
275
+ } : null
171
276
  };
172
277
  }
173
- function conversationQuestion(tasks, byId) {
174
- for (const parent of tasks) {
175
- const question = parent.questionTaskId ? byId.get(parent.questionTaskId) : void 0;
176
- if (parent.state === "needs_human" && question?.state === "needs_human" && question.question && question.parentTaskId === parent.id && question.parentTaskAttempt === parent.attempt && question.replyTo.channel === parent.replyTo.channel && question.replyTo.address === parent.replyTo.address) return question;
278
+ async function authorizedSummary(summary, owner) {
279
+ const { session } = summary;
280
+ if (!agents.some((agent) => agent.id === session.agent)) return null;
281
+ if (session.attached) {
282
+ const root = session.taskId ? await stores.tasks.get(session.taskId) : null;
283
+ if (!root || !await mayAccessTask(owner, root)) return null;
284
+ }
285
+ if (summary.task && (session.attached || session.external) && options.canAccessTask) {
286
+ const selected = await stores.tasks.get(summary.task.id);
287
+ if (!selected || !await mayAccessTask(owner, selected)) return null;
177
288
  }
178
- return null;
289
+ let question = summary.question;
290
+ if (question && session.external) {
291
+ const task = await stores.tasks.get(question.id);
292
+ const parent = task?.parentTaskId ? await stores.tasks.get(task.parentTaskId) : null;
293
+ if (!task || !parent || !await mayAccessTask(owner, task) || !await mayAccessTask(owner, parent)) question = null;
294
+ }
295
+ const route = summary.task?.work.route ? options.routes.get(summary.task.work.route) : void 0;
296
+ const candidate = summary.task ? route?.agent ?? (summary.task.work.route?.id === "conversation" && summary.task.work.route.version === 1 ? options.continuationAgent : void 0) : session.agent;
297
+ const activeAgent = agents.some((agent) => agent.id === candidate) && (!summary.task || summary.activeAgent === candidate) ? candidate : void 0;
298
+ let attention = summary.attention;
299
+ if (summary.question && !question) {
300
+ const head = summary.execution ? await stores.transcripts.head(summary.execution) : null;
301
+ attention = head?.state === "failed" ? "failed" : session.external && (head?.state === "waiting" || head?.state === "needs input") ? "conversation" : void 0;
302
+ }
303
+ return {
304
+ ...summary,
305
+ question,
306
+ attention,
307
+ activeAgent,
308
+ execution: activeAgent ? summary.execution : null
309
+ };
179
310
  }
180
- function workflowTasks(session, tasks) {
181
- return [...tasks].filter((task) => task.work.route === void 0 && !task.question && task.replyTo.channel === "api" && task.replyTo.address === session.id && task.repositoryIds.length === 1 && task.repositoryIds[0] === session.repositoryId);
311
+ function relatedQuery(session) {
312
+ return {
313
+ match: { replyTo: session.external?.replyTo ?? {
314
+ channel: "api",
315
+ address: session.id
316
+ } },
317
+ ...!session.external ? { fields: [
318
+ {
319
+ path: ["repositoryIds"],
320
+ test: "eq",
321
+ value: session.repositoryId ? [session.repositoryId] : []
322
+ },
323
+ {
324
+ path: ["work", "route"],
325
+ test: "exists"
326
+ },
327
+ ...session.taskId ? [{
328
+ path: ["id"],
329
+ test: "ne",
330
+ value: session.taskId
331
+ }] : []
332
+ ] } : {}
333
+ };
182
334
  }
183
- function workflowAttention(session, tasks) {
184
- const work = workflowTasks(session, tasks);
185
- if (work.some((task) => task.state === "failed")) return "failed";
186
- if (work.some((task) => task.state === "stalled")) return "stalled";
335
+ async function authorizedRelatedTask(session, task) {
336
+ if (session.external && (!task.repositoryIds.every((id) => session.external.repositoryIds.includes(id)) || !await mayAccessTask(session.owner, task))) return null;
337
+ if (!session.external && (task.repositoryIds.length !== 1 || task.repositoryIds[0] !== session.repositoryId)) return null;
338
+ return task;
187
339
  }
188
- async function view(session, snapshot, detail = "full") {
189
- if (session.external) {
190
- const external = session.external;
191
- const relatedTasks = (await mapConcurrent((options.canAccessTask ? snapshot ? [...snapshot.tasks.values()] : await stores.tasks.forReplyTo(external.replyTo) : []).filter((task) => task.replyTo.channel === external.replyTo.channel && task.replyTo.address === external.replyTo.address && task.repositoryIds.every((id) => external.repositoryIds.includes(id))), async (task) => await options.canAccessTask?.({ id: session.owner }, task) ? task : null)).filter((task) => task !== null).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
192
- const question = conversationQuestion(relatedTasks, new Map(relatedTasks.map((task) => [task.id, task])));
193
- const transcript = session.execution ? await stores.transcripts.get(session.execution) : null;
194
- const humanAttention = question ? "question" : transcript?.state === "failed" ? "failed" : transcript?.state === "waiting" || transcript?.state === "needs input" ? "conversation" : void 0;
340
+ async function sessionPullRequests(session, selectedTask, signal) {
341
+ const maxRelatedTaskPages = 10;
342
+ const changeIds = /* @__PURE__ */ new Set();
343
+ const addTask = (task) => {
344
+ if (task?.changeId) changeIds.add(task.changeId);
345
+ };
346
+ addTask(selectedTask);
347
+ if (session.changeId) changeIds.add(session.changeId);
348
+ if (session.taskId && selectedTask?.id !== session.taskId) {
349
+ const root = await stores.tasks.get(session.taskId);
350
+ if (root) addTask(await authorizedRelatedTask(session, root));
351
+ }
352
+ let cursor;
353
+ let pages = 0;
354
+ let truncated = changeIds.size > maxSessionPullRequests;
355
+ let limitReached = false;
356
+ do {
357
+ pages += 1;
358
+ const page = await stores.tasks.list({
359
+ limit: 100,
360
+ cursor,
361
+ ...relatedQuery(session)
362
+ });
363
+ const tasks = await mapConcurrent(page.items, (task) => authorizedRelatedTask(session, task));
364
+ for (const task of tasks) {
365
+ const changeId = task?.changeId;
366
+ if (!changeId || changeIds.has(changeId)) continue;
367
+ if (changeIds.size >= maxSessionPullRequests) {
368
+ truncated = true;
369
+ limitReached = true;
370
+ break;
371
+ }
372
+ changeIds.add(changeId);
373
+ }
374
+ cursor = page.nextCursor ?? void 0;
375
+ if (cursor && pages >= maxRelatedTaskPages) {
376
+ truncated = true;
377
+ limitReached = true;
378
+ }
379
+ } while (cursor && !limitReached);
380
+ const targets = (await mapConcurrent([...changeIds].slice(0, maxSessionPullRequests), async (changeId) => {
381
+ const change = await stores.changes.get(changeId);
382
+ if (!change) return null;
383
+ const repository = await stores.repositories.get(change.repositoryId);
384
+ const allowedRepositoryIds = session.external?.repositoryIds ?? (session.repositoryId ? [session.repositoryId] : []);
385
+ if (!repository || !allowedRepositoryIds.includes(repository.id)) return null;
195
386
  return {
196
- session,
197
- task: null,
198
- execution: session.execution ?? null,
199
- activeAgent: session.agent,
200
- source: session.external.replyTo.channel,
201
- relatedTasks,
202
- question,
203
- canFollowUp: false,
204
- ...humanAttention ? { attention: humanAttention } : {}
387
+ change,
388
+ repository
205
389
  };
390
+ })).filter((target) => target !== null);
391
+ if (!targets.length) return {
392
+ pullRequests: [],
393
+ truncated
394
+ };
395
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
396
+ let observations;
397
+ if (!options.pullRequestStatuses) observations = targets.map(({ change }) => ({
398
+ changeId: change.id,
399
+ state: null,
400
+ sync: "unavailable",
401
+ checkedAt,
402
+ syncMessage: "Live pull request status is not configured for this factory."
403
+ }));
404
+ else try {
405
+ observations = z.array(pullRequestStatusObservationSchema).max(maxSessionPullRequests).parse(await options.pullRequestStatuses({
406
+ pullRequests: targets,
407
+ signal
408
+ }));
409
+ } catch (error) {
410
+ options.reportError?.(error, {
411
+ method: "GET",
412
+ path: "session-pull-requests"
413
+ });
414
+ observations = targets.map(({ change }) => ({
415
+ changeId: change.id,
416
+ state: null,
417
+ sync: "unavailable",
418
+ checkedAt,
419
+ syncMessage: "Live pull request status could not be refreshed."
420
+ }));
206
421
  }
207
- const boundTask = !snapshot && session.taskId ? await stores.tasks.get(session.taskId) : null;
208
- const addressed = (!snapshot || session.notifications.some((event) => ["question", "failed"].includes(event.outcome))) && (boundTask || session.repositoryId !== void 0) ? await stores.tasks.forReplyTo({
209
- channel: "api",
210
- address: session.id
211
- }) : [];
212
- const byId = new Map([...snapshot?.tasks ?? [], ...addressed.map((task) => [task.id, task])]);
213
- if (boundTask) byId.set(boundTask.id, boundTask);
214
- const { root, relatedTasks, tasks } = sessionTasks(session, byId);
215
- const task = tasks.at(-1) ?? null;
216
- const questionParents = [...tasks, ...workflowTasks(session, byId.values())];
217
- if (!snapshot) await mapConcurrent(questionParents.filter((item) => item.state === "needs_human" && item.questionTaskId), async (item) => {
218
- const question = await stores.tasks.get(item.questionTaskId);
219
- if (question) byId.set(question.id, question);
220
- });
221
- const question = conversationQuestion(questionParents, byId);
222
- const history = [];
223
- const includeHistory = detail === "full" && snapshot?.receipts !== null;
224
- const receipts = tasks.length && includeHistory ? snapshot?.receipts ?? await stores.receipts.list() : [];
225
- for (const item of includeHistory ? tasks : []) {
226
- const agent = taskAgent(session, item);
227
- if (!agent) continue;
228
- const message = session.continuations?.find((entry) => entry.taskId === item.id)?.message ?? (item.id === root?.id ? session.message : `${item.kind.replaceAll("_", " ")} · ${item.id}`);
229
- const executions = receipts.filter((receipt) => receipt.taskId === item.id && receipt.execution !== void 0).sort((a, b) => a.attempt - b.attempt || a.recordedAt.localeCompare(b.recordedAt)).flatMap((receipt) => receipt.execution ? [receipt.execution] : []);
230
- if (item.execution) executions.push(item.execution);
231
- for (const execution of executions) {
232
- if (history.some((entry) => entry.execution.provider === execution.provider && entry.execution.sessionId === execution.sessionId)) continue;
233
- const continuation = session.continuations?.find((entry) => entry.taskId === item.id);
234
- const attachments = continuation ? continuation.attachments ?? [] : item.id === root?.id ? session.attachments : void 0;
235
- history.push({
236
- taskId: item.id,
237
- agent,
238
- message,
239
- execution,
240
- ...attachments ? { attachments } : {}
241
- });
242
- }
422
+ const byChange = /* @__PURE__ */ new Map();
423
+ for (const observation of observations) {
424
+ if (!changeIds.has(observation.changeId) || byChange.has(observation.changeId)) continue;
425
+ byChange.set(observation.changeId, observation);
243
426
  }
244
- const activeAgent = task ? taskAgent(session, task) : session.agent;
245
- const execution = (activeAgent ? task?.execution ?? (task === null ? session.execution : void 0) : void 0) ?? null;
246
- const dependencyTaskIds = (task ? await stores.graphs.get(task.rootTaskId) : null)?.nodes.find((node) => node.taskId === task?.id)?.dependencies;
247
- const signal = root?.origin.signalId ? await stores.signals.get(root.origin.signalId) : null;
248
- const changeId = task?.changeId ?? root?.changeId ?? session.changeId;
249
- const change = changeId ? snapshot?.changes ? snapshot.changes.get(changeId) ?? null : await stores.changes.get(changeId) : null;
250
- const delegated = workflowTasks(session, byId.values());
251
- const hasWorkflowFailure = delegated.some((item) => item.state === "failed");
252
- const canCancel = task === null || !["succeeded", "cancelled"].includes(task.state) || delegated.some((item) => ![
253
- "succeeded",
254
- "failed",
255
- "cancelled"
256
- ].includes(item.state));
257
- const humanAttention = question ? "question" : workflowAttention(session, byId.values()) ?? attention(task, change);
258
427
  return {
259
- session: {
260
- ...session,
261
- continuations: session.continuations?.map(({ context: _context, ...item }) => item)
428
+ pullRequests: (await mapConcurrent(targets, async ({ change: observed, repository }) => {
429
+ const change = await stores.changes.get(observed.id) ?? observed;
430
+ const observation = byChange.get(change.id) ?? {
431
+ changeId: change.id,
432
+ state: null,
433
+ sync: "unavailable",
434
+ checkedAt,
435
+ syncMessage: "Live pull request status was not returned by the configured reader."
436
+ };
437
+ const state = (change.state === "merged" || change.state === "deployed" || change.state === "rolled_out" || change.state === "rolled_back" ? "merged" : change.state === "closed" ? "closed" : null) ?? observation.state;
438
+ const verification = change.state === "verifying" ? "pending" : change.state === "closed" ? "unknown" : "passed";
439
+ const reviewReadiness = state === "draft" ? "draft" : state === "merged" ? "complete" : state === "closed" ? "closed" : change.state === "verifying" ? "not_ready" : change.state === "ready" ? "ready" : "unknown";
440
+ return sessionPullRequestSchema.parse({
441
+ changeId: change.id,
442
+ repositoryId: repository.id,
443
+ repository: repository.slug,
444
+ number: change.prNumber,
445
+ url: change.prUrl,
446
+ state,
447
+ changeState: change.state,
448
+ verification,
449
+ reviewReadiness,
450
+ sync: observation.sync,
451
+ observedAt: observation.observedAt,
452
+ checkedAt: observation.checkedAt,
453
+ syncMessage: observation.syncMessage
454
+ });
455
+ })).sort((a, b) => a.repository.localeCompare(b.repository) || a.number - b.number),
456
+ truncated
457
+ };
458
+ }
459
+ async function historyPage(session, query) {
460
+ const page = await stores.conversations.history(session.id, query);
461
+ const items = (await mapConcurrent(page.items, async (entry) => await accessibleExecution(session, entry) ? entry : null)).filter((entry) => entry !== null);
462
+ return {
463
+ ...page,
464
+ items
465
+ };
466
+ }
467
+ async function accessibleExecution(session, entry) {
468
+ if (!agents.some((agent) => agent.id === entry.agent)) return false;
469
+ if (!entry.taskId) return true;
470
+ if (!session.taskId) return false;
471
+ const task = await stores.tasks.get(entry.taskId);
472
+ if (!task || await taskAgent(session, task) !== entry.agent) return false;
473
+ if (task.repositoryIds.length !== 1 || task.repositoryIds[0] !== session.repositoryId) return false;
474
+ if (session.attached && !await mayAccessTask(session.owner, task)) return false;
475
+ return true;
476
+ }
477
+ async function view(session, _snapshot, detail = "full", includePullRequests = false, signal = new AbortController().signal) {
478
+ const storedSummary = await stores.conversations.get(session.id);
479
+ const summary = storedSummary ? await authorizedSummary(storedSummary, session.owner) : null;
480
+ if (!summary) throw new ApiError(503, "Conversation projection is unavailable");
481
+ const [task, question, notifications, history, related] = await Promise.all([
482
+ summary.task ? stores.tasks.get(summary.task.id) : null,
483
+ summary.question ? stores.tasks.get(summary.question.id) : null,
484
+ detail === "full" ? stores.sessions.listNotifications(session.id, { limit: 30 }) : {
485
+ items: [],
486
+ nextCursor: null
487
+ },
488
+ detail === "full" ? historyPage(session, { limit: 20 }) : {
489
+ items: [],
490
+ nextCursor: null
262
491
  },
492
+ detail === "full" ? stores.tasks.list({
493
+ limit: 30,
494
+ ...relatedQuery(session)
495
+ }) : {
496
+ items: [],
497
+ nextCursor: null
498
+ }
499
+ ]);
500
+ const continuation = task ? await stores.sessions.getContinuation(session.id, task.id) : null;
501
+ const relatedTasks = (await mapConcurrent(related.items, (item) => authorizedRelatedTask(session, item))).filter((item) => item !== null);
502
+ const associated = includePullRequests ? await sessionPullRequests(session, task, signal) : {
503
+ pullRequests: [],
504
+ truncated: false
505
+ };
506
+ const workGroups = detail === "full" ? await sessionWorkGroups(session, task, relatedTasks, associated.pullRequests) : [];
507
+ return sessionViewSchema.parse({
508
+ ...summary,
509
+ session,
263
510
  task,
264
- execution,
511
+ finding: await sessionFinding(session, task),
512
+ question,
513
+ continuation,
514
+ notifications: [...notifications.items].reverse(),
515
+ notificationsNextCursor: notifications.nextCursor,
516
+ history: detail === "full" && !session.external ? [...history.items].reverse().filter((item) => item.execution.sessionId !== summary.execution?.sessionId || item.execution.provider !== summary.execution?.provider) : void 0,
517
+ historyNextCursor: history.nextCursor,
265
518
  relatedTasks,
266
- activeAgent,
267
- dependencyTaskIds,
268
- source: session.attached ? signal?.source ?? "factory" : void 0,
269
- prUrl: change?.prUrl,
270
- ...humanAttention ? { attention: humanAttention } : {},
271
- hasWorkflowFailure,
272
- canCancel,
273
- canFollowUp: options.continuationAgent !== void 0 && root !== null,
274
- history: includeHistory ? history.filter((entry) => entry.execution.sessionId !== execution?.sessionId || entry.execution.provider !== execution?.provider) : void 0,
275
- question
276
- };
519
+ relatedTasksNextCursor: related.nextCursor,
520
+ pullRequests: associated.pullRequests,
521
+ pullRequestsTruncated: associated.truncated,
522
+ workGroups,
523
+ prUrl: summary.prUrl ?? associated.pullRequests.at(-1)?.url
524
+ });
277
525
  }
278
526
  async function listSessions(url, owner, repositories) {
279
- const scope = z.enum(["all", "interactive"]).parse(url.searchParams.get("scope") ?? "all");
280
- const detail = z.enum(["full", "summary"]).parse(url.searchParams.get("detail") ?? "full");
281
- const limit = url.searchParams.has("limit") ? z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit")) : void 0;
282
527
  const filters = z.object({
283
- query: z.string().trim().max(200).optional().describe("Case-insensitive terms in the request, identifiers, agent, repository, or source."),
284
- repository: z.string().min(1).max(200).optional().describe("Configured owner/repository slug."),
285
- agent: z.string().min(1).max(31).optional().describe("Current executing agent."),
286
- source: z.literal("automatic").optional().describe("Only interactions attached to automatic work."),
287
- state: z.union([taskStateSchema, z.literal("conversation")]).optional().describe("Current Task state, or conversation for taskless sessions."),
288
- attention: z.enum(["true", "false"]).optional().describe("Only work requiring human attention when true."),
289
- topLevel: z.enum(["true", "false"]).optional().describe("Only Task entrypoints without a parent Task; excludes taskless conversations."),
290
- includeTotal: z.enum(["true", "false"]).optional().describe("Include the authorized matching count before the cursor.")
528
+ detail: z.enum(["full", "summary"]).optional(),
529
+ scope: z.enum(["all", "interactive"]).default("all"),
530
+ limit: z.coerce.number().int().min(1).max(100).default(30),
531
+ cursor: z.string().max(8192).optional(),
532
+ query: z.string().trim().max(200).optional(),
533
+ repository: repositorySlugSchema.optional(),
534
+ agent: z.string().min(1).max(31).optional(),
535
+ source: z.literal("automatic").optional(),
536
+ state: z.union([taskStateSchema, z.literal("conversation")]).optional(),
537
+ attention: z.enum(["true", "false"]).optional(),
538
+ topLevel: z.enum(["true", "false"]).optional(),
539
+ includeTotal: z.enum(["true", "false"]).optional()
291
540
  }).parse(Object.fromEntries(url.searchParams));
292
- let cursor;
293
- if (url.searchParams.has("cursor")) {
294
- if (limit === void 0) throw new ApiError(400, "Session cursor requires a limit");
295
- try {
296
- cursor = z.tuple([z.iso.datetime(), z.string().min(1).max(200)]).parse(JSON.parse(Buffer.from(url.searchParams.get("cursor"), "base64url").toString()));
297
- } catch {
298
- throw new ApiError(400, "Invalid session cursor");
299
- }
300
- }
301
- const enabled = (id) => {
302
- const repository = repositories.get(id);
303
- return repository?.enabled && options.repositories.includes(repository.slug);
304
- };
541
+ const enabled = [...repositories.values()].filter((repo) => repo.enabled && options.repositories.includes(repo.slug));
542
+ const selected = filters.repository ? enabled.find((repo) => repo.slug === filters.repository) : void 0;
543
+ if (filters.repository && !selected) throw new ApiError(403, "Repository is not configured");
305
544
  const requestedIds = url.searchParams.get("ids");
306
545
  if (requestedIds !== null) {
307
- if (detail !== "summary" || [...url.searchParams.keys()].some((name) => name !== "ids" && name !== "detail")) throw new ApiError(400, "Session ids require detail=summary and cannot be combined with list filters");
546
+ if ([...url.searchParams.keys()].some((key) => key !== "ids" && key !== "detail")) throw new ApiError(400, "Session ids cannot be combined with list filters");
308
547
  const ids = [...new Set(requestedIds.split(",").filter(Boolean).map((id) => agentSessionIdSchema.parse(id)))];
309
548
  if (ids.length === 0 || ids.length > 50) throw new ApiError(400, "Session ids must contain 1–50 session IDs");
310
549
  const sessions = await mapConcurrent(ids, async (id) => {
311
550
  try {
312
- return await owned(id, owner);
551
+ await owned(id, owner);
552
+ const summary = await stores.conversations.get(id);
553
+ const authorized = summary ? await authorizedSummary(summary, owner) : null;
554
+ return authorized ? await findingSummary(authorized) : null;
313
555
  } catch (error) {
314
556
  if (error instanceof ApiError && error.status === 404) return null;
315
557
  throw error;
316
558
  }
317
559
  });
318
560
  return json({
319
- sessions: await mapConcurrent(sessions.filter((session) => session !== null), (session) => view(session, void 0, "summary")),
561
+ sessions: sessions.filter((session) => session !== null),
320
562
  nextCursor: null
321
563
  });
322
564
  }
323
- const stored = (await stores.sessions.summaries()).filter((session) => session.owner === owner);
324
- const sessions = stored.filter((session) => (scope === "all" || !session.attached && !session.external) && agents.some((agent) => agent.id === session.agent) && (session.repositoryId === void 0 || enabled(session.repositoryId)) && (session.external === void 0 || session.external.repositoryIds.every(enabled)));
325
- const tasks = scope === "all" && options.canAccessTask || filters.attention === "true" || sessions.some((session) => session.taskId !== void 0) ? await stores.tasks.list() : [];
326
- const byId = new Map(tasks.map((task) => [task.id, task]));
327
- const candidates = (await mapConcurrent(sessions, async (session) => {
328
- if (!session.attached) return session;
329
- const task = session.taskId ? byId.get(session.taskId) : void 0;
330
- return task && await options.canAccessTask?.({ id: owner }, task) ? session : null;
331
- })).filter((session) => session !== null).map((session) => ({
332
- createdAt: session.createdAt,
333
- key: session.attached ? `task:${session.taskId}` : `session:${session.id}`,
334
- session,
335
- load: detail === "summary" ? async () => session : async () => await stores.sessions.get(session.id) ?? session
336
- }));
337
- if (scope === "all" && options.canAccessTask) {
338
- const attached = new Set(stored.filter((session) => session.attached).map((session) => session.taskId));
339
- const discovered = await mapConcurrent(tasks, async (task) => {
340
- if (attached.has(task.id) || task.replyTo.channel === "api" || task.repositoryIds.length !== 1 || !enabled(task.repositoryIds[0])) return null;
341
- const binding = task.work.route;
342
- const route = binding ? options.routes.get(binding) : void 0;
343
- if (!route || !agents.some((agent) => agent.id === route.agent) || !await options.canAccessTask?.({ id: owner }, task)) return null;
344
- return {
345
- createdAt: task.createdAt,
346
- key: `task:${task.id}`,
347
- task,
348
- load: async () => {
349
- const signal = task.origin.signalId ? await stores.signals.get(task.origin.signalId) : null;
350
- return stores.sessions.attachTask({
351
- owner,
352
- taskId: task.id,
353
- agent: route.agent,
354
- message: `${route.id.replaceAll("-", " ")} · ${repositories.get(task.repositoryIds[0]).slug} · ${signal?.source ?? "factory"} · ${task.replyTo.address}`.slice(0, 16e3)
355
- });
356
- }
357
- };
358
- });
359
- candidates.push(...discovered.filter((candidate) => candidate !== null));
360
- }
361
- const filtering = filters.query || filters.repository || filters.agent || filters.state || filters.source || filters.attention === "true" || filters.topLevel === "true";
362
- const linkedTasks = /* @__PURE__ */ new Map();
363
- if (filtering) for (const task of tasks) {
364
- if (task.replyTo.channel !== "api") continue;
365
- const linked = linkedTasks.get(task.replyTo.address) ?? [];
366
- linked.push(task);
367
- linkedTasks.set(task.replyTo.address, linked);
368
- }
369
- const changes = filters.attention === "true" ? new Map((await stores.changes.list()).map((change) => [change.id, change])) : void 0;
370
- const matches = (filtering ? await mapConcurrent(candidates, async (candidate) => {
371
- const session = candidate.session;
372
- const work = session ? sessionTasks(session, byId, linkedTasks.get(session.id) ?? []) : null;
373
- const task = work?.tasks.at(-1) ?? candidate.task ?? null;
374
- const root = work?.root ?? candidate.task;
375
- const entryTask = session?.taskId ? byId.get(session.taskId) : candidate.task;
376
- if (filters.topLevel === "true" && (entryTask === void 0 || entryTask.parentTaskId !== void 0)) return null;
377
- const slugs = [.../* @__PURE__ */ new Set([
378
- ...root?.repositoryIds ?? [],
379
- ...task?.repositoryIds ?? [],
380
- ...session?.repositoryId ? [session.repositoryId] : [],
381
- ...session?.external?.repositoryIds ?? []
382
- ])].flatMap((id) => repositories.get(id)?.slug ?? []);
383
- const agent = session ? task ? taskAgent(session, task) : session.agent : task ? taskRoute(task)?.agent : void 0;
384
- if (filters.repository && !slugs.some((slug) => slug === filters.repository)) return null;
385
- if (filters.agent && agent !== filters.agent) return null;
386
- if (filters.state && (task?.state ?? "conversation") !== filters.state) return null;
387
- if (filters.source && session && !session.attached && !session.external) return null;
388
- if (filters.attention === "true") {
389
- const changeId = task?.changeId ?? root?.changeId ?? session?.changeId;
390
- const change = changeId ? changes?.get(changeId) ?? null : null;
391
- if (session?.external ? !(await view(session, void 0, "summary")).attention : !attention(task, change) && !(session && workflowAttention(session, linkedTasks.get(session.id) ?? [])) && !conversationQuestion([...work?.tasks ?? [], ...session ? workflowTasks(session, linkedTasks.get(session.id) ?? []) : []], byId)) return null;
392
- }
393
- if (filters.query) {
394
- const signal = root?.origin.signalId ? await stores.signals.get(root.origin.signalId) : null;
395
- const route = root ? taskRoute(root) : void 0;
396
- const searchable = [
397
- session?.id,
398
- session?.message,
399
- ...session?.attachments?.map((item) => item.filename) ?? [],
400
- task?.id,
401
- agent,
402
- ...slugs,
403
- route?.id.replaceAll("-", " "),
404
- signal?.source ?? (session?.attached || candidate.task ? "factory" : ""),
405
- root?.replyTo.address,
406
- session?.external?.replyTo.channel,
407
- session?.external?.replyTo.address
408
- ].join(" ").toLowerCase();
409
- if (!filters.query.toLowerCase().split(/\s+/u).every((word) => searchable.includes(word))) return null;
410
- }
411
- return candidate;
412
- }) : candidates).filter((candidate) => candidate !== null);
413
- const ordered = matches.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.key.localeCompare(a.key)).filter((candidate) => !cursor || candidate.createdAt.localeCompare(cursor[0]) < 0 || candidate.createdAt === cursor[0] && candidate.key.localeCompare(cursor[1]) < 0);
414
- const page = limit === void 0 ? ordered : ordered.slice(0, limit);
415
- const visible = await mapConcurrent(page, (candidate) => candidate.load());
416
- const receipts = detail === "summary" ? null : visible.some((session) => session.taskId !== void 0) ? await stores.receipts.list() : [];
417
- const last = page.at(-1);
565
+ const shared = options.sharedWorkspaceOwner === owner;
566
+ const page = await stores.conversations.list({
567
+ owner,
568
+ repositoryId: selected?.id,
569
+ attention: filters.attention === "true",
570
+ limit: filters.limit,
571
+ cursor: filters.cursor,
572
+ search: filters.query || void 0,
573
+ includeTotal: filters.includeTotal === "true" && !options.canAccessTask,
574
+ match: {
575
+ ...filters.scope === "interactive" ? { interactive: true } : {},
576
+ ...filters.source ? { automatic: true } : {},
577
+ ...!shared && !options.canAccessTask ? { attached: false } : {},
578
+ ...filters.agent ? { agent: filters.agent } : {},
579
+ ...filters.state ? { state: filters.state } : {},
580
+ ...filters.topLevel === "true" ? { topLevel: true } : {}
581
+ },
582
+ fields: [
583
+ {
584
+ path: ["repositoryIds"],
585
+ test: "subset",
586
+ value: enabled.map((repo) => repo.id)
587
+ },
588
+ {
589
+ path: ["sessionAgent"],
590
+ test: "oneOf",
591
+ value: agents.map((agent) => agent.id)
592
+ },
593
+ {
594
+ path: ["agent"],
595
+ test: "oneOf",
596
+ value: agents.map((agent) => agent.id)
597
+ },
598
+ {
599
+ path: ["routeAgent"],
600
+ test: "oneOf",
601
+ value: [
602
+ ...agents.map((agent) => JSON.stringify(["", agent.id])),
603
+ ...options.continuationAgent ? [JSON.stringify(["conversation@1", options.continuationAgent])] : [],
604
+ ...options.routes.routes.filter((route) => agents.some((agent) => agent.id === route.agent)).map((route) => JSON.stringify([`${route.id}@${route.version}`, route.agent]))
605
+ ]
606
+ }
607
+ ]
608
+ });
609
+ const sessions = options.canAccessTask ? (await mapConcurrent(page.items, (summary) => authorizedSummary(summary, owner))).filter((summary) => summary !== null) : page.items;
418
610
  return json({
419
- sessions: await mapConcurrent(visible, (session) => view(session, {
420
- tasks: byId,
421
- receipts,
422
- changes
423
- }, detail)),
424
- ...filters.includeTotal === "true" ? { totalCount: matches.length } : {},
425
- ...limit === void 0 ? {} : { nextCursor: last && ordered.length > page.length ? Buffer.from(JSON.stringify([last.createdAt, last.key])).toString("base64url") : null }
611
+ sessions: await mapConcurrent(sessions, findingSummary),
612
+ nextCursor: page.nextCursor,
613
+ ...page.totalCount === void 0 ? {} : { totalCount: page.totalCount }
426
614
  });
427
615
  }
428
616
  async function assertRepository(id) {
@@ -438,7 +626,7 @@ function createFactoryApi(options) {
438
626
  if (session.external) await mapConcurrent(session.external.repositoryIds, assertRepository);
439
627
  if (session.attached) {
440
628
  const task = session.taskId ? await stores.tasks.get(session.taskId) : null;
441
- if (!task || !await options.canAccessTask?.({ id: owner }, task)) throw new ApiError(404, "Session not found");
629
+ if (!task || !await mayAccessTask(owner, task)) throw new ApiError(404, "Session not found");
442
630
  }
443
631
  return session;
444
632
  }
@@ -451,15 +639,41 @@ function createFactoryApi(options) {
451
639
  }
452
640
  } else {
453
641
  const boundTask = session.taskId === current.task.id ? current.task : session.taskId ? await stores.tasks.get(session.taskId) : null;
454
- const tasks = [...boundTask ? [boundTask] : [], ...current.relatedTasks ?? []];
455
- const task = recoveryTaskId ? tasks.find((item) => item.id === recoveryTaskId) : tasks.find((item) => item.state === "queued" && item.answerEventIds?.length) ?? current.task;
642
+ const recovery = recoveryTaskId ? await stores.tasks.get(taskIdSchema.parse(recoveryTaskId)) : null;
643
+ if (recoveryTaskId && (!recovery || recovery.id !== session.taskId && (recovery.replyTo.channel !== "api" || recovery.replyTo.address !== session.id || recovery.repositoryIds.length !== 1 || recovery.repositoryIds[0] !== session.repositoryId))) throw new ApiError(403, "Recovery Task does not belong to this interaction");
644
+ const answered = recoveryTaskId ? null : (await stores.tasks.list({
645
+ limit: 1,
646
+ match: {
647
+ replyTo: {
648
+ channel: "api",
649
+ address: session.id
650
+ },
651
+ state: "queued"
652
+ },
653
+ fields: [
654
+ {
655
+ path: ["repositoryIds"],
656
+ test: "eq",
657
+ value: session.repositoryId ? [session.repositoryId] : []
658
+ },
659
+ {
660
+ path: ["work", "route"],
661
+ test: "exists"
662
+ },
663
+ {
664
+ path: ["answerEventIds"],
665
+ test: "notEmpty"
666
+ }
667
+ ]
668
+ })).items[0];
669
+ const task = recovery ?? (boundTask?.state === "queued" && boundTask.answerEventIds?.length ? boundTask : answered) ?? current.task;
456
670
  if (task?.state === "queued") {
457
- if (!taskAgent(session, task)) throw new ApiError(409, "Task has no configured agent route");
671
+ if (!await taskAgent(session, task)) throw new ApiError(409, "Task has no configured agent route");
458
672
  const answers = await Promise.all((task.answerEventIds ?? []).map((id) => stores.answers.get(id)));
459
673
  const message = [
460
674
  `You are assigned Factory task ${task.id}. Read it before starting.`,
461
675
  session.changeId === void 0 ? "" : `Existing change: ${session.changeId}.`,
462
- session.continuations?.find((item) => item.taskId === task.id)?.message ?? session.message,
676
+ (await stores.sessions.getContinuation(session.id, task.id))?.message ?? session.message,
463
677
  ...answers.flatMap((answer) => answer === null ? [] : [`${answer.question}\nAnswer: ${answer.answer}`])
464
678
  ].filter(Boolean).join("\n\n");
465
679
  try {
@@ -468,14 +682,14 @@ function createFactoryApi(options) {
468
682
  route: task.work.route,
469
683
  budget: options.budget,
470
684
  launch: async (running) => {
471
- const continuation = session.continuations?.find((item) => item.taskId === running.id);
472
- if (continuation && (continuation.agent !== options.continuationAgent || taskAgent(session, running) !== continuation.agent)) throw new ApiError(409, "Conversation agent is no longer configured");
473
- if (continuation) return start(executionSession(session, running), running, await continuationBrief(session, current, continuation.message, running.id), `factory-task:${running.id}:${running.attempt}`);
685
+ const continuation = await stores.sessions.getContinuation(session.id, running.id);
686
+ if (continuation && (continuation.agent !== options.continuationAgent || await taskAgent(session, running) !== continuation.agent)) throw new ApiError(409, "Conversation agent is no longer configured");
687
+ if (continuation) return start(await executionSession(session, running), running, await continuationBrief(session, current, continuation.message, running.id), `factory-task:${running.id}:${running.attempt}`);
474
688
  if (session.attached || running.id !== session.taskId) {
475
689
  if (!options.resumeTask) throw new ApiError(409, "Recovery for existing work is not configured");
476
690
  return options.resumeTask(running);
477
691
  }
478
- return start(executionSession(session, running), running, message, `factory-task:${running.id}:${running.attempt}`);
692
+ return start(await executionSession(session, running), running, message, `factory-task:${running.id}:${running.attempt}`);
479
693
  }
480
694
  });
481
695
  } catch (error) {
@@ -575,11 +789,20 @@ function createFactoryApi(options) {
575
789
  const repository = await stores.repositories.get(repositoryId);
576
790
  if (!repository) throw new ApiError(404, "Repository not found");
577
791
  await assertRepository(repositoryId);
578
- const active = (await stores.tasks.list()).find((task) => task.kind === "security_scan" && task.repositoryIds.includes(repository.id) && ![
579
- "succeeded",
580
- "failed",
581
- "cancelled"
582
- ].includes(task.state));
792
+ const active = (await stores.tasks.list({
793
+ limit: 1,
794
+ match: [
795
+ "queued",
796
+ "running",
797
+ "needs_human",
798
+ "stalled",
799
+ "verifying"
800
+ ].map((state) => ({
801
+ kind: "security_scan",
802
+ repositoryIds: [repository.id],
803
+ state
804
+ }))
805
+ })).items[0];
583
806
  if (active) return json({
584
807
  taskId: active.id,
585
808
  reused: true
@@ -609,7 +832,7 @@ function createFactoryApi(options) {
609
832
  }, 202);
610
833
  }
611
834
  async function continuationBrief(session, current, message, taskId) {
612
- const continuation = session.continuations?.find((item) => item.taskId === taskId);
835
+ const continuation = await stores.sessions.getContinuation(session.id, taskId);
613
836
  const saved = continuation?.context;
614
837
  const historicalImages = [];
615
838
  const task = await stores.tasks.get(taskId);
@@ -625,9 +848,9 @@ function createFactoryApi(options) {
625
848
  execution: current.execution
626
849
  });
627
850
  const sections = await Promise.all(entries.slice(-12).map(async (entry, index) => {
628
- const task = entry.taskId ? await stores.tasks.get(entry.taskId) : null;
629
851
  try {
630
- const snapshot = await materializedTranscript(session, task, entry.agent, entry.execution, AbortSignal.timeout(15e3));
852
+ const snapshot = (await stores.transcripts.page(entry.execution, { limit: 100 }))?.snapshot;
853
+ if (!snapshot || snapshot.cursor === 0) throw new ApiError(404, "Transcript has not been persisted yet");
631
854
  const transcript = new Transcript();
632
855
  transcript.hydrate(snapshot);
633
856
  historicalImages[index] = transcript.blocks.flatMap((block) => block.attachments ?? []);
@@ -642,7 +865,7 @@ function createFactoryApi(options) {
642
865
  }));
643
866
  const candidates = [
644
867
  ...session.attachments ?? [],
645
- ...session.continuations?.slice(-12).flatMap((item) => item.contextAttachments ?? item.attachments ?? []) ?? [],
868
+ ...(await stores.sessions.listContinuations(session.id, { limit: 12 })).items.flatMap((item) => item.contextAttachments ?? item.attachments ?? []),
646
869
  ...historicalImages.flat(),
647
870
  ...continuation?.attachments ?? []
648
871
  ];
@@ -688,38 +911,32 @@ function createFactoryApi(options) {
688
911
  attachments: selected
689
912
  }));
690
913
  }
691
- async function materializedTranscript(session, task, agent, execution, signal) {
692
- const cached = await stores.transcripts.get(execution);
693
- if (cached !== null && cached.state !== "running" && cached.state !== "connecting") return cached;
694
- const transcript = new Transcript();
695
- if (cached !== null) transcript.hydrate(cached);
696
- const path = `/stream?${new URLSearchParams({
697
- startIndex: String(transcript.cursor),
698
- includeTailIndex: "1"
699
- })}`;
700
- const response = await options.forward({
701
- session: executionSession(session, task, agent),
702
- execution,
703
- path,
704
- request: new Request("http://factory/transcript", { signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)]) })
705
- });
706
- if (!response.ok || !response.body) {
707
- await response.body?.cancel();
708
- throw new ApiError(response.status || 502, response.status === 404 ? "Execution transcript is unavailable on this deployment. It may belong to another deployment or have expired." : "Execution transcript is unavailable");
914
+ async function transcriptResponse(url, execution) {
915
+ const blockId = url.searchParams.get("blockId");
916
+ if (blockId !== null) {
917
+ z.string().min(1).max(1e3).parse(blockId);
918
+ if (url.searchParams.has("cursor") || url.searchParams.has("limit")) throw new ApiError(400, "Block reads cannot include page parameters");
919
+ const block = await stores.transcripts.block(execution, blockId);
920
+ if (!block) throw new ApiError(404, "Transcript block not found");
921
+ return json(block);
709
922
  }
710
- const tailValue = response.headers.get("x-eve-stream-tail-index");
711
- const tail = tailValue === null ? NaN : Number(tailValue);
712
- if (!Number.isSafeInteger(tail) || tail < -1) {
713
- await response.body.cancel();
714
- throw new Error("Execution transcript did not include a valid durable cursor");
715
- }
716
- if (transcript.cursor <= tail) for await (const event of readEvents(response.body)) {
717
- transcript.accept(event);
718
- if (transcript.cursor === tail + 1) break;
719
- }
720
- else await response.body.cancel();
721
- if (transcript.cursor !== tail + 1) throw new Error("Execution transcript closed before its durable cursor");
722
- return stores.transcripts.put(execution, transcript.snapshot());
923
+ const limit = z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit") ?? 30);
924
+ const cursor = url.searchParams.get("cursor") ?? void 0;
925
+ const page = await stores.transcripts.page(execution, {
926
+ limit,
927
+ cursor
928
+ });
929
+ return page ? json(page) : json({
930
+ snapshot: {
931
+ blocks: [],
932
+ inputs: [],
933
+ cursor: 0,
934
+ state: "connecting",
935
+ tokens: 0
936
+ },
937
+ nextCursor: null,
938
+ totalBlocks: 0
939
+ }, 202);
723
940
  }
724
941
  async function proxy(session, request, suffix, payload) {
725
942
  const current = await view(session, void 0, "summary");
@@ -747,7 +964,7 @@ function createFactoryApi(options) {
747
964
  ...request.method === "POST" ? { body: JSON.stringify(payload !== null && typeof payload === "object" ? Object.fromEntries(Object.entries(payload).filter(([key]) => key !== "executionId" && key !== "operationId")) : {}) } : {}
748
965
  });
749
966
  const result = await options.forward({
750
- session: executionSession(session, current.task),
967
+ session: await executionSession(session, current.task),
751
968
  execution: current.execution,
752
969
  path: suffix,
753
970
  request: upstream
@@ -803,36 +1020,50 @@ function createFactoryApi(options) {
803
1020
  if (path.length > 3) throw new ApiError(404, "API route not found");
804
1021
  if (resource === "intake") {
805
1022
  if (!auth.actor || !(options.operators ?? []).includes(auth.actor)) throw new ApiError(403, "Intake requires an individually authenticated configured operator");
806
- const repos = (await stores.repositories.list()).filter((repo) => repo.enabled && options.repositories.includes(repo.slug));
1023
+ const repos = (await configuredRepositories()).filter((repo) => repo.enabled && options.repositories.includes(repo.slug));
807
1024
  const repository = url.searchParams.get("repository");
808
1025
  if (repository && !options.repositories.some((slug) => slug === repository)) throw new ApiError(403, "Repository is not configured");
809
1026
  const allowed = new Map(repos.filter((repo) => !repository || repo.slug === repository).map((repo) => [repo.id, repo.slug]));
810
- const tasks = await stores.tasks.list();
811
- const view = (signal) => {
1027
+ const view = async (signal) => {
1028
+ const tasks = await stores.tasks.list({
1029
+ limit: 30,
1030
+ match: { origin: { signalId: signal.id } }
1031
+ });
812
1032
  const { payload: _payload, ...record } = signal;
813
1033
  return intakeViewSchema.parse({
814
1034
  ...record,
815
1035
  repository: allowed.get(signal.repositoryId),
816
- taskIds: tasks.filter((task) => task.origin.signalId === signal.id).map((task) => task.id)
1036
+ taskIds: tasks.items.map((task) => task.id),
1037
+ tasksNextCursor: tasks.nextCursor
817
1038
  });
818
1039
  };
819
1040
  if (path.length === 1 && request.method === "GET") {
820
1041
  const limit = z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit") ?? 100);
821
1042
  const cursor = url.searchParams.get("cursor");
822
- const remaining = (await stores.signals.list()).filter((signal) => allowed.has(signal.repositoryId)).sort((a, b) => a.id.localeCompare(b.id)).filter((signal) => !cursor || signal.id > cursor);
823
- const items = remaining.slice(0, limit).map(view);
1043
+ if (allowed.size === 0) return json({
1044
+ items: [],
1045
+ nextCursor: null
1046
+ });
1047
+ const page = await stores.signals.list({
1048
+ limit,
1049
+ cursor: cursor ?? void 0,
1050
+ match: [...allowed.keys()].map((repositoryId) => ({ repositoryId }))
1051
+ });
824
1052
  return json({
825
- items,
826
- nextCursor: remaining.length > limit ? items.at(-1).id : null
1053
+ items: await mapConcurrent(page.items, view),
1054
+ nextCursor: page.nextCursor
827
1055
  });
828
1056
  }
829
1057
  if (id && path.length === 3 && action === "retry" && request.method === "POST") {
830
1058
  const signal = await stores.signals.get(signalIdSchema.parse(id));
831
1059
  if (!signal || !allowed.has(signal.repositoryId)) throw new ApiError(404, "Intake not found");
832
1060
  const input = retryIntakeSchema.parse(await body(request));
833
- if (signal.outcome !== "pending" || signal.triage?.state !== "blocked" || signal.triage.attempt !== input.expectedAttempt || tasks.some((task) => task.origin.signalId === signal.id)) throw new ApiError(409, "Only the observed blocked attempt with no linked task can be retried; refresh intake");
1061
+ if (signal.outcome !== "pending" || signal.triage?.state !== "blocked" || signal.triage.attempt !== input.expectedAttempt || (await stores.tasks.list({
1062
+ limit: 1,
1063
+ match: { origin: { signalId: signal.id } }
1064
+ })).items.length > 0) throw new ApiError(409, "Only the observed blocked attempt with no linked task can be retried; refresh intake");
834
1065
  if (!options.retryIntake) throw new ApiError(409, "This factory has no retry launcher for this intake source");
835
- return json(view(await options.retryIntake(signal, {
1066
+ return json(await view(await options.retryIntake(signal, {
836
1067
  ...input,
837
1068
  by: auth.actor
838
1069
  })));
@@ -885,7 +1116,7 @@ function createFactoryApi(options) {
885
1116
  reason: "External activity is not configured."
886
1117
  });
887
1118
  const allowedRepositories = async () => {
888
- const disabled = new Set((await stores.repositories.list()).filter((repo) => !repo.enabled).map((repo) => repo.slug));
1119
+ const disabled = new Set((await configuredRepositories()).filter((repo) => !repo.enabled).map((repo) => repo.slug));
889
1120
  return options.repositories.filter((slug) => !disabled.has(slug));
890
1121
  };
891
1122
  const repositories = await allowedRepositories();
@@ -908,7 +1139,7 @@ function createFactoryApi(options) {
908
1139
  if (resource === "agents") return json({ agents });
909
1140
  if (resource === "repositories") return json({
910
1141
  repositories: options.repositories,
911
- details: (await stores.repositories.list()).filter((repo) => options.repositories.includes(repo.slug)).map(({ id, slug }) => ({
1142
+ details: (await configuredRepositories()).filter((repo) => options.repositories.includes(repo.slug)).map(({ id, slug }) => ({
912
1143
  id,
913
1144
  slug
914
1145
  }))
@@ -916,14 +1147,44 @@ function createFactoryApi(options) {
916
1147
  const requestedRepositoryValue = resource === "changes" ? url.searchParams.get("repository") : null;
917
1148
  const requestedRepository = requestedRepositoryValue === null ? null : repositorySlugSchema.parse(requestedRepositoryValue);
918
1149
  if (requestedRepository !== null && !options.repositories.includes(requestedRepository)) throw new ApiError(403, "Repository is not configured");
919
- const repositories = new Map((await stores.repositories.list()).map((repo) => [repo.id, repo]));
1150
+ const repositories = new Map((await configuredRepositories()).map((repo) => [repo.id, repo]));
920
1151
  const enabledIds = new Set([...repositories.values()].filter((repo) => repo.enabled && options.repositories.includes(repo.slug) && (requestedRepository === null || repo.slug === requestedRepository)).map((repo) => repo.id));
921
- if (resource === "changes") return json({ changes: (await stores.changes.list()).filter((change) => enabledIds.has(change.repositoryId) && (change.state === "verifying" || change.state === "ready")) });
1152
+ const query = {
1153
+ limit: z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit") ?? 30),
1154
+ cursor: url.searchParams.get("cursor") ?? void 0
1155
+ };
1156
+ if (resource === "changes") {
1157
+ if (!enabledIds.size) return json({
1158
+ changes: [],
1159
+ nextCursor: null
1160
+ });
1161
+ const page = await stores.changes.list({
1162
+ ...query,
1163
+ match: [...enabledIds].flatMap((repositoryId) => ["verifying", "ready"].map((state) => ({
1164
+ repositoryId,
1165
+ state
1166
+ })))
1167
+ });
1168
+ return json({
1169
+ changes: page.items,
1170
+ nextCursor: page.nextCursor
1171
+ });
1172
+ }
922
1173
  if (resource === "sessions") return await listSessions(url, auth.id, repositories);
923
1174
  if (resource === "tasks") {
924
- const tasks = [];
925
- for (const task of await stores.tasks.list()) if (task.repositoryIds.every((id) => enabledIds.has(id)) && (task.origin.operator === auth.id || task.replyTo.channel !== "api" && await options.canAccessTask?.(auth, task))) tasks.push(task);
926
- return json({ tasks });
1175
+ const page = await stores.tasks.list({
1176
+ ...query,
1177
+ fields: [{
1178
+ path: ["repositoryIds"],
1179
+ test: "subset",
1180
+ value: [...enabledIds]
1181
+ }]
1182
+ });
1183
+ const tasks = (await mapConcurrent(page.items, async (task) => task.origin.operator === auth.id || task.replyTo.channel !== "api" && await mayAccessTask(auth.id, task) ? task : null)).filter((task) => task !== null);
1184
+ return json({
1185
+ tasks,
1186
+ nextCursor: page.nextCursor
1187
+ });
927
1188
  }
928
1189
  }
929
1190
  if (request.method === "POST" && path.length === 1 && (resource === "signals" || resource === "sessions")) return await create(request, auth.id, resource === "sessions");
@@ -932,10 +1193,11 @@ function createFactoryApi(options) {
932
1193
  if (session.external && request.method === "POST") throw new ApiError(403, "External conversations are read-only; continue in the original channel");
933
1194
  if (request.method === "GET" && action === void 0) {
934
1195
  const detail = z.enum(["full", "summary"]).parse(url.searchParams.get("detail") ?? "full");
935
- return json(await view(session, void 0, detail));
1196
+ return json(await view(session, void 0, detail, true, request.signal));
936
1197
  }
937
1198
  if (request.method === "GET" && (action === "graph" || action === "graph-transcript" || action === "graph-stream")) {
938
- const graph = await readSessionGraph(options, session);
1199
+ const taskId = (action === "graph" ? taskIdSchema.optional() : taskIdSchema).parse(url.searchParams.get("taskId") ?? void 0);
1200
+ const graph = await readSessionGraph(options, session, taskId);
939
1201
  if (action === "graph") {
940
1202
  const current = await view(session, void 0, "summary");
941
1203
  const currentTaskId = graph.nodes.find((node) => node.task.id === current.task?.id)?.task.id ?? null;
@@ -945,7 +1207,6 @@ function createFactoryApi(options) {
945
1207
  nodes: graph.nodes.map(({ session: _session, ...node }) => node)
946
1208
  }));
947
1209
  }
948
- const taskId = taskIdSchema.parse(url.searchParams.get("taskId"));
949
1210
  const node = graph.nodes.find((item) => item.task.id === taskId);
950
1211
  if (!node) throw new ApiError(404, "Task is not part of this graph");
951
1212
  const execution = node.task.execution;
@@ -958,7 +1219,7 @@ function createFactoryApi(options) {
958
1219
  if (value !== null) query.set(name, value);
959
1220
  }
960
1221
  const response = await options.forward({
961
- session: executionSession(node.session, node.task, node.agent),
1222
+ session: await executionSession(node.session, node.task, node.agent),
962
1223
  execution,
963
1224
  path: `/stream?${query}`,
964
1225
  request
@@ -978,10 +1239,10 @@ function createFactoryApi(options) {
978
1239
  }
979
1240
  });
980
1241
  }
981
- const snapshot = await materializedTranscript(node.session, node.task, node.agent, execution, request.signal);
1242
+ const response = await transcriptResponse(url, execution);
982
1243
  const latest = await stores.tasks.get(node.task.id);
983
1244
  if (latest?.execution?.sessionId !== execution.sessionId || latest.execution.provider !== execution.provider) throw new ApiError(409, "Execution changed; refresh the graph.");
984
- return json(snapshot);
1245
+ return response;
985
1246
  }
986
1247
  if (request.method === "GET" && (action === "review" || action === "previews")) {
987
1248
  const current = await view(session, void 0, "summary");
@@ -995,17 +1256,31 @@ function createFactoryApi(options) {
995
1256
  reason: action === "review" ? "Review evidence is not configured for this factory." : "Live previews are not configured for this factory."
996
1257
  });
997
1258
  const reviewTasks = async (value) => {
998
- const delegated = value.session.external ? [] : workflowTasks(value.session, await stores.tasks.forReplyTo({
999
- channel: "api",
1000
- address: value.session.id
1001
- }));
1002
- return [...new Map([
1003
- ...value.task ? [value.task] : [],
1004
- ...value.relatedTasks ?? [],
1005
- ...delegated
1006
- ].map((task) => [task.id, task])).values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
1259
+ const page = value.session.external ? {
1260
+ items: [],
1261
+ nextCursor: null
1262
+ } : await stores.tasks.list({
1263
+ limit: 100,
1264
+ match: { replyTo: {
1265
+ channel: "api",
1266
+ address: value.session.id
1267
+ } },
1268
+ fields: [{
1269
+ path: ["repositoryIds"],
1270
+ test: "eq",
1271
+ value: value.session.repositoryId ? [value.session.repositoryId] : []
1272
+ }, {
1273
+ path: ["question"],
1274
+ test: "missing"
1275
+ }]
1276
+ });
1277
+ return {
1278
+ tasks: [...new Map([...value.task ? [value.task] : [], ...page.items].map((task) => [task.id, task])).values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)),
1279
+ truncated: page.nextCursor !== null
1280
+ };
1007
1281
  };
1008
- const tasks = await reviewTasks(current);
1282
+ const selected = await reviewTasks(current);
1283
+ const tasks = selected.tasks;
1009
1284
  const identity = (value, work) => JSON.stringify([
1010
1285
  value.execution,
1011
1286
  value.task?.id,
@@ -1023,9 +1298,13 @@ function createFactoryApi(options) {
1023
1298
  signal: request.signal
1024
1299
  }));
1025
1300
  const latest = await view(await owned(id, auth.id), void 0, "summary");
1026
- if (identity(current, tasks) !== identity(latest, await reviewTasks(latest))) throw new ApiError(409, "Execution changed; refresh the session before reviewing work");
1301
+ if (identity(current, tasks) !== identity(latest, (await reviewTasks(latest)).tasks)) throw new ApiError(409, "Execution changed; refresh the session before reviewing work");
1027
1302
  if (result.executionId !== executionId) throw new Error("Review reader returned a different execution");
1028
- return json(result);
1303
+ return json(result.state === "ready" && selected.truncated ? {
1304
+ ...result,
1305
+ truncated: true,
1306
+ limitations: [...result.limitations, "Evidence covers the 100 most recent related Tasks and the current Task."]
1307
+ } : result);
1029
1308
  }
1030
1309
  if (request.method === "GET" && action === "files") {
1031
1310
  const query = workspaceFilesRequestSchema.parse({
@@ -1042,7 +1321,7 @@ function createFactoryApi(options) {
1042
1321
  reason: execution ? "File browsing is not configured for this factory." : "The workspace is not available yet."
1043
1322
  });
1044
1323
  const result = workspaceFilesSchema.parse(await options.workspaceFiles({
1045
- session: executionSession(session, current.task),
1324
+ session: await executionSession(session, current.task),
1046
1325
  execution,
1047
1326
  query,
1048
1327
  signal: request.signal
@@ -1061,7 +1340,7 @@ function createFactoryApi(options) {
1061
1340
  reason: execution ? "Workspace inspection is not configured for this factory." : "The workspace is not available yet."
1062
1341
  }));
1063
1342
  const result = workspaceDiffSchema.parse(await options.workspaceDiff({
1064
- session: executionSession(session, current.task),
1343
+ session: await executionSession(session, current.task),
1065
1344
  execution,
1066
1345
  signal: request.signal
1067
1346
  }));
@@ -1070,17 +1349,37 @@ function createFactoryApi(options) {
1070
1349
  if (result.executionId !== execution.sessionId) throw new Error("Workspace inspection returned a different execution");
1071
1350
  return json(result);
1072
1351
  }
1352
+ if (request.method === "GET" && [
1353
+ "history",
1354
+ "notifications",
1355
+ "related-tasks"
1356
+ ].includes(action ?? "")) {
1357
+ const query = {
1358
+ limit: z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit") ?? 30),
1359
+ cursor: url.searchParams.get("cursor") ?? void 0
1360
+ };
1361
+ if (action === "history") return json(await historyPage(session, query));
1362
+ if (action === "notifications") return json(await stores.sessions.listNotifications(session.id, query));
1363
+ const page = await stores.tasks.list({
1364
+ ...query,
1365
+ ...relatedQuery(session)
1366
+ });
1367
+ const items = (await mapConcurrent(page.items, async (task) => session.external ? task.repositoryIds.every((id) => session.external.repositoryIds.includes(id)) && await mayAccessTask(session.owner, task) ? task : null : task.repositoryIds.length === 1 && task.repositoryIds[0] === session.repositoryId ? task : null)).filter((task) => task !== null);
1368
+ return json({
1369
+ items,
1370
+ nextCursor: page.nextCursor
1371
+ });
1372
+ }
1073
1373
  if (request.method === "GET" && action === "transcript") {
1074
1374
  const executionId = z.string().min(1).parse(url.searchParams.get("executionId"));
1075
1375
  if (url.searchParams.get("history") === "1") {
1076
- const entry = (await view(session)).history?.find((item) => item.execution.sessionId === executionId);
1077
- if (!entry) throw new ApiError(404, "Execution is not part of this conversation history");
1078
- const task = entry.taskId ? await stores.tasks.get(entry.taskId) : null;
1079
- return json(await materializedTranscript(session, task, entry.agent, entry.execution, request.signal));
1376
+ const entry = await stores.conversations.execution(session.id, executionId);
1377
+ if (!entry || !await accessibleExecution(session, entry)) throw new ApiError(404, "Execution is not part of this conversation history");
1378
+ return await transcriptResponse(url, entry.execution);
1080
1379
  }
1081
1380
  const current = await view(session, void 0, "summary");
1082
1381
  if (current.execution?.sessionId !== executionId) throw new ApiError(409, "Execution changed; refresh the session before loading its transcript");
1083
- return json(await materializedTranscript(session, current.task, current.activeAgent ?? session.agent, current.execution, request.signal));
1382
+ return await transcriptResponse(url, current.execution);
1084
1383
  }
1085
1384
  if (request.method === "GET" && action === "stream") {
1086
1385
  const query = new URLSearchParams();
@@ -1089,12 +1388,12 @@ function createFactoryApi(options) {
1089
1388
  if (value !== null) query.set(name, value);
1090
1389
  }
1091
1390
  if (url.searchParams.get("history") === "1") {
1092
- const entry = (await view(session)).history?.find((item) => item.execution.sessionId === url.searchParams.get("executionId"));
1093
- if (!entry) throw new ApiError(404, "Execution is not part of this conversation history");
1391
+ const entry = await stores.conversations.execution(session.id, z.string().min(1).parse(url.searchParams.get("executionId")));
1392
+ if (!entry || !await accessibleExecution(session, entry)) throw new ApiError(404, "Execution is not part of this conversation history");
1094
1393
  query.set("includeTailIndex", "1");
1095
1394
  const task = entry.taskId ? await stores.tasks.get(entry.taskId) : null;
1096
1395
  const response = await options.forward({
1097
- session: executionSession(session, task, entry.agent),
1396
+ session: await executionSession(session, task, entry.agent),
1098
1397
  execution: entry.execution,
1099
1398
  path: `/stream?${query}`,
1100
1399
  request
@@ -1114,7 +1413,7 @@ function createFactoryApi(options) {
1114
1413
  if (request.method !== "POST") throw new ApiError(405, "Use GET or POST for this route");
1115
1414
  if (action === "messages") {
1116
1415
  const input = sendSessionMessageRequestSchema.parse(await body(request));
1117
- const previous = input.operationId ? session.continuations?.find((item) => item.operationId === input.operationId) : void 0;
1416
+ const previous = input.operationId ? await stores.sessions.getContinuationByOperation(session.id, input.operationId) : void 0;
1118
1417
  if (previous) {
1119
1418
  if (previous.message !== input.message || JSON.stringify(previous.attachments ?? []) !== JSON.stringify(input.attachments ?? [])) throw new ApiError(409, "Follow-up operationId was reused with different input");
1120
1419
  const recovered = await stores.sessions.continue(session.id, previous);
@@ -1122,7 +1421,7 @@ function createFactoryApi(options) {
1122
1421
  return json(current.task?.id === previous.taskId ? await launch(recovered) : current, 202);
1123
1422
  }
1124
1423
  const current = await view(session);
1125
- if (current.task?.state === "running" && session.continuations?.some((item) => item.taskId === current.task?.id)) throw new ApiError(409, "Wait for this discussion turn to finish before sending another follow-up");
1424
+ if (current.task?.state === "running" && await stores.sessions.getContinuation(session.id, current.task.id)) throw new ApiError(409, "Wait for this discussion turn to finish before sending another follow-up");
1126
1425
  if ((current.execution?.sessionId ?? null) !== input.executionId) throw new ApiError(409, "Execution changed; refresh the session before continuing");
1127
1426
  if (current.task && [
1128
1427
  "succeeded",
@@ -1148,11 +1447,8 @@ function createFactoryApi(options) {
1148
1447
  const input = answerSessionQuestionRequestSchema.parse(await body(request));
1149
1448
  const current = await view(session);
1150
1449
  const question = await stores.tasks.get(input.questionId);
1151
- const parent = session.taskId && question?.parentTaskId === session.taskId ? await stores.tasks.get(session.taskId) : current.relatedTasks?.find((task) => task.id === question?.parentTaskId) ?? workflowTasks(session, await stores.tasks.forReplyTo({
1152
- channel: "api",
1153
- address: session.id
1154
- })).find((task) => task.id === question?.parentTaskId);
1155
- if (!question?.question || !parent || question.replyTo.channel !== parent.replyTo.channel || question.replyTo.address !== parent.replyTo.address) throw new ApiError(403, "Question does not belong to this interaction");
1450
+ const parent = session.taskId && question?.parentTaskId === session.taskId ? await stores.tasks.get(session.taskId) : current.relatedTasks?.find((task) => task.id === question?.parentTaskId) ?? (question?.parentTaskId ? await stores.tasks.get(question.parentTaskId) : null);
1451
+ if (!question?.question || !parent || parent.id !== session.taskId && (parent.replyTo.channel !== "api" || parent.replyTo.address !== session.id || parent.repositoryIds.length !== 1 || parent.repositoryIds[0] !== session.repositoryId) || question.replyTo.channel !== parent.replyTo.channel || question.replyTo.address !== parent.replyTo.address) throw new ApiError(403, "Question does not belong to this interaction");
1156
1452
  await stores.tasks.answerQuestion({
1157
1453
  questionTaskId: question.id,
1158
1454
  answer: input.answer,
@@ -1171,9 +1467,28 @@ function createFactoryApi(options) {
1171
1467
  const current = await view(session);
1172
1468
  if (input.executionId !== void 0 && input.executionId !== current.execution?.sessionId) throw new ApiError(409, "Execution changed; refresh the session");
1173
1469
  if (action === "cancel" || action === "reset") {
1174
- const delegated = workflowTasks(session, await stores.tasks.forReplyTo({
1175
- channel: "api",
1176
- address: session.id
1470
+ const delegated = workflowTasks(session, await collectFactoryRecords(stores.tasks.list, {
1471
+ limit: 100,
1472
+ match: [
1473
+ "queued",
1474
+ "running",
1475
+ "verifying",
1476
+ "needs_human",
1477
+ "stalled"
1478
+ ].map((state) => ({
1479
+ replyTo: {
1480
+ channel: "api",
1481
+ address: session.id
1482
+ },
1483
+ state
1484
+ })),
1485
+ fields: [{
1486
+ path: ["work", "route"],
1487
+ test: "missing"
1488
+ }, {
1489
+ path: ["question"],
1490
+ test: "missing"
1491
+ }]
1177
1492
  }));
1178
1493
  for (const task of delegated) if (![
1179
1494
  "succeeded",
@@ -1210,6 +1525,7 @@ function createFactoryApi(options) {
1210
1525
  error: error.message,
1211
1526
  ...error instanceof ApprovalAccessError && error.actor ? { actor: error.actor } : {}
1212
1527
  }, error.status);
1528
+ if (error instanceof InvalidStoreQueryError) return json({ error: error.message }, 400);
1213
1529
  if (error instanceof z.ZodError) return json({
1214
1530
  error: "Invalid request",
1215
1531
  issues: error.issues
@@ -1225,9 +1541,9 @@ function createFactoryApi(options) {
1225
1541
  }
1226
1542
  async function startContinuation(task) {
1227
1543
  const session = task.replyTo.channel === "api" ? await stores.sessions.get(agentSessionIdSchema.parse(task.replyTo.address)) : null;
1228
- const continuation = session?.continuations?.find((item) => item.taskId === task.id);
1229
- if (!session || !continuation || continuation.agent !== options.continuationAgent || taskAgent(session, task) !== continuation.agent) throw new ApiError(409, "Conversation continuation is no longer configured");
1230
- return start(executionSession(session, task), task, await continuationBrief(session, await view(session), continuation.message, task.id), `factory-task:${task.id}:${task.attempt}`);
1544
+ const continuation = session ? await stores.sessions.getContinuation(session.id, task.id) : null;
1545
+ if (!session || !continuation || continuation.agent !== options.continuationAgent || await taskAgent(session, task) !== continuation.agent) throw new ApiError(409, "Conversation continuation is no longer configured");
1546
+ return start(await executionSession(session, task), task, await continuationBrief(session, await view(session), continuation.message, task.id), `factory-task:${task.id}:${task.attempt}`);
1231
1547
  }
1232
1548
  return {
1233
1549
  handle,
@@ -1271,6 +1587,6 @@ async function body(request, limit = 64e3) {
1271
1587
  }
1272
1588
  }
1273
1589
  //#endregion
1274
- export { ImageInputError, SessionConflictError, agentModelConfigSchema, agentSessionIdSchema, agentSessionSchema, answerSessionQuestionRequestSchema, apiAgentSchema, assembleWorkspaceDiff, controlSessionRequestSchema, createFactoryApi, createSessionRequestSchema, imagePartFilename, inboxActivityItemSchema, inboxActivitySchema, modelSettingsSchema, repositoryDetailsSchema, repositorySummarySchema, resolveImageMessage, sendSessionMessageRequestSchema, sessionInputResponseSchema, sessionPageSchema, sessionPreviewSchema, sessionPreviewsSchema, sessionReviewCheckSchema, sessionReviewIssueSchema, sessionReviewSchema, sessionReviewScreenshotSchema, sessionViewSchema, submitSessionInputRequestSchema, unavailableWorkspaceDiff, updateModelDefaultsSchema, workspaceDiffFileSchema, workspaceDiffSchema, workspaceFileContentSchema, workspaceFileEntrySchema, workspaceFilesRequestSchema, workspaceFilesSchema, workspaceGitFilesScript, workspacePathSchema, workspaceSnapshotSchema, workspaceSnapshotScript };
1590
+ export { ImageInputError, SessionConflictError, agentModelConfigSchema, agentSessionIdSchema, agentSessionSchema, answerSessionQuestionRequestSchema, apiAgentSchema, assembleWorkspaceDiff, controlSessionRequestSchema, createFactoryApi, createSessionRequestSchema, imagePartFilename, inboxActivityItemSchema, inboxActivitySchema, modelSettingsSchema, repositoryDetailsSchema, repositorySummarySchema, resolveImageMessage, sendSessionMessageRequestSchema, sessionInputResponseSchema, sessionPageSchema, sessionPreviewSchema, sessionPreviewsSchema, sessionPullRequestSchema, sessionPullRequestStateSchema, sessionReviewCheckSchema, sessionReviewIssueSchema, sessionReviewSchema, sessionReviewScreenshotSchema, sessionViewSchema, sessionWorkGroupSchema, sessionWorkResultSchema, sessionWorkTaskSchema, submitSessionInputRequestSchema, unavailableWorkspaceDiff, updateModelDefaultsSchema, workspaceDiffFileSchema, workspaceDiffSchema, workspaceFileContentSchema, workspaceFileEntrySchema, workspaceFilesRequestSchema, workspaceFilesSchema, workspaceGitFilesScript, workspacePathSchema, workspaceSnapshotSchema, workspaceSnapshotScript };
1275
1591
 
1276
1592
  //# sourceMappingURL=api.mjs.map