@xopcai/xopc 0.0.199 → 0.0.200

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 (135) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-Bt70bZjv.js → agent-avatar-dicebear-eMarz29u.js} +1 -1
  4. package/dist/gateway/static/root/assets/agents-DeLW-454.js +68 -0
  5. package/dist/gateway/static/root/assets/{app-management-settings-panel-B4x7JfL8.js → app-management-settings-panel-yfb68wq9.js} +1 -1
  6. package/dist/gateway/static/root/assets/{appearance-settings-Dwdagf-Q.js → appearance-settings-B8SPknFM.js} +1 -1
  7. package/dist/gateway/static/root/assets/{apps-page-DkTiesxe.js → apps-page-Bmor22Ny.js} +1 -1
  8. package/dist/gateway/static/root/assets/{archive-plugin-Cv0VXKLm.js → archive-plugin-DmYRiOP3.js} +1 -1
  9. package/dist/gateway/static/root/assets/{attachment-load-CgidKrXn.js → attachment-load-D66JvcPi.js} +1 -1
  10. package/dist/gateway/static/root/assets/automations-page-Q4e8m11h.js +6 -0
  11. package/dist/gateway/static/root/assets/{binary-plugins-DN3CQgIq.js → binary-plugins-ChIsZArT.js} +1 -1
  12. package/dist/gateway/static/root/assets/{block-editor-BwkSl1F_.js → block-editor-Ck-k_d0Z.js} +5 -5
  13. package/dist/gateway/static/root/assets/browser-settings-page--uVL73Sb.js +8 -0
  14. package/dist/gateway/static/root/assets/{browser-workflows-page-C_I31wLm.js → browser-workflows-page-BC5jm1zL.js} +2 -2
  15. package/dist/gateway/static/root/assets/capabilities-settings-panel-DXpf3QYN.js +7 -0
  16. package/dist/gateway/static/root/assets/capability-presets-settings-panel-NAjCsrTi.js +3 -0
  17. package/dist/gateway/static/root/assets/{channels-settings-DV5BhAeu.js → channels-settings-CiWUo7jW.js} +1 -1
  18. package/dist/gateway/static/root/assets/{connectors-api-5FEBYguL.js → connectors-api-5xiCRVUS.js} +1 -1
  19. package/dist/gateway/static/root/assets/connectors-page-DOwlMseU.js +2 -0
  20. package/dist/gateway/static/root/assets/{cron-expression-SEFx5rnr.js → cron-expression-B3PebXKZ.js} +1 -1
  21. package/dist/gateway/static/root/assets/{date-picker-D4zCUQJL.js → date-picker-C1Vke7Xn.js} +1 -1
  22. package/dist/gateway/static/root/assets/{dependency-picker-DpQAxS2k.js → dependency-picker-FrQ0tsBC.js} +1 -1
  23. package/dist/gateway/static/root/assets/{desktop-pet-B_5G3lQl.js → desktop-pet-CPBAzNJi.js} +1 -1
  24. package/dist/gateway/static/root/assets/{desktop-pet-settings-ClEyoUJe.js → desktop-pet-settings-D6s3glDP.js} +1 -1
  25. package/dist/gateway/static/root/assets/{directory-picker-path-field-AsFWmOjG.js → directory-picker-path-field-CbNQ7QQS.js} +1 -1
  26. package/dist/gateway/static/root/assets/{extension-debug-page-C22Y2o5l.js → extension-debug-page-dTK14hUv.js} +1 -1
  27. package/dist/gateway/static/root/assets/{extension-page-C61Ikawk.js → extension-page-Cpdj1Kip.js} +1 -1
  28. package/dist/gateway/static/root/assets/{extension-settings-page-WlWWcDFr.js → extension-settings-page-iNHUZGd5.js} +1 -1
  29. package/dist/gateway/static/root/assets/{gateway-config-api-ByIOMQdc.js → gateway-config-api-SvMvWqMM.js} +1 -1
  30. package/dist/gateway/static/root/assets/{gateway-settings-CzzS9A17.js → gateway-settings-BilR3dGZ.js} +1 -1
  31. package/dist/gateway/static/root/assets/heartbeat-settings-zK40dcQ5.js +1 -0
  32. package/dist/gateway/static/root/assets/home-page-NyXy5MLr.js +3 -0
  33. package/dist/gateway/static/root/assets/{index-CR233S0Q.css → index-Bp8I_eX4.css} +1 -1
  34. package/dist/gateway/static/root/assets/index-CCK8zdlS.js +152 -0
  35. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BD_TFz0_.js → keyboard-shortcuts-settings-nKzsroud.js} +1 -1
  36. package/dist/gateway/static/root/assets/local-app-workbench-page-Ds3sPXTh.js +2 -0
  37. package/dist/gateway/static/root/assets/{local-apps-page-BwbUeLfm.js → local-apps-page-DV1s_FYh.js} +1 -1
  38. package/dist/gateway/static/root/assets/logs-page-Biz4eA02.js +2 -0
  39. package/dist/gateway/static/root/assets/{management-settings-CW2VpqtT.js → management-settings-D7KjYgHu.js} +1 -1
  40. package/dist/gateway/static/root/assets/{markdown-split-B1M9KO16.js → markdown-split-CTYbpkrd.js} +1 -1
  41. package/dist/gateway/static/root/assets/{media-plugins-B_QlNdqZ.js → media-plugins-BXYnaiZ6.js} +1 -1
  42. package/dist/gateway/static/root/assets/{notes-page-BTbD4GmH.js → notes-page-CWWVXpw5.js} +1 -1
  43. package/dist/gateway/static/root/assets/notes-workbench-DnBkQihH.js +4 -0
  44. package/dist/gateway/static/root/assets/{preview-open-alternatives-CeqtArlZ.js → preview-open-alternatives-pimQAkeI.js} +1 -1
  45. package/dist/gateway/static/root/assets/{product-open-page-DnalBZZs.js → product-open-page-BoYSS_YR.js} +1 -1
  46. package/dist/gateway/static/root/assets/project-detail-page-ZO7t1UkB.js +1 -0
  47. package/dist/gateway/static/root/assets/projects-page-By-vq41g.js +1 -0
  48. package/dist/gateway/static/root/assets/remote-access-hub-C0C_Y9d0.js +1 -0
  49. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-cGkBEfg4.js → runtime-tools-settings-panel-DT8flaX1.js} +2 -2
  50. package/dist/gateway/static/root/assets/{schema-form-CmsrLlxu.js → schema-form-DabEP-UI.js} +1 -1
  51. package/dist/gateway/static/root/assets/sessions-page-BNlVwUzV.js +1 -0
  52. package/dist/gateway/static/root/assets/settings-advanced-gate-CxYBQDaM.js +1 -0
  53. package/dist/gateway/static/root/assets/{settings-form-section-DUWCbaMJ.js → settings-form-section-DCsfZAtC.js} +1 -1
  54. package/dist/gateway/static/root/assets/{settings-loading-skeleton-Bvy5rdMY.js → settings-loading-skeleton-Dac6Yd0T.js} +1 -1
  55. package/dist/gateway/static/root/assets/{settings-page-cZSAwsRR.js → settings-page-BDoYoEar.js} +1 -1
  56. package/dist/gateway/static/root/assets/setup-status-panel-DWijJ1rw.js +1 -0
  57. package/dist/gateway/static/root/assets/{share-preview-page-BzA2NCAH.js → share-preview-page-ie9I2_7h.js} +1 -1
  58. package/dist/gateway/static/root/assets/shares-settings-DU9VrJLf.js +1 -0
  59. package/dist/gateway/static/root/assets/skill-reload-api-C5RaIvoE.js +1 -0
  60. package/dist/gateway/static/root/assets/{skills-page-Bvde7_i5.js → skills-page-N0YhaR5S.js} +1 -1
  61. package/dist/gateway/static/root/assets/{skills-page.utils-vXSqHR7g.js → skills-page.utils-C639DLXr.js} +1 -1
  62. package/dist/gateway/static/root/assets/{system-settings-panel-9cQGOGCK.js → system-settings-panel-CpshpQtC.js} +1 -1
  63. package/dist/gateway/static/root/assets/task-detail-page-DsTocYkK.js +1 -0
  64. package/dist/gateway/static/root/assets/{use-autosave-7XfKULPI.js → use-autosave-ms6rDy59.js} +1 -1
  65. package/dist/gateway/static/root/assets/{user-context-page-CZ9m9aC_.js → user-context-page-BUxIemwy.js} +1 -1
  66. package/dist/gateway/static/root/assets/{voice-api-key-field-DXr21v-3.js → voice-api-key-field-CObrQWh2.js} +1 -1
  67. package/dist/gateway/static/root/assets/{work-discovery-overlay-CeG7g_DZ.js → work-discovery-overlay-BBxrnEFt.js} +1 -1
  68. package/dist/gateway/static/root/assets/work-discovery-page-DN3l-HW3.js +1 -0
  69. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-DSQQZP9T.js → workflow-run-setup-panel-BxXhe3IV.js} +2 -2
  70. package/dist/gateway/static/root/assets/workflows-page-BCHkjd1i.js +5 -0
  71. package/dist/gateway/static/root/index.html +2 -2
  72. package/dist/package.js +1 -1
  73. package/dist/packages/gateway-contract/src/tasks.js +51 -9
  74. package/dist/src/agent/context/execution-scope.js +3 -1
  75. package/dist/src/agent/tasks/task-judge-service.d.ts +1 -1
  76. package/dist/src/agent/tasks/task-judge-service.js +3 -2
  77. package/dist/src/agent/tools/xopc-use-tool.js +13 -3
  78. package/dist/src/automations/domain/validation.d.ts +0 -12
  79. package/dist/src/automations/draft/automation-draft-validator.d.ts +0 -3
  80. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  81. package/dist/src/gateway/hono/app.js +1 -1
  82. package/dist/src/gateway/hono/routes/agent-stream.js +2 -74
  83. package/dist/src/gateway/hono/routes/session-input-handler.d.ts +62 -0
  84. package/dist/src/gateway/hono/routes/session-input-handler.js +82 -0
  85. package/dist/src/gateway/hono/routes/tasks.js +161 -2
  86. package/dist/src/gateway/service/task-conversation-query-service.d.ts +58 -0
  87. package/dist/src/gateway/service/task-conversation-query-service.js +84 -0
  88. package/dist/src/gateway/service.d.ts +10 -0
  89. package/dist/src/gateway/service.js +70 -37
  90. package/dist/src/projects/project-agent.js +1 -1
  91. package/dist/src/storage/sqlite/migrations/120_task_conversation.sql +112 -0
  92. package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
  93. package/dist/src/storage/sqlite/migrations/runner.js +3 -3
  94. package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
  95. package/dist/src/storage/sqlite/schema.js +1 -1
  96. package/dist/src/tasks/execution-context.js +8 -4
  97. package/dist/src/tasks/index.d.ts +2 -0
  98. package/dist/src/tasks/index.js +3 -1
  99. package/dist/src/tasks/task-application-service.js +10 -17
  100. package/dist/src/tasks/task-context-assembler.js +9 -4
  101. package/dist/src/tasks/task-context-repository.d.ts +0 -6
  102. package/dist/src/tasks/task-context-repository.js +0 -4
  103. package/dist/src/tasks/task-conversation-repository.d.ts +71 -0
  104. package/dist/src/tasks/task-conversation-repository.js +194 -0
  105. package/dist/src/tasks/task-handoff-service.d.ts +31 -0
  106. package/dist/src/tasks/task-handoff-service.js +143 -0
  107. package/dist/src/tasks/task-read-model-projector.js +1 -5
  108. package/dist/src/tasks/task-repository.d.ts +0 -2
  109. package/dist/src/tasks/task-repository.js +2 -9
  110. package/dist/src/tasks/task-run-coordinator.js +3 -1
  111. package/dist/src/workflows/service/workflow-run-service.js +4 -2
  112. package/package.json +3 -3
  113. package/dist/gateway/static/root/assets/agents-D19uNmJf.js +0 -68
  114. package/dist/gateway/static/root/assets/automations-page-BR6qMOVr.js +0 -6
  115. package/dist/gateway/static/root/assets/browser-settings-page-CDFQWAxI.js +0 -8
  116. package/dist/gateway/static/root/assets/capabilities-settings-panel-D41mssOq.js +0 -7
  117. package/dist/gateway/static/root/assets/capability-presets-settings-panel-BXgCyuu2.js +0 -3
  118. package/dist/gateway/static/root/assets/connectors-page-C_qkEN0T.js +0 -2
  119. package/dist/gateway/static/root/assets/heartbeat-settings-B4A_CXUL.js +0 -1
  120. package/dist/gateway/static/root/assets/home-page-FVGYYPWo.js +0 -3
  121. package/dist/gateway/static/root/assets/index-CGbKTCST.js +0 -152
  122. package/dist/gateway/static/root/assets/local-app-workbench-page-BNpkigNb.js +0 -2
  123. package/dist/gateway/static/root/assets/logs-page-DXP9OQQt.js +0 -2
  124. package/dist/gateway/static/root/assets/notes-workbench-JRiq7QDW.js +0 -4
  125. package/dist/gateway/static/root/assets/project-detail-page-CjWA13aE.js +0 -1
  126. package/dist/gateway/static/root/assets/projects-page-jsCkScbG.js +0 -1
  127. package/dist/gateway/static/root/assets/remote-access-hub-CGPJTEkM.js +0 -1
  128. package/dist/gateway/static/root/assets/sessions-page-BBHzziMd.js +0 -1
  129. package/dist/gateway/static/root/assets/settings-advanced-gate-2cGg8-9h.js +0 -1
  130. package/dist/gateway/static/root/assets/setup-status-panel-Cf06b66-.js +0 -1
  131. package/dist/gateway/static/root/assets/shares-settings-xlNKydYY.js +0 -1
  132. package/dist/gateway/static/root/assets/skill-reload-api-C5Xu93c_.js +0 -1
  133. package/dist/gateway/static/root/assets/task-detail-page-Bkno4tTe.js +0 -1
  134. package/dist/gateway/static/root/assets/work-discovery-page-CQWZD03N.js +0 -1
  135. package/dist/gateway/static/root/assets/workflows-page-Cs-MXFJ1.js +0 -5
@@ -1,10 +1,12 @@
1
1
  import { runSqliteWriteTransaction } from "../../../storage/sqlite/transaction.js";
2
2
  import { TaskRepository } from "../../../tasks/task-repository.js";
3
+ import { TaskConversationRepository } from "../../../tasks/task-conversation-repository.js";
3
4
  import { TaskRunRepository } from "../../../tasks/task-run-repository.js";
4
5
  import { TaskReadModelProjector } from "../../../tasks/task-read-model-projector.js";
5
6
  import { TaskPhaseSchema } from "../../../../packages/gateway-contract/src/task-lifecycle.js";
6
- import { TaskBoardPositionRequestSchema, TaskCommandRequestSchema, TaskContextInputSchema, TaskCreateRequestSchema, TaskDependencyUpdateRequestSchema, TaskPatchRequestSchema } from "../../../../packages/gateway-contract/src/tasks.js";
7
+ import { TaskBoardPositionRequestSchema, TaskCommandRequestSchema, TaskContextInputSchema, TaskCreateRequestSchema, TaskDependencyUpdateRequestSchema, TaskHandoffRequestSchema, TaskPatchRequestSchema } from "../../../../packages/gateway-contract/src/tasks.js";
7
8
  import { ProjectMonitoringUpdateSchema } from "../../../../packages/gateway-contract/src/project-monitoring.js";
9
+ import { resolveProjectAgentId } from "../../../projects/project-agent.js";
8
10
  import { TaskContextRepository } from "../../../tasks/task-context-repository.js";
9
11
  import { enqueueTaskChangedEvent } from "../../../tasks/task-change-events.js";
10
12
  import { TaskDependencyError, TaskDependencyService } from "../../../tasks/task-dependency-service.js";
@@ -12,7 +14,10 @@ import { TaskApplicationService } from "../../../tasks/task-application-service.
12
14
  import { ProjectMonitoringService } from "../../../tasks/project-monitoring-service.js";
13
15
  import { ProjectOperatingViewService } from "../../../tasks/project-operating-view-service.js";
14
16
  import { TaskValueMetricsService } from "../../../tasks/task-value-metrics-service.js";
17
+ import { TaskHandoffService } from "../../../tasks/task-handoff-service.js";
15
18
  import { TaskSignalService } from "../../../tasks/task-signal-service.js";
19
+ import { submitSessionInput } from "./session-input-handler.js";
20
+ import { TaskConversationQueryService } from "../../service/task-conversation-query-service.js";
16
21
  //#region src/gateway/hono/routes/tasks.ts
17
22
  function registerTaskRoutes(authenticated, deps) {
18
23
  const taskRateLimit = deps.taskRateLimitMiddleware ?? deps.strictRateLimitMiddleware;
@@ -23,9 +28,17 @@ function registerTaskRoutes(authenticated, deps) {
23
28
  const tasks = new TaskRepository();
24
29
  const runs = new TaskRunRepository();
25
30
  const context = new TaskContextRepository();
31
+ const conversations = new TaskConversationRepository();
32
+ const conversationQuery = new TaskConversationQueryService(deps.service.sessions);
26
33
  const projector = new TaskReadModelProjector();
27
34
  const signals = new TaskSignalService(() => deps.service.dispatchTaskRuns());
28
35
  const dependencies = new TaskDependencyService();
36
+ const handoffs = new TaskHandoffService({
37
+ getConfig: () => deps.service.currentConfig,
38
+ sessionIndex: deps.service.sessionIndexInstance,
39
+ getActiveRunId: (sessionKey) => deps.service.getActiveWebchatRunId(sessionKey),
40
+ abortRun: (runId) => deps.service.abortAgentRun(runId)
41
+ });
29
42
  authenticated.get("/api/tasks", (c) => {
30
43
  const phase = TaskPhaseSchema.safeParse(c.req.query("phase"));
31
44
  const rawLimit = Number(c.req.query("limit") ?? 50);
@@ -53,7 +66,25 @@ function registerTaskRoutes(authenticated, deps) {
53
66
  error: "Invalid task request"
54
67
  }, 400);
55
68
  try {
56
- const created = application.create(parsed.data);
69
+ const requestedAgentId = parsed.data.delegateAgentId ?? (parsed.data.activation.mode === "start" && parsed.data.activation.executor?.kind === "agent" ? parsed.data.activation.executor.agentId : void 0);
70
+ const agentId = resolveProjectAgentId({
71
+ config: deps.service.currentConfig,
72
+ projects: deps.service.projects,
73
+ explicitAgentId: requestedAgentId,
74
+ projectId: parsed.data.projectId
75
+ });
76
+ const input = {
77
+ ...parsed.data,
78
+ delegateAgentId: agentId,
79
+ activation: parsed.data.activation.mode === "start" ? {
80
+ ...parsed.data.activation,
81
+ executor: parsed.data.activation.executor?.kind === "agent" || !parsed.data.activation.executor ? {
82
+ kind: "agent",
83
+ agentId
84
+ } : parsed.data.activation.executor
85
+ } : parsed.data.activation
86
+ };
87
+ const created = application.create(input);
57
88
  if (created.ok === false) return c.json({
58
89
  ok: false,
59
90
  code: created.reason,
@@ -95,12 +126,140 @@ function registerTaskRoutes(authenticated, deps) {
95
126
  runs: runs.listByTask(task.id),
96
127
  receipts: runs.listReceipts(task.id),
97
128
  context: context.list(task.id),
129
+ conversation: conversations.requireState(task.id),
130
+ sessions: conversations.listSessions(task.id),
98
131
  authorityGrants: context.listActiveGrants(task.id),
99
132
  dependencies: dependencies.listDependencies(task.id),
100
133
  dependents: dependencies.listDependents(task.id),
101
134
  allowedCommands: model.allowedCommands
102
135
  });
103
136
  });
137
+ authenticated.post("/api/tasks/:id/conversation", taskRateLimit, async (c) => {
138
+ try {
139
+ const result = await deps.service.ensureTaskConversation(c.req.param("id"));
140
+ if (result.created) {
141
+ const task = tasks.require(c.req.param("id"));
142
+ runSqliteWriteTransaction((db) => enqueueTaskChangedEvent(db, {
143
+ taskId: task.id,
144
+ projectId: task.projectId,
145
+ version: task.version,
146
+ changedFields: ["conversation"],
147
+ actor: { kind: "user" }
148
+ }));
149
+ deps.service.dispatchTaskEvents();
150
+ }
151
+ return c.json({
152
+ ok: true,
153
+ ...result
154
+ });
155
+ } catch (error) {
156
+ const message = error instanceof Error ? error.message : String(error);
157
+ return c.json({
158
+ ok: false,
159
+ error: message
160
+ }, message.startsWith("Task not found") ? 404 : 409);
161
+ }
162
+ });
163
+ authenticated.post("/api/tasks/:id/handoff", taskRateLimit, async (c) => {
164
+ const parsed = TaskHandoffRequestSchema.safeParse(await c.req.json().catch(() => ({})));
165
+ if (!parsed.success) return c.json({
166
+ ok: false,
167
+ error: "Invalid task handoff request"
168
+ }, 400);
169
+ try {
170
+ const result = await handoffs.handoff({
171
+ taskId: c.req.param("id"),
172
+ ...parsed.data
173
+ });
174
+ runSqliteWriteTransaction((db) => enqueueTaskChangedEvent(db, {
175
+ taskId: result.task.id,
176
+ projectId: result.task.projectId,
177
+ version: result.task.version,
178
+ changedFields: ["delegateAgentId", "conversation"],
179
+ actor: { kind: "user" }
180
+ }));
181
+ deps.service.dispatchTaskEvents();
182
+ return c.json({
183
+ ok: true,
184
+ task: result.task,
185
+ conversation: result.conversation,
186
+ ...result.fromAgentId ? { fromAgentId: result.fromAgentId } : {},
187
+ toAgentId: result.toAgentId,
188
+ activeSessionKey: result.activeSessionKey,
189
+ assignmentEpoch: result.assignmentEpoch
190
+ });
191
+ } catch (error) {
192
+ const message = error instanceof Error ? error.message : String(error);
193
+ return c.json({
194
+ ok: false,
195
+ error: message
196
+ }, message.startsWith("Agent not found") || message.startsWith("Task not found") ? 404 : 409);
197
+ }
198
+ });
199
+ authenticated.post("/api/tasks/:id/inputs", deps.chatRateLimitMiddleware, async (c) => {
200
+ const active = conversations.getActiveSession(c.req.param("id"));
201
+ if (!active?.sessionKey) return c.json({
202
+ ok: false,
203
+ error: {
204
+ code: "NOT_FOUND",
205
+ message: "Task has no active conversation"
206
+ }
207
+ }, 404);
208
+ const expectedSessionKey = c.req.header("X-Xopc-Expected-Session-Key")?.trim();
209
+ if (expectedSessionKey && expectedSessionKey !== active.sessionKey) return c.json({
210
+ ok: false,
211
+ error: {
212
+ code: "CONFLICT",
213
+ message: "Task executor changed; refresh the conversation"
214
+ }
215
+ }, 409);
216
+ return submitSessionInput(c, deps, active.sessionKey);
217
+ });
218
+ authenticated.patch("/api/tasks/:id/conversation/config", taskRateLimit, async (c) => {
219
+ const body = await c.req.json().catch(() => null);
220
+ const model = typeof body?.model === "string" ? body.model.trim() : "";
221
+ if (!model) return c.json({
222
+ ok: false,
223
+ error: "Missing model"
224
+ }, 400);
225
+ const active = conversations.getActiveSession(c.req.param("id"));
226
+ if (!active?.sessionKey) return c.json({
227
+ ok: false,
228
+ error: "Task has no active conversation"
229
+ }, 404);
230
+ const result = await deps.service.sessions.patchAgentConfig(active.sessionKey, { model });
231
+ if (!result.ok) return c.json({
232
+ ok: false,
233
+ error: result.error
234
+ }, 400);
235
+ return c.json({
236
+ ok: true,
237
+ activeSessionKey: active.sessionKey
238
+ });
239
+ });
240
+ authenticated.get("/api/tasks/:id/conversation/history", async (c) => {
241
+ const parsedLimit = Number.parseInt(c.req.query("limit") ?? "50", 10);
242
+ const parsedOffset = Number.parseInt(c.req.query("offset") ?? "0", 10);
243
+ const rawBefore = c.req.query("before")?.trim();
244
+ const before = rawBefore === void 0 ? void 0 : Number.parseInt(rawBefore, 10);
245
+ if (rawBefore !== void 0 && (!Number.isInteger(before) || before < 0)) return c.json({ error: "Invalid conversation history cursor" }, 400);
246
+ const result = await conversationQuery.getMessagePage(c.req.param("id"), {
247
+ limit: Number.isFinite(parsedLimit) ? Math.min(200, Math.max(1, parsedLimit)) : 50,
248
+ offset: Number.isFinite(parsedOffset) ? Math.max(0, parsedOffset) : 0,
249
+ ...before === void 0 ? {} : { before }
250
+ });
251
+ return result ? c.json(result) : c.json({ error: "Task conversation not found" }, 404);
252
+ });
253
+ authenticated.get("/api/tasks/:id/conversation/timeline", async (c) => {
254
+ const items = await conversationQuery.getTimeline(c.req.param("id"));
255
+ return items ? c.json({
256
+ ok: true,
257
+ items
258
+ }) : c.json({
259
+ ok: false,
260
+ error: "Task conversation not found"
261
+ }, 404);
262
+ });
104
263
  authenticated.patch("/api/tasks/:id", taskRateLimit, async (c) => {
105
264
  const parsed = TaskPatchRequestSchema.safeParse(await c.req.json().catch(() => ({})));
106
265
  if (!parsed.success) return c.json({
@@ -0,0 +1,58 @@
1
+ import type { SessionTimelineItem } from '../../session/transcript-outline.js';
2
+ import { TaskConversationRepository } from '../../tasks/task-conversation-repository.js';
3
+ import type { GatewaySessionsApi } from './sessions-api.js';
4
+ export declare class TaskConversationQueryService {
5
+ #private;
6
+ private readonly sessions;
7
+ private readonly conversations;
8
+ constructor(sessions: Pick<GatewaySessionsApi, 'getMessagePage' | 'getTimeline'>, conversations?: Pick<TaskConversationRepository, 'listSessions'>);
9
+ getMessagePage(taskId: string, options: {
10
+ limit: number;
11
+ offset: number;
12
+ before?: number;
13
+ }): Promise<{
14
+ session: {
15
+ messages: import("../../session/types.js").Message[];
16
+ transcriptSummary?: import("../../session/types.js").SessionTranscriptSummary;
17
+ transcriptRows?: import("../../session/session-context-for-llm.js").TranscriptStoredRow[];
18
+ key: string;
19
+ name?: string;
20
+ status: import("../../session/types.js").SessionStatus;
21
+ tags: string[];
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ lastAccessedAt: string;
25
+ messageCount: number;
26
+ estimatedTokens: number;
27
+ compactedCount: number;
28
+ lastFlushedAt?: string;
29
+ flushCount?: number;
30
+ sourceChannel: string;
31
+ sourceChatId: string;
32
+ customData?: Record<string, unknown>;
33
+ projectId?: string;
34
+ routing?: import("../../session/types.js").SessionRoutingMeta;
35
+ stats?: import("../../session/types.js").SessionStats;
36
+ sessionType: import("../../session/types.js").SessionType;
37
+ hiddenFromSessionList?: boolean;
38
+ parentSessionKey?: string;
39
+ workflowRunId?: string;
40
+ workflowDefinitionId?: string;
41
+ workflowAgentId?: string;
42
+ workflowAgentLabel?: string;
43
+ sessionId?: string;
44
+ cwd?: string;
45
+ sessionStartedAt?: string;
46
+ lastInteractionAt?: string;
47
+ };
48
+ pagination: {
49
+ nextBeforeCursor?: string;
50
+ before?: string;
51
+ total: number;
52
+ limit: number;
53
+ offset: number;
54
+ hasMore: boolean;
55
+ };
56
+ }>;
57
+ getTimeline(taskId: string): Promise<SessionTimelineItem[] | null>;
58
+ }
@@ -0,0 +1,84 @@
1
+ import { TaskConversationRepository } from "../../tasks/task-conversation-repository.js";
2
+ //#region src/gateway/service/task-conversation-query-service.ts
3
+ var TaskConversationQueryService = class {
4
+ constructor(sessions, conversations = new TaskConversationRepository()) {
5
+ this.sessions = sessions;
6
+ this.conversations = conversations;
7
+ }
8
+ async getMessagePage(taskId, options) {
9
+ const links = this.#executionSessions(taskId);
10
+ if (links.length === 0) return null;
11
+ const probes = await Promise.all(links.map((link) => this.sessions.getMessagePage(link.sessionKey, { limit: 1 })));
12
+ if (probes.some((page) => !page)) return null;
13
+ const pages = probes;
14
+ const total = pages.reduce((sum, page) => sum + page.pagination.total, 0);
15
+ const end = Math.min(total, Math.max(0, options.before ?? total - options.offset));
16
+ const start = Math.max(0, end - options.limit);
17
+ const messages = [];
18
+ let cursor = 0;
19
+ for (let index = 0; index < links.length; index += 1) {
20
+ const sessionTotal = pages[index].pagination.total;
21
+ const localStart = Math.max(0, start - cursor);
22
+ const localEnd = Math.min(sessionTotal, end - cursor);
23
+ if (localStart < localEnd) {
24
+ const page = await this.sessions.getMessagePage(links[index].sessionKey, {
25
+ offset: sessionTotal - localEnd,
26
+ limit: localEnd - localStart
27
+ });
28
+ if (!page) return null;
29
+ messages.push(...page.session.messages);
30
+ }
31
+ cursor += sessionTotal;
32
+ }
33
+ const activeIndex = links.findIndex((link) => link.status === "active");
34
+ return {
35
+ session: {
36
+ ...pages[activeIndex >= 0 ? activeIndex : pages.length - 1].session,
37
+ messages
38
+ },
39
+ pagination: {
40
+ total,
41
+ limit: options.limit,
42
+ offset: options.offset,
43
+ hasMore: start > 0,
44
+ ...options.before === void 0 ? {} : { before: String(end) },
45
+ ...start > 0 ? { nextBeforeCursor: String(start) } : {}
46
+ }
47
+ };
48
+ }
49
+ async getTimeline(taskId) {
50
+ const links = this.#executionSessions(taskId);
51
+ if (links.length === 0) return null;
52
+ const output = [];
53
+ let displayOffset = 0;
54
+ let turnOffset = 0;
55
+ for (const [index, link] of links.entries()) {
56
+ const [items, page] = await Promise.all([this.sessions.getTimeline(link.sessionKey), this.sessions.getMessagePage(link.sessionKey, { limit: 1 })]);
57
+ if (!items || !page) return null;
58
+ if (index > 0) output.push({
59
+ id: `assignment:${link.id}`,
60
+ kind: "branch",
61
+ title: link.agentId ? `Execution continued by ${link.agentId}` : "Execution continued",
62
+ timestamp: link.startedAt,
63
+ depth: 0,
64
+ turn: turnOffset,
65
+ displayIndex: displayOffset
66
+ });
67
+ output.push(...items.map((item) => ({
68
+ ...item,
69
+ id: `${link.id}:${item.id}`,
70
+ turn: item.turn + turnOffset,
71
+ ...item.displayIndex === void 0 ? {} : { displayIndex: item.displayIndex + displayOffset }
72
+ })));
73
+ displayOffset += page.pagination.total;
74
+ const highestTurn = items.reduce((max, item) => Math.max(max, item.turn), -1);
75
+ turnOffset += highestTurn + 1;
76
+ }
77
+ return output;
78
+ }
79
+ #executionSessions(taskId) {
80
+ return this.conversations.listSessions(taskId).filter((link) => link.role === "execution").sort((a, b) => a.assignmentEpoch - b.assignmentEpoch || a.startedAt - b.startedAt);
81
+ }
82
+ };
83
+ //#endregion
84
+ export { TaskConversationQueryService };
@@ -12,6 +12,7 @@ import { type ResolvedGatewayAuth } from './auth.js';
12
12
  import { ModelCatalogSyncService } from '../providers/model-catalog-sync-service.js';
13
13
  import { ProjectService } from '../projects/index.js';
14
14
  import { LocalAppService } from '../local-apps/index.js';
15
+ import { TaskConversationRepository } from '../tasks/task-conversation-repository.js';
15
16
  import { type BrowserRecipeService } from '../browser/recipes/index.js';
16
17
  import { listInsights, ProactiveEventService, ProactiveInboxService, ProactiveInboxWorker, ProactiveScenarioService, ProactiveTemporalWorker, ProactiveWorker } from '../proactive/index.js';
17
18
  import { GatewaySessionsApi } from './service/sessions-api.js';
@@ -120,6 +121,15 @@ export declare class GatewayService {
120
121
  private createTaskRunDispatcher;
121
122
  private createMobileNotificationService;
122
123
  dispatchTaskRuns(): void;
124
+ ensureTaskConversation(taskId: string, options?: {
125
+ runId?: string;
126
+ requestedAgentId?: string;
127
+ }): Promise<{
128
+ sessionKey: string;
129
+ agentId: string;
130
+ created: boolean;
131
+ conversation: ReturnType<TaskConversationRepository['requireState']>;
132
+ }>;
123
133
  dispatchTaskEvents(): void;
124
134
  runAgent(...args: Parameters<GatewayAgentRunner['runAgent']>): ReturnType<GatewayAgentRunner['runAgent']>;
125
135
  submitSessionInput(...args: Parameters<GatewayAgentRunner['submitSessionInput']>): Promise<{
@@ -19,19 +19,20 @@ import { loadConfig, saveConfig } from "../config/loader.js";
19
19
  import { prewarmModelRegistry } from "../providers/model-registry.js";
20
20
  import { init_providers, providers_exports } from "../providers/index.js";
21
21
  import { TaskRepository } from "../tasks/task-repository.js";
22
+ import { TaskConversationRepository } from "../tasks/task-conversation-repository.js";
22
23
  import { disposeAllSessionMcpRuntimes } from "../agent/mcp/bundle-mcp-runtime.js";
23
24
  import { resolveEffectiveGatewayPort } from "./host.js";
24
25
  import { AutomationService } from "../automations/service/automation-service.js";
25
26
  import { onAutomationProductEvent, publishAutomationProductEvent } from "../automations/product-events.js";
26
27
  import "../automations/index.js";
27
28
  import { ProjectService } from "../projects/project-service.js";
29
+ import { resolveProjectAgentId } from "../projects/project-agent.js";
28
30
  import "../projects/index.js";
29
31
  import { sanitizeTunnelConfig } from "../tunnel/tunnel-config.js";
30
32
  import { wireTunnelEventsToGateway } from "../tunnel/gateway-lifecycle.js";
31
33
  import { buckets } from "./rate-limit/buckets.js";
32
34
  import { isGatewayStrictSecurityEnabled } from "./rate-limit/env-flags.js";
33
35
  import "./rate-limit/index.js";
34
- import { TaskContextRepository } from "../tasks/task-context-repository.js";
35
36
  import { TaskApplicationService } from "../tasks/task-application-service.js";
36
37
  import { ProactiveScenarioService } from "../proactive/scenarios/service.js";
37
38
  import { TaskOutboxDispatcher } from "../tasks/task-outbox-dispatcher.js";
@@ -541,42 +542,10 @@ var GatewayService = class {
541
542
  if (!this.taskRunDispatcher) this.taskRunDispatcher = new TaskRunDispatcher({
542
543
  workerId: "gateway-agent",
543
544
  ensureSession: async (taskId, runId, requestedAgentId) => {
544
- const task = new TaskRepository().require(taskId);
545
- const agentId = requestedAgentId || task.delegateAgentId || getDefaultAgentId(this.config);
546
- const peerId = `task-${sanitizeSegment(taskId) || Date.now()}`;
547
- const sessionKey = buildSessionKey({
548
- agentId,
549
- source: "webchat",
550
- accountId: "default",
551
- peerKind: "direct",
552
- peerId
553
- });
554
- if (!await this.sessionIndex.getSessionMetadata(sessionKey)) await this.sessionIndex.saveMessages(sessionKey, [], { metadata: {
555
- sourceChannel: "webchat",
556
- sourceChatId: `default:direct:${peerId}`,
557
- sessionType: "chat",
558
- routing: {
559
- agentId,
560
- source: "webchat",
561
- accountId: "default",
562
- peerKind: "direct",
563
- peerId
564
- },
565
- projectId: task.projectId,
566
- customData: {
567
- taskId,
568
- origin: "task",
569
- triggerKind: "user",
570
- taskRunId: runId
571
- }
572
- } });
573
- if (task.projectId) this.projects.attachSession(sessionKey, task.projectId);
574
- new TaskContextRepository().linkSession({
575
- taskId,
576
- sessionKey,
577
- role: "execution"
578
- });
579
- return sessionKey;
545
+ return (await this.ensureTaskConversation(taskId, {
546
+ runId,
547
+ requestedAgentId
548
+ })).sessionKey;
580
549
  },
581
550
  runAgent: async (runId, sessionKey, message) => {
582
551
  const stream = this.agentRunner.runAgent(message, "webchat", sessionKey, {
@@ -598,6 +567,70 @@ var GatewayService = class {
598
567
  this.createTaskRunDispatcher().dispatch();
599
568
  this.createWorkflowRunService().dispatchTaskRuns();
600
569
  }
570
+ async ensureTaskConversation(taskId, options = {}) {
571
+ const task = new TaskRepository().require(taskId);
572
+ const agentId = resolveProjectAgentId({
573
+ config: this.config,
574
+ projects: this.projects,
575
+ explicitAgentId: options.requestedAgentId ?? task.delegateAgentId,
576
+ projectId: task.projectId
577
+ });
578
+ const conversations = new TaskConversationRepository();
579
+ const active = conversations.getActiveSession(task.id);
580
+ if (active) {
581
+ if (active.agentId && active.agentId !== agentId) throw new Error("Task executor differs from the active conversation");
582
+ const conversation = conversations.activateExecutionSession({
583
+ taskId: task.id,
584
+ sessionKey: active.sessionKey,
585
+ agentId,
586
+ runId: options.runId
587
+ });
588
+ return {
589
+ sessionKey: active.sessionKey,
590
+ agentId,
591
+ created: false,
592
+ conversation
593
+ };
594
+ }
595
+ const peerId = `task-${sanitizeSegment(task.id) || Date.now()}`;
596
+ const sessionKey = buildSessionKey({
597
+ agentId,
598
+ source: "webchat",
599
+ accountId: "default",
600
+ peerKind: "direct",
601
+ peerId
602
+ });
603
+ if (!await this.sessionIndex.getSessionMetadata(sessionKey)) await this.sessionIndex.saveMessages(sessionKey, [], { metadata: {
604
+ sourceChannel: "webchat",
605
+ sourceChatId: `default:direct:${peerId}`,
606
+ sessionType: "chat",
607
+ routing: {
608
+ agentId,
609
+ source: "webchat",
610
+ accountId: "default",
611
+ peerKind: "direct",
612
+ peerId
613
+ },
614
+ projectId: task.projectId,
615
+ customData: {
616
+ origin: "task",
617
+ triggerKind: "user",
618
+ ...options.runId ? { taskRunId: options.runId } : {}
619
+ }
620
+ } });
621
+ if (task.projectId) this.projects.attachSession(sessionKey, task.projectId);
622
+ return {
623
+ sessionKey,
624
+ agentId,
625
+ created: true,
626
+ conversation: conversations.activateExecutionSession({
627
+ taskId: task.id,
628
+ sessionKey,
629
+ agentId,
630
+ runId: options.runId
631
+ })
632
+ };
633
+ }
601
634
  dispatchTaskEvents() {
602
635
  new TaskOutboxDispatcher(publishAutomationProductEvent).drain();
603
636
  }
@@ -8,7 +8,7 @@ function normalizeAgentId(value) {
8
8
  function resolveProjectAgentId(input) {
9
9
  const fallback = getDefaultAgentId(input.config);
10
10
  const explicitAgentId = normalizeAgentId(input.explicitAgentId);
11
- if (explicitAgentId) return agentExists(explicitAgentId, input.config) ? explicitAgentId : fallback;
11
+ if (explicitAgentId && agentExists(explicitAgentId, input.config)) return explicitAgentId;
12
12
  const projectId = input.projectId?.trim();
13
13
  const projectAgentId = projectId ? normalizeAgentId(input.projects.get(projectId)?.defaultAgentId) : void 0;
14
14
  if (projectAgentId && agentExists(projectAgentId, input.config)) return projectAgentId;
@@ -0,0 +1,112 @@
1
+ ALTER TABLE task_sessions ADD COLUMN agent_id TEXT;
2
+ ALTER TABLE task_sessions ADD COLUMN run_id TEXT;
3
+ ALTER TABLE task_sessions ADD COLUMN assignment_epoch INTEGER NOT NULL DEFAULT 0;
4
+ ALTER TABLE task_sessions ADD COLUMN status TEXT NOT NULL DEFAULT 'completed'
5
+ CHECK (status IN ('active', 'completed', 'superseded', 'failed'));
6
+ ALTER TABLE task_sessions ADD COLUMN started_at INTEGER;
7
+ ALTER TABLE task_sessions ADD COLUMN ended_at INTEGER;
8
+
9
+ UPDATE task_sessions
10
+ SET agent_id = (
11
+ SELECT sessions.agent_id FROM sessions WHERE sessions.session_key = task_sessions.session_key
12
+ ), started_at = created_at;
13
+
14
+ UPDATE task_sessions
15
+ SET run_id = (
16
+ SELECT task_runs.run_id
17
+ FROM task_runs
18
+ WHERE task_runs.task_id = task_sessions.task_id
19
+ AND task_runs.session_key = task_sessions.session_key
20
+ ORDER BY task_runs.queued_at DESC
21
+ LIMIT 1
22
+ )
23
+ WHERE role = 'execution';
24
+
25
+ UPDATE task_sessions
26
+ SET status = 'superseded', ended_at = created_at
27
+ WHERE role = 'execution';
28
+
29
+ DELETE FROM task_sessions
30
+ WHERE role = 'execution'
31
+ AND session_key IS NOT NULL
32
+ AND task_session_id NOT IN (
33
+ SELECT canonical.task_session_id
34
+ FROM task_sessions AS canonical
35
+ WHERE canonical.role = 'execution'
36
+ AND canonical.session_key IS NOT NULL
37
+ AND canonical.task_session_id = (
38
+ SELECT candidate.task_session_id
39
+ FROM task_sessions AS candidate
40
+ WHERE candidate.role = 'execution'
41
+ AND candidate.session_key = canonical.session_key
42
+ ORDER BY candidate.created_at DESC, candidate.task_session_id DESC
43
+ LIMIT 1
44
+ )
45
+ );
46
+
47
+ UPDATE task_sessions
48
+ SET status = 'active', ended_at = NULL, assignment_epoch = 1
49
+ WHERE task_session_id IN (
50
+ SELECT latest.task_session_id FROM task_sessions AS latest
51
+ WHERE latest.role = 'execution'
52
+ AND latest.task_session_id = (
53
+ SELECT candidate.task_session_id
54
+ FROM task_sessions AS candidate
55
+ WHERE candidate.task_id = latest.task_id AND candidate.role = 'execution'
56
+ ORDER BY candidate.created_at DESC, candidate.task_session_id DESC
57
+ LIMIT 1
58
+ )
59
+ );
60
+
61
+ CREATE UNIQUE INDEX idx_task_sessions_active_execution
62
+ ON task_sessions(task_id)
63
+ WHERE role = 'execution' AND status = 'active';
64
+ CREATE UNIQUE INDEX idx_task_sessions_execution_owner
65
+ ON task_sessions(session_key)
66
+ WHERE role = 'execution' AND session_key IS NOT NULL;
67
+
68
+ CREATE TABLE task_conversation_state (
69
+ task_id TEXT PRIMARY KEY,
70
+ active_session_key TEXT,
71
+ current_executor_agent_id TEXT,
72
+ assignment_epoch INTEGER NOT NULL DEFAULT 0,
73
+ status TEXT NOT NULL DEFAULT 'idle'
74
+ CHECK (status IN ('idle', 'active')),
75
+ updated_at INTEGER NOT NULL,
76
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE,
77
+ FOREIGN KEY (active_session_key) REFERENCES sessions(session_key) ON DELETE SET NULL
78
+ );
79
+
80
+ INSERT INTO task_conversation_state (
81
+ task_id, active_session_key, current_executor_agent_id,
82
+ assignment_epoch, status, updated_at
83
+ )
84
+ SELECT
85
+ tasks.task_id,
86
+ active.session_key,
87
+ COALESCE(active.agent_id, tasks.delegate_agent_id, tasks.owner_id),
88
+ COALESCE(active.assignment_epoch, 0),
89
+ CASE WHEN active.session_key IS NULL THEN 'idle' ELSE 'active' END,
90
+ tasks.updated_at
91
+ FROM tasks
92
+ LEFT JOIN task_sessions AS active
93
+ ON active.task_id = tasks.task_id
94
+ AND active.role = 'execution'
95
+ AND active.status = 'active';
96
+
97
+ CREATE TABLE task_handoff_snapshots (
98
+ snapshot_id TEXT PRIMARY KEY,
99
+ task_id TEXT NOT NULL,
100
+ from_session_key TEXT,
101
+ to_session_key TEXT NOT NULL,
102
+ from_agent_id TEXT,
103
+ to_agent_id TEXT NOT NULL,
104
+ assignment_epoch INTEGER NOT NULL,
105
+ payload_json TEXT NOT NULL,
106
+ created_at INTEGER NOT NULL,
107
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE,
108
+ FOREIGN KEY (from_session_key) REFERENCES sessions(session_key) ON DELETE SET NULL,
109
+ FOREIGN KEY (to_session_key) REFERENCES sessions(session_key) ON DELETE CASCADE
110
+ );
111
+ CREATE INDEX idx_task_handoffs_task
112
+ ON task_handoff_snapshots(task_id, assignment_epoch DESC);
@@ -3,7 +3,7 @@ import type { ApplyMigrationsOptions } from './types.js';
3
3
  /** Baseline schema version applied from schema.sql on first open. */
4
4
  export declare const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;
5
5
  /** Latest schema version this release supports (increment when adding migrations). */
6
- export declare const XOPC_DB_SCHEMA_VERSION = 119;
6
+ export declare const XOPC_DB_SCHEMA_VERSION = 120;
7
7
  export declare function resolveMigrationsDir(override?: string): string;
8
8
  /**
9
9
  * Apply sequential SQL migrations from (currentVersion + 1) through targetVersion.
@@ -12,7 +12,7 @@ const log = createLogger("Sqlite:Migrations");
12
12
  /** Baseline schema version applied from schema.sql on first open. */
13
13
  const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;
14
14
  /** Latest schema version this release supports (increment when adding migrations). */
15
- const XOPC_DB_SCHEMA_VERSION = 119;
15
+ const XOPC_DB_SCHEMA_VERSION = 120;
16
16
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
17
17
  function backupBeforeTaskCutover(db, databasePath) {
18
18
  db.exec("PRAGMA wal_checkpoint(FULL)");
@@ -57,7 +57,7 @@ function applySingleMigration(db, migration) {
57
57
  * Each step runs in its own transaction and updates schema_meta on success.
58
58
  */
59
59
  function applyPendingMigrations(db, options = {}) {
60
- const targetVersion = options.targetVersion ?? 119;
60
+ const targetVersion = options.targetVersion ?? 120;
61
61
  let currentVersion = readSchemaVersion(db);
62
62
  if (currentVersion > targetVersion) throw new DatabaseSchemaTooNewError(currentVersion, targetVersion);
63
63
  if (currentVersion >= targetVersion) return currentVersion;
@@ -94,7 +94,7 @@ function applyPendingMigrations(db, options = {}) {
94
94
  }
95
95
  /** Inspect schema version without mutating the database. */
96
96
  function inspectSchemaMigrationStatus(db, options = {}) {
97
- const appVersion = options.targetVersion ?? 119;
97
+ const appVersion = options.targetVersion ?? 120;
98
98
  const dbVersion = readSchemaVersion(db);
99
99
  if (dbVersion > appVersion) return {
100
100
  dbVersion,