@pixelbyte-software/pixcode 1.34.0 → 1.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/LICENSE +718 -718
  2. package/README.de.md +248 -248
  3. package/README.ja.md +240 -240
  4. package/README.ko.md +240 -240
  5. package/README.md +303 -303
  6. package/README.ru.md +248 -248
  7. package/README.tr.md +250 -250
  8. package/README.zh-CN.md +240 -240
  9. package/dist/api-docs.html +548 -395
  10. package/dist/assets/index-B8w57E1r.css +32 -0
  11. package/dist/assets/index-CBdsvGSR.js +854 -0
  12. package/dist/clear-cache.html +85 -85
  13. package/dist/convert-icons.md +52 -52
  14. package/dist/favicon.svg +8 -8
  15. package/dist/generate-icons.js +48 -48
  16. package/dist/icons/codex-white.svg +3 -3
  17. package/dist/icons/codex.svg +3 -3
  18. package/dist/icons/cursor-white.svg +11 -11
  19. package/dist/icons/icon-128x128.svg +9 -9
  20. package/dist/icons/icon-144x144.svg +9 -9
  21. package/dist/icons/icon-152x152.svg +9 -9
  22. package/dist/icons/icon-192x192.svg +9 -9
  23. package/dist/icons/icon-384x384.svg +9 -9
  24. package/dist/icons/icon-512x512.svg +9 -9
  25. package/dist/icons/icon-72x72.svg +9 -9
  26. package/dist/icons/icon-96x96.svg +9 -9
  27. package/dist/icons/icon-template.svg +9 -9
  28. package/dist/icons/qwen-logo.svg +14 -14
  29. package/dist/index.html +59 -59
  30. package/dist/logo.svg +12 -12
  31. package/dist/manifest.json +60 -60
  32. package/dist/openapi.yaml +1693 -1311
  33. package/dist/sw.js +124 -124
  34. package/dist-server/server/claude-sdk.js +38 -7
  35. package/dist-server/server/claude-sdk.js.map +1 -1
  36. package/dist-server/server/cli.js +107 -112
  37. package/dist-server/server/cli.js.map +1 -1
  38. package/dist-server/server/daemon/manager.js +33 -33
  39. package/dist-server/server/daemon-manager.js +159 -112
  40. package/dist-server/server/daemon-manager.js.map +1 -1
  41. package/dist-server/server/database/json-store.js +8 -5
  42. package/dist-server/server/database/json-store.js.map +1 -1
  43. package/dist-server/server/index.js +31 -10
  44. package/dist-server/server/index.js.map +1 -1
  45. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js +45 -19
  46. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js.map +1 -1
  47. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -1
  48. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +1 -0
  49. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -1
  50. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js +202 -0
  51. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js.map +1 -0
  52. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js +205 -0
  53. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js.map +1 -0
  54. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js +205 -0
  55. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js.map +1 -0
  56. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js +205 -0
  57. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js.map +1 -0
  58. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js +205 -0
  59. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js.map +1 -0
  60. package/dist-server/server/modules/orchestration/a2a/routes.js +298 -34
  61. package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -1
  62. package/dist-server/server/modules/orchestration/a2a/task-store.js +144 -0
  63. package/dist-server/server/modules/orchestration/a2a/task-store.js.map +1 -0
  64. package/dist-server/server/modules/orchestration/a2a/validator.js +16 -0
  65. package/dist-server/server/modules/orchestration/a2a/validator.js.map +1 -1
  66. package/dist-server/server/modules/orchestration/index.js +14 -0
  67. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  68. package/dist-server/server/modules/orchestration/preview/port-watcher.js +90 -0
  69. package/dist-server/server/modules/orchestration/preview/port-watcher.js.map +1 -0
  70. package/dist-server/server/modules/orchestration/preview/preview-proxy.js +58 -0
  71. package/dist-server/server/modules/orchestration/preview/preview-proxy.js.map +1 -0
  72. package/dist-server/server/modules/orchestration/preview/types.js +2 -0
  73. package/dist-server/server/modules/orchestration/preview/types.js.map +1 -0
  74. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js +37 -0
  75. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js.map +1 -0
  76. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +68 -0
  77. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -0
  78. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +128 -0
  79. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -0
  80. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js +2 -0
  81. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js.map +1 -0
  82. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js +126 -0
  83. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js.map +1 -0
  84. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +1047 -0
  85. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -0
  86. package/dist-server/server/modules/orchestration/workflows/workflow-store.js +76 -0
  87. package/dist-server/server/modules/orchestration/workflows/workflow-store.js.map +1 -0
  88. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +151 -0
  89. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -0
  90. package/dist-server/server/modules/orchestration/workflows/workflow.types.js +2 -0
  91. package/dist-server/server/modules/orchestration/workflows/workflow.types.js.map +1 -0
  92. package/dist-server/server/modules/orchestration/workflows/workspace-target.js +98 -0
  93. package/dist-server/server/modules/orchestration/workflows/workspace-target.js.map +1 -0
  94. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js +122 -0
  95. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js.map +1 -0
  96. package/dist-server/server/modules/orchestration/workspace/path-safety.js +48 -0
  97. package/dist-server/server/modules/orchestration/workspace/path-safety.js.map +1 -0
  98. package/dist-server/server/modules/orchestration/workspace/types.js +11 -0
  99. package/dist-server/server/modules/orchestration/workspace/types.js.map +1 -0
  100. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js +80 -0
  101. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js.map +1 -0
  102. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js +96 -0
  103. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js.map +1 -0
  104. package/dist-server/server/modules/providers/index.js +3 -0
  105. package/dist-server/server/modules/providers/index.js.map +1 -0
  106. package/dist-server/server/openai-codex.js +35 -4
  107. package/dist-server/server/openai-codex.js.map +1 -1
  108. package/dist-server/server/routes/commands.js +25 -25
  109. package/dist-server/server/routes/git.js +17 -17
  110. package/dist-server/server/routes/taskmaster.js +525 -508
  111. package/dist-server/server/routes/taskmaster.js.map +1 -1
  112. package/package.json +180 -178
  113. package/scripts/fix-node-pty.js +67 -67
  114. package/scripts/smoke/a2a-roundtrip.mjs +86 -17
  115. package/scripts/smoke/orchestration-api.mjs +172 -0
  116. package/scripts/smoke/orchestration-live-run.mjs +176 -0
  117. package/server/claude-sdk.js +898 -857
  118. package/server/cli.js +935 -940
  119. package/server/constants/config.js +4 -4
  120. package/server/cursor-cli.js +342 -342
  121. package/server/daemon/manager.js +564 -564
  122. package/server/daemon-manager.js +959 -920
  123. package/server/database/db.js +794 -794
  124. package/server/database/json-store.js +197 -194
  125. package/server/gemini-cli.js +535 -535
  126. package/server/gemini-response-handler.js +79 -79
  127. package/server/index.js +3135 -3104
  128. package/server/load-env.js +34 -34
  129. package/server/middleware/auth.js +173 -173
  130. package/server/modules/orchestration/a2a/adapter-registry.ts +72 -22
  131. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +9 -3
  132. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +1 -0
  133. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -0
  134. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -0
  135. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -0
  136. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -0
  137. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -0
  138. package/server/modules/orchestration/a2a/routes.ts +349 -36
  139. package/server/modules/orchestration/a2a/task-store.ts +178 -0
  140. package/server/modules/orchestration/a2a/types.ts +14 -0
  141. package/server/modules/orchestration/a2a/validator.ts +25 -2
  142. package/server/modules/orchestration/index.ts +40 -0
  143. package/server/modules/orchestration/preview/port-watcher.ts +112 -0
  144. package/server/modules/orchestration/preview/preview-proxy.ts +60 -0
  145. package/server/modules/orchestration/preview/types.ts +19 -0
  146. package/server/modules/orchestration/tasks/orchestration-task-store.ts +45 -0
  147. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +73 -0
  148. package/server/modules/orchestration/tasks/orchestration-task.service.ts +145 -0
  149. package/server/modules/orchestration/tasks/orchestration-task.types.ts +29 -0
  150. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -0
  151. package/server/modules/orchestration/workflows/workflow-runner.ts +1206 -0
  152. package/server/modules/orchestration/workflows/workflow-store.ts +97 -0
  153. package/server/modules/orchestration/workflows/workflow.routes.ts +169 -0
  154. package/server/modules/orchestration/workflows/workflow.types.ts +70 -0
  155. package/server/modules/orchestration/workflows/workspace-target.ts +120 -0
  156. package/server/modules/orchestration/workspace/docker-workspace.ts +135 -0
  157. package/server/modules/orchestration/workspace/path-safety.ts +55 -0
  158. package/server/modules/orchestration/workspace/types.ts +52 -0
  159. package/server/modules/orchestration/workspace/workspace-manager.ts +97 -0
  160. package/server/modules/orchestration/workspace/worktree-workspace.ts +125 -0
  161. package/server/modules/providers/index.ts +2 -0
  162. package/server/modules/providers/list/claude/claude-auth.provider.ts +145 -145
  163. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  164. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  165. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  166. package/server/modules/providers/list/codex/codex-auth.provider.ts +115 -115
  167. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  168. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  169. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  170. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  171. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  172. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  173. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  174. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +163 -163
  175. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  176. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  177. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  178. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +130 -130
  179. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  180. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +232 -232
  181. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  182. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -145
  183. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  184. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
  185. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  186. package/server/modules/providers/provider.registry.ts +40 -40
  187. package/server/modules/providers/provider.routes.ts +819 -819
  188. package/server/modules/providers/services/mcp.service.ts +86 -86
  189. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  190. package/server/modules/providers/services/sessions.service.ts +45 -45
  191. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  192. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  193. package/server/modules/providers/shared/provider-configs.ts +142 -142
  194. package/server/modules/providers/tests/mcp.test.ts +293 -293
  195. package/server/openai-codex.js +462 -426
  196. package/server/opencode-cli.js +459 -459
  197. package/server/opencode-response-handler.js +107 -107
  198. package/server/projects.js +3105 -3105
  199. package/server/qwen-code-cli.js +395 -395
  200. package/server/qwen-response-handler.js +73 -73
  201. package/server/routes/agent.js +1365 -1365
  202. package/server/routes/auth.js +138 -138
  203. package/server/routes/codex.js +19 -19
  204. package/server/routes/commands.js +554 -554
  205. package/server/routes/cursor.js +52 -52
  206. package/server/routes/gemini.js +24 -24
  207. package/server/routes/git.js +1488 -1488
  208. package/server/routes/mcp-utils.js +31 -31
  209. package/server/routes/messages.js +61 -61
  210. package/server/routes/network.js +120 -120
  211. package/server/routes/plugins.js +318 -318
  212. package/server/routes/projects.js +915 -915
  213. package/server/routes/qwen.js +27 -27
  214. package/server/routes/settings.js +286 -286
  215. package/server/routes/taskmaster.js +1496 -1471
  216. package/server/routes/telegram.js +125 -125
  217. package/server/routes/user.js +123 -123
  218. package/server/services/external-access.js +171 -171
  219. package/server/services/install-jobs.js +571 -571
  220. package/server/services/notification-orchestrator.js +242 -242
  221. package/server/services/provider-credentials.js +189 -189
  222. package/server/services/provider-models.js +381 -381
  223. package/server/services/telegram/bot.js +279 -279
  224. package/server/services/telegram/telegram-http-client.js +130 -130
  225. package/server/services/telegram/translations.js +170 -170
  226. package/server/services/vapid-keys.js +36 -36
  227. package/server/sessionManager.js +225 -225
  228. package/server/shared/interfaces.ts +54 -54
  229. package/server/shared/types.ts +172 -172
  230. package/server/shared/utils.ts +193 -193
  231. package/server/tsconfig.json +36 -36
  232. package/server/utils/colors.js +21 -21
  233. package/server/utils/commandParser.js +303 -303
  234. package/server/utils/frontmatter.js +18 -18
  235. package/server/utils/gitConfig.js +34 -34
  236. package/server/utils/mcp-detector.js +147 -147
  237. package/server/utils/plugin-loader.js +457 -457
  238. package/server/utils/plugin-process-manager.js +184 -184
  239. package/server/utils/port-access.js +209 -209
  240. package/server/utils/runtime-paths.js +37 -37
  241. package/server/utils/taskmaster-websocket.js +128 -128
  242. package/server/utils/url-detection.js +71 -71
  243. package/server/vite-daemon.js +78 -78
  244. package/shared/modelConstants.js +162 -162
  245. package/shared/networkHosts.js +22 -22
  246. package/dist/assets/index-B1ghfb4w.css +0 -32
  247. package/dist/assets/index-BvClqlMf.js +0 -852
@@ -10,6 +10,7 @@ import { adapterRegistry } from '@/modules/orchestration/a2a/adapter-registry.js
10
10
  import { buildPixcodeAgentCard } from '@/modules/orchestration/a2a/agent-card.js';
11
11
  import { a2aAuth } from '@/modules/orchestration/a2a/auth.middleware.js';
12
12
  import { a2aBus } from '@/modules/orchestration/a2a/bus.js';
13
+ import { A2ATaskStore } from '@/modules/orchestration/a2a/task-store.js';
13
14
  import type {
14
15
  BusEvent,
15
16
  Message,
@@ -21,21 +22,58 @@ import {
21
22
  assertMessage,
22
23
  assertSubmitTaskInput,
23
24
  } from '@/modules/orchestration/a2a/validator.js';
25
+ import { portWatcher } from '@/modules/orchestration/preview/port-watcher.js';
26
+ import type { PreviewArtifactData } from '@/modules/orchestration/preview/types.js';
27
+ import { workspaceManager } from '@/modules/orchestration/workspace/workspace-manager.js';
28
+ import type {
29
+ WorkspaceHandle,
30
+ WorkspaceKind,
31
+ WorkspaceMetadata,
32
+ } from '@/modules/orchestration/workspace/types.js';
33
+ import { WorkspaceError } from '@/modules/orchestration/workspace/types.js';
34
+
35
+ type RoutingHints = {
36
+ preferredAdapterId?: string;
37
+ preferredProvider?: string;
38
+ preferredSkillId?: string;
39
+ };
40
+
41
+ function readRoutingHints(value: unknown): RoutingHints {
42
+ if (!value || typeof value !== 'object') {
43
+ return {};
44
+ }
24
45
 
25
- // In-memory task store. Persistence is out of scope for the foundation;
26
- // a follow-on plan adds SQLite-backed storage.
27
- const tasks = new Map<string, Task>();
46
+ const source = value as Record<string, unknown>;
47
+ return {
48
+ preferredAdapterId:
49
+ typeof source.preferredAdapterId === 'string' ? source.preferredAdapterId : undefined,
50
+ preferredProvider:
51
+ typeof source.preferredProvider === 'string' ? source.preferredProvider : undefined,
52
+ preferredSkillId:
53
+ typeof source.preferredSkillId === 'string' ? source.preferredSkillId : undefined,
54
+ };
55
+ }
56
+
57
+ const TERMINAL: TaskState[] = ['completed', 'canceled', 'failed'];
28
58
  // Per-task bus unsubscribe handles; called on terminal state.
29
59
  const taskUnsubs = new Map<string, () => void>();
30
60
  // Eviction timeouts (terminal tasks live for 1 hour before being purged).
31
61
  const taskEvictions = new Map<string, NodeJS.Timeout>();
32
62
  const TERMINAL_TASK_TTL_MS = 60 * 60 * 1000; // 1 hour
33
63
  const MAX_TASKS = 1000;
64
+ const taskStore = new A2ATaskStore({ terminalTaskTtlMs: TERMINAL_TASK_TTL_MS });
65
+ const activeWorkspaces = new Map<string, WorkspaceHandle>();
66
+ const previewStops = new Map<string, () => void>();
67
+ const finalizingTasks = new Set<string>();
34
68
 
35
69
  function newId(prefix: string): string {
36
70
  return `${prefix}_${crypto.randomBytes(8).toString('hex')}`;
37
71
  }
38
72
 
73
+ function isTerminalTaskState(state: TaskState): boolean {
74
+ return TERMINAL.includes(state);
75
+ }
76
+
39
77
  function getBaseUrl(req: Request): string {
40
78
  // TODO: this trusts X-Forwarded-Proto/Host without checking app's
41
79
  // trust-proxy setting. Same posture as auth.middleware.ts; revisit
@@ -45,40 +83,174 @@ function getBaseUrl(req: Request): string {
45
83
  return `${proto}://${host}`;
46
84
  }
47
85
 
86
+ function scheduleTaskEviction(taskId: string): void {
87
+ const existingTimeout = taskEvictions.get(taskId);
88
+ if (existingTimeout) {
89
+ clearTimeout(existingTimeout);
90
+ }
91
+
92
+ const task = taskStore.get(taskId);
93
+ if (!task) {
94
+ taskEvictions.delete(taskId);
95
+ return;
96
+ }
97
+
98
+ const remainingMs = Math.max(0, task.updatedAt + TERMINAL_TASK_TTL_MS - Date.now());
99
+ taskEvictions.set(
100
+ taskId,
101
+ setTimeout(() => {
102
+ taskStore.delete(taskId);
103
+ taskEvictions.delete(taskId);
104
+ }, remainingMs),
105
+ );
106
+ }
107
+
108
+ function parseTaskState(value: unknown): TaskState | undefined {
109
+ if (typeof value !== 'string') {
110
+ return undefined;
111
+ }
112
+
113
+ const normalized = value.trim();
114
+ if (
115
+ normalized === 'submitted' ||
116
+ normalized === 'working' ||
117
+ normalized === 'input-required' ||
118
+ normalized === 'completed' ||
119
+ normalized === 'canceled' ||
120
+ normalized === 'failed'
121
+ ) {
122
+ return normalized;
123
+ }
124
+
125
+ return undefined;
126
+ }
127
+
128
+ function parsePositiveInt(value: unknown, fallback: number): number {
129
+ if (typeof value !== 'string') {
130
+ return fallback;
131
+ }
132
+
133
+ const parsed = Number.parseInt(value, 10);
134
+ if (!Number.isFinite(parsed) || parsed < 0) {
135
+ return fallback;
136
+ }
137
+
138
+ return parsed;
139
+ }
140
+
141
+ function readString(value: unknown): string | undefined {
142
+ return typeof value === 'string' && value.trim() ? value : undefined;
143
+ }
144
+
145
+ function readBoolean(value: unknown): boolean | undefined {
146
+ return typeof value === 'boolean' ? value : undefined;
147
+ }
148
+
149
+ function readObject(value: unknown): Record<string, unknown> | undefined {
150
+ return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
151
+ }
152
+
153
+ function readWorkspaceKind(value: unknown): WorkspaceKind | undefined {
154
+ return value === 'host' || value === 'worktree' || value === 'docker' ? value : undefined;
155
+ }
156
+
157
+ function workspaceMetadata(workspace: WorkspaceHandle, keepAfterCompletion?: boolean): WorkspaceMetadata {
158
+ return {
159
+ id: workspace.id,
160
+ kind: workspace.kind,
161
+ path: workspace.path,
162
+ baseRef: workspace.baseRef,
163
+ branchName: workspace.branchName,
164
+ keepAfterCompletion,
165
+ };
166
+ }
167
+
168
+ async function finalizeTerminalTask(task: Task): Promise<void> {
169
+ if (finalizingTasks.has(task.id)) return;
170
+ finalizingTasks.add(task.id);
171
+
172
+ const stopPreview = previewStops.get(task.id);
173
+ if (stopPreview) {
174
+ stopPreview();
175
+ previewStops.delete(task.id);
176
+ }
177
+
178
+ const workspace = activeWorkspaces.get(task.id);
179
+ try {
180
+ if (workspace) {
181
+ const diff = await workspace.diff();
182
+ a2aBus.publish({
183
+ kind: 'artifact',
184
+ taskId: task.id,
185
+ artifact: {
186
+ artifactId: newId('art'),
187
+ type: 'file-diff',
188
+ parts: [{ kind: 'text', text: diff }],
189
+ metadata: {
190
+ source: 'workspace-diff',
191
+ workspaceId: workspace.id,
192
+ workspaceKind: workspace.kind,
193
+ baseRef: workspace.baseRef,
194
+ },
195
+ },
196
+ });
197
+
198
+ const keepAfterCompletion = task.metadata?.workspace &&
199
+ typeof task.metadata.workspace === 'object' &&
200
+ readBoolean((task.metadata.workspace as Record<string, unknown>).keepAfterCompletion);
201
+ if (workspace.kind !== 'host' && keepAfterCompletion !== true) {
202
+ await workspace.destroy();
203
+ }
204
+ }
205
+ } catch (error) {
206
+ const message = error instanceof Error ? error.message : String(error);
207
+ task.metadata = {
208
+ ...task.metadata,
209
+ workspaceFinalizationError: message,
210
+ };
211
+ task.updatedAt = Date.now();
212
+ taskStore.set(task);
213
+ } finally {
214
+ activeWorkspaces.delete(task.id);
215
+ const unsub = taskUnsubs.get(task.id);
216
+ if (unsub) {
217
+ unsub();
218
+ taskUnsubs.delete(task.id);
219
+ }
220
+ scheduleTaskEviction(task.id);
221
+ finalizingTasks.delete(task.id);
222
+ }
223
+ }
224
+
48
225
  function attachBusToTask(task: Task): void {
49
226
  const unsubscribe = a2aBus.subscribe(task.id, (event: BusEvent) => {
50
227
  if (event.kind === 'task-state') {
51
228
  task.state = event.state;
52
229
  if (event.error) task.error = event.error;
53
230
  task.updatedAt = Date.now();
54
- if (event.state === 'completed' || event.state === 'canceled' || event.state === 'failed') {
55
- // Release the listener; schedule eviction.
56
- const unsub = taskUnsubs.get(task.id);
57
- if (unsub) {
58
- unsub();
59
- taskUnsubs.delete(task.id);
60
- }
61
- const existingTimeout = taskEvictions.get(task.id);
62
- if (existingTimeout) clearTimeout(existingTimeout);
63
- taskEvictions.set(
64
- task.id,
65
- setTimeout(() => {
66
- tasks.delete(task.id);
67
- taskEvictions.delete(task.id);
68
- }, TERMINAL_TASK_TTL_MS),
69
- );
231
+ taskStore.set(task);
232
+ if (isTerminalTaskState(event.state)) {
233
+ void finalizeTerminalTask(task);
70
234
  }
71
235
  } else if (event.kind === 'message') {
72
236
  task.history.push(event.message);
73
237
  task.updatedAt = Date.now();
238
+ taskStore.set(task);
74
239
  } else if (event.kind === 'artifact') {
75
240
  task.artifacts.push(event.artifact);
76
241
  task.updatedAt = Date.now();
242
+ taskStore.set(task);
77
243
  }
78
244
  });
79
245
  taskUnsubs.set(task.id, unsubscribe);
80
246
  }
81
247
 
248
+ for (const task of taskStore.values()) {
249
+ if (isTerminalTaskState(task.state)) {
250
+ scheduleTaskEviction(task.id);
251
+ }
252
+ }
253
+
82
254
  export function createA2ARouter(): Router {
83
255
  const router: Router = express.Router();
84
256
 
@@ -103,6 +275,57 @@ export function createA2ARouter(): Router {
103
275
  res.json(adapter.agentCard);
104
276
  });
105
277
 
278
+ router.post('/adapters/resolve', (req, res) => {
279
+ const selector = typeof req.body?.adapterId === 'string' ? req.body.adapterId : '';
280
+ if (!selector.trim()) {
281
+ res.status(400).json({
282
+ error: { code: 'ADAPTER_ID_REQUIRED', message: 'adapterId is required.' },
283
+ });
284
+ return;
285
+ }
286
+
287
+ const routing = readRoutingHints(req.body?.routing);
288
+ const adapter = adapterRegistry.resolve(selector, routing);
289
+ if (!adapter) {
290
+ res.status(404).json({
291
+ error: {
292
+ code: 'ADAPTER_NOT_FOUND',
293
+ message: selector,
294
+ availableAdapters: adapterRegistry.list().map((candidate) => candidate.id),
295
+ },
296
+ });
297
+ return;
298
+ }
299
+
300
+ res.json({
301
+ selector,
302
+ resolvedAdapterId: adapter.id,
303
+ agentCard: adapter.agentCard,
304
+ });
305
+ });
306
+
307
+ router.get('/tasks', (req, res) => {
308
+ const state = parseTaskState(req.query.state);
309
+ const contextId = typeof req.query.contextId === 'string' ? req.query.contextId : undefined;
310
+ const adapterId = typeof req.query.adapterId === 'string' ? req.query.adapterId : undefined;
311
+ const limit = parsePositiveInt(req.query.limit, 50);
312
+
313
+ const tasks = taskStore
314
+ .list({ state, contextId, adapterId, limit })
315
+ .map((task) => taskStore.summarize(task));
316
+
317
+ res.json({
318
+ tasks,
319
+ count: tasks.length,
320
+ filters: {
321
+ state,
322
+ contextId,
323
+ adapterId,
324
+ limit,
325
+ },
326
+ });
327
+ });
328
+
106
329
  // Task lifecycle
107
330
  router.post('/tasks', async (req: Request, res: Response) => {
108
331
  try {
@@ -113,20 +336,27 @@ export function createA2ARouter(): Router {
113
336
  return;
114
337
  }
115
338
 
116
- const adapter = adapterRegistry.get(req.body.adapterId);
339
+ const metadata = req.body.metadata as Record<string, unknown> | undefined;
340
+ const routing = readRoutingHints(metadata?.routing);
341
+
342
+ const adapter = adapterRegistry.resolve(req.body.adapterId, routing);
117
343
  if (!adapter) {
118
344
  res.status(404).json({
119
- error: { code: 'ADAPTER_NOT_FOUND', message: req.body.adapterId },
345
+ error: {
346
+ code: 'ADAPTER_NOT_FOUND',
347
+ message: req.body.adapterId,
348
+ availableAdapters: adapterRegistry.list().map((candidate) => candidate.id),
349
+ },
120
350
  });
121
351
  return;
122
352
  }
123
353
 
124
354
  // Enforce MAX_TASKS cap. Evict the oldest terminal task first; if all
125
355
  // active, fail closed with 503.
126
- if (tasks.size >= MAX_TASKS) {
356
+ if (taskStore.size >= MAX_TASKS) {
127
357
  let evicted = false;
128
- for (const [tid, t] of tasks) {
129
- if (t.state === 'completed' || t.state === 'canceled' || t.state === 'failed') {
358
+ for (const [tid, t] of taskStore.entries()) {
359
+ if (isTerminalTaskState(t.state)) {
130
360
  const timeout = taskEvictions.get(tid);
131
361
  if (timeout) clearTimeout(timeout);
132
362
  taskEvictions.delete(tid);
@@ -135,7 +365,7 @@ export function createA2ARouter(): Router {
135
365
  unsub();
136
366
  taskUnsubs.delete(tid);
137
367
  }
138
- tasks.delete(tid);
368
+ taskStore.delete(tid);
139
369
  evicted = true;
140
370
  break;
141
371
  }
@@ -153,20 +383,98 @@ export function createA2ARouter(): Router {
153
383
  id: newId('task'),
154
384
  contextId: req.body.contextId,
155
385
  state: 'submitted',
156
- history: [userMessage],
386
+ history: [],
157
387
  artifacts: [],
158
388
  metadata: req.body.metadata,
159
389
  createdAt: Date.now(),
160
390
  updatedAt: Date.now(),
161
391
  };
162
- tasks.set(task.id, task);
392
+ task.history.push({ ...userMessage, taskId: task.id });
393
+ const workspaceOptions = (metadata?.workspace && typeof metadata.workspace === 'object'
394
+ ? metadata.workspace
395
+ : {}) as Record<string, unknown>;
163
396
  // Persist adapterId in metadata so cancel can resolve the owning adapter
164
397
  // even when the original request body is no longer available.
165
- task.metadata = { ...task.metadata, adapterId: req.body.adapterId };
398
+ task.metadata = {
399
+ ...task.metadata,
400
+ adapterId: adapter.id,
401
+ adapterSelector: req.body.adapterId,
402
+ };
403
+ taskStore.set(task);
166
404
  attachBusToTask(task);
167
405
 
406
+ let workspace: WorkspaceHandle;
168
407
  try {
169
- await adapter.submitTask(task, { cwd: process.cwd() });
408
+ workspace = await workspaceManager.create({
409
+ taskId: task.id,
410
+ projectPath: readString(workspaceOptions.projectPath) ?? process.cwd(),
411
+ kind: readWorkspaceKind(workspaceOptions.kind) ?? readWorkspaceKind(metadata?.isolation),
412
+ baseRef: readString(workspaceOptions.baseRef) ?? readString(metadata?.baseRef) ?? 'HEAD',
413
+ keepAfterCompletion: readBoolean(workspaceOptions.keepAfterCompletion),
414
+ metadata: workspaceOptions,
415
+ });
416
+ } catch (err) {
417
+ const workspaceError = err instanceof WorkspaceError ? err : undefined;
418
+ a2aBus.publish({
419
+ kind: 'task-state',
420
+ taskId: task.id,
421
+ state: 'failed',
422
+ error: {
423
+ code: workspaceError?.code ?? 'WORKSPACE_CREATE_FAILED',
424
+ message: err instanceof Error ? err.message : String(err),
425
+ details: workspaceError?.details,
426
+ },
427
+ });
428
+ res.status(202).json(task);
429
+ return;
430
+ }
431
+
432
+ activeWorkspaces.set(task.id, workspace);
433
+ task.metadata = {
434
+ ...task.metadata,
435
+ workspace: workspaceMetadata(workspace, readBoolean(workspaceOptions.keepAfterCompletion)),
436
+ };
437
+ taskStore.set(task);
438
+
439
+ previewStops.set(
440
+ task.id,
441
+ portWatcher.watch({
442
+ taskId: task.id,
443
+ workspace,
444
+ onPort: (event) => {
445
+ const data: PreviewArtifactData = {
446
+ url: event.url,
447
+ proxiedUrl: `/preview/${event.port}/`,
448
+ port: event.port,
449
+ host: event.host,
450
+ processName: event.processName,
451
+ confidence: event.confidence,
452
+ };
453
+ a2aBus.publish({
454
+ kind: 'artifact',
455
+ taskId: task.id,
456
+ artifact: {
457
+ artifactId: newId('art'),
458
+ type: 'preview-url',
459
+ parts: [{ kind: 'data', data: { ...data } }],
460
+ metadata: {
461
+ source: 'port-watcher',
462
+ workspaceId: workspace.id,
463
+ },
464
+ },
465
+ });
466
+ },
467
+ }),
468
+ );
469
+
470
+ try {
471
+ await adapter.submitTask(task, {
472
+ cwd: workspace.path,
473
+ workspace,
474
+ model: readString(metadata?.model),
475
+ permissionMode: readString(metadata?.permissionMode),
476
+ toolsSettings: readObject(metadata?.toolsSettings),
477
+ });
170
478
  } catch (err) {
171
479
  // Publish to bus so SSE subscribers and the attachBusToTask listener
172
480
  // both see the failure transition. The listener mutates the stored
@@ -186,7 +494,7 @@ export function createA2ARouter(): Router {
186
494
  });
187
495
 
188
496
  router.get('/tasks/:id', (req, res) => {
189
- const task = tasks.get(req.params.id);
497
+ const task = taskStore.get(req.params.id);
190
498
  if (!task) {
191
499
  res.status(404).json({ error: { code: 'TASK_NOT_FOUND', message: req.params.id } });
192
500
  return;
@@ -195,7 +503,7 @@ export function createA2ARouter(): Router {
195
503
  });
196
504
 
197
505
  router.get('/tasks/:id/stream', (req, res) => {
198
- const task = tasks.get(req.params.id);
506
+ const task = taskStore.get(req.params.id);
199
507
  if (!task) {
200
508
  res.status(404).json({ error: { code: 'TASK_NOT_FOUND', message: req.params.id } });
201
509
  return;
@@ -209,11 +517,10 @@ export function createA2ARouter(): Router {
209
517
  const initial = { kind: 'task-snapshot' as const, task };
210
518
  res.write(`event: snapshot\ndata: ${JSON.stringify(initial)}\n\n`);
211
519
 
212
- const TERMINAL: TaskState[] = ['completed', 'canceled', 'failed'];
213
520
  const unsubscribe = a2aBus.subscribe(task.id, (event) => {
214
521
  res.write(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`);
215
522
  if (event.kind === 'task-state' && TERMINAL.includes(event.state)) {
216
- res.end();
523
+ setTimeout(() => res.end(), 1500);
217
524
  }
218
525
  });
219
526
 
@@ -223,14 +530,14 @@ export function createA2ARouter(): Router {
223
530
  });
224
531
 
225
532
  router.post('/tasks/:id/cancel', async (req, res) => {
226
- const task = tasks.get(req.params.id);
533
+ const task = taskStore.get(req.params.id);
227
534
  if (!task) {
228
535
  res.status(404).json({ error: { code: 'TASK_NOT_FOUND', message: req.params.id } });
229
536
  return;
230
537
  }
231
538
  // Look up the adapter that owns this task. We stored adapterId in metadata.
232
539
  const adapterId = req.body?.adapterId ?? task.metadata?.adapterId;
233
- const adapter = typeof adapterId === 'string' ? adapterRegistry.get(adapterId) : undefined;
540
+ const adapter = typeof adapterId === 'string' ? adapterRegistry.resolve(adapterId) : undefined;
234
541
  if (!adapter) {
235
542
  res.status(400).json({
236
543
  error: {
@@ -241,7 +548,7 @@ export function createA2ARouter(): Router {
241
548
  return;
242
549
  }
243
550
  await adapter.cancelTask(task.id);
244
- res.json(tasks.get(task.id));
551
+ res.json(taskStore.get(task.id));
245
552
  });
246
553
 
247
554
  router.post('/messages', (req, res) => {
@@ -252,6 +559,12 @@ export function createA2ARouter(): Router {
252
559
  res.status(400).json({ error: { code: 'INVALID_INPUT', message: e.message, path: e.path } });
253
560
  return;
254
561
  }
562
+ if (typeof req.body.taskId === 'string' && !taskStore.get(req.body.taskId)) {
563
+ res.status(404).json({
564
+ error: { code: 'TASK_NOT_FOUND', message: req.body.taskId },
565
+ });
566
+ return;
567
+ }
255
568
  a2aBus.publish({
256
569
  kind: 'message',
257
570
  taskId: req.body.taskId ?? 'broadcast',
@@ -0,0 +1,178 @@
1
+ // server/modules/orchestration/a2a/task-store.ts
2
+ // A2A task persistence backed by shared JsonStore for atomic writes and corruption recovery.
3
+ // Exposes a Map-compatible surface (get/set/delete/size/entries/values) so routes.ts
4
+ // can treat the store as a typed Map while still benefiting from durable JSON storage.
5
+
6
+ import os from 'os';
7
+ import path from 'path';
8
+
9
+ import { JsonStore } from '@/database/json-store.js';
10
+ import type { Message, Task, TaskState, TaskSummary } from '@/modules/orchestration/a2a/types.js';
11
+
12
+ const filePath = path.join(os.homedir(), '.pixcode', 'a2a-tasks.json');
13
+
14
+ export interface A2ATaskStoreOptions {
15
+ terminalTaskTtlMs?: number;
16
+ }
17
+
18
+ export interface ListOptions {
19
+ state?: TaskState;
20
+ contextId?: string;
21
+ adapterId?: string;
22
+ limit?: number;
23
+ }
24
+
25
+ export class A2ATaskStore {
26
+ private store: JsonStore;
27
+
28
+ constructor(_options?: A2ATaskStoreOptions) {
29
+ this.store = new JsonStore(filePath, { a2a_tasks: [] });
30
+ }
31
+
32
+ // ── Map-compatible API ─────────────────────────────────────────────────────
33
+
34
+ get(id: string): Task | undefined {
35
+ return this.store.findWhere('a2a_tasks', (t: Task) => t.id === id) ?? undefined;
36
+ }
37
+
38
+ set(task: Task): void {
39
+ const existing = this.get(task.id);
40
+ if (existing) {
41
+ this.store.updateWhere('a2a_tasks', (t: Task) => t.id === task.id, task);
42
+ } else {
43
+ this.store.insert('a2a_tasks', task, { autoId: false });
44
+ }
45
+ }
46
+
47
+ delete(id: string): void {
48
+ this.store.deleteWhere('a2a_tasks', (t: Task) => t.id === id);
49
+ }
50
+
51
+ get size(): number {
52
+ return (this.store.filterWhere('a2a_tasks', () => true) as Task[]).length;
53
+ }
54
+
55
+ *entries(): IterableIterator<[string, Task]> {
56
+ const tasks = this.store.filterWhere('a2a_tasks', () => true) as Task[];
57
+ for (const task of tasks) {
58
+ yield [task.id, task];
59
+ }
60
+ }
61
+
62
+ *values(): IterableIterator<Task> {
63
+ const tasks = this.store.filterWhere('a2a_tasks', () => true) as Task[];
64
+ for (const task of tasks) {
65
+ yield task;
66
+ }
67
+ }
68
+
69
+ // ── Query helpers ──────────────────────────────────────────────────────────
70
+
71
+ list(options: ListOptions = {}): Task[] {
72
+ const { state, contextId, adapterId, limit = 50 } = options;
73
+ let tasks = this.store.filterWhere('a2a_tasks', (t: Task) => {
74
+ if (state && t.state !== state) return false;
75
+ if (contextId && t.contextId !== contextId) return false;
76
+ if (adapterId && (t.metadata?.adapterId as string | undefined) !== adapterId) return false;
77
+ return true;
78
+ }) as Task[];
79
+ if (limit > 0) tasks = tasks.slice(0, limit);
80
+ return tasks;
81
+ }
82
+
83
+ summarize(task: Task): TaskSummary {
84
+ return {
85
+ id: task.id,
86
+ contextId: task.contextId,
87
+ state: task.state,
88
+ adapterId: task.metadata?.adapterId as string | undefined,
89
+ adapterSelector: task.metadata?.adapterSelector as string | undefined,
90
+ error: task.error,
91
+ createdAt: task.createdAt,
92
+ updatedAt: task.updatedAt,
93
+ messageCount: task.history?.length ?? 0,
94
+ artifactCount: task.artifacts?.length ?? 0,
95
+ lastMessage: task.history?.at(-1),
96
+ };
97
+ }
98
+
99
+ // ── Legacy named methods (kept for any future internal callers) ────────────
100
+
101
+ getTask(id: string): Task | undefined {
102
+ return this.get(id);
103
+ }
104
+
105
+ getTaskByIndex(index: number): Task | undefined {
106
+ const tasks = this.store.filterWhere('a2a_tasks', () => true) as Task[];
107
+ return tasks[index] ?? undefined;
108
+ }
109
+
110
+ getTasks(): Task[] {
111
+ return this.store.filterWhere('a2a_tasks', () => true) as Task[];
112
+ }
113
+
114
+ createTask(task: Task): void {
115
+ this.store.insert('a2a_tasks', task, { autoId: false });
116
+ }
117
+
118
+ updateTask(id: string, updates: Partial<Task>): void {
119
+ const existing = this.get(id);
120
+ if (!existing) return;
121
+ const updated = { ...existing, ...updates, id: existing.id };
122
+ this.store.updateWhere('a2a_tasks', (t: Task) => t.id === id, updated);
123
+ }
124
+
125
+ updateTaskState(id: string, state: TaskState): void {
126
+ this.updateTask(id, { status: { state } } as Partial<Task>);
127
+ }
128
+
129
+ deleteTask(id: string): void {
130
+ this.delete(id);
131
+ }
132
+
133
+ appendHistory(id: string, entry: { state: TaskState; timestamp: string }): void {
134
+ const task = this.get(id);
135
+ if (!task) return;
136
+ const history = [...(task.history ?? []), entry as unknown as Message];
137
+ this.updateTask(id, { history } as Partial<Task>);
138
+ }
139
+
140
+ addArtifact(id: string, artifact: NonNullable<Task['artifacts']>[number]): void {
141
+ const task = this.get(id);
142
+ if (!task) return;
143
+ const artifacts = [...(task.artifacts ?? []), artifact];
144
+ this.updateTask(id, { artifacts } as Partial<Task>);
145
+ }
146
+
147
+ addMessage(id: string, message: Message): void {
148
+ const task = this.get(id);
149
+ if (!task) return;
150
+ const history = [...(task.history ?? []), message];
151
+ this.updateTask(id, { history } as Partial<Task>);
152
+ }
153
+
154
+ updateMessage(id: string, messageId: string, updates: Partial<Message>): void {
155
+ const task = this.get(id);
156
+ if (!task?.history) return;
157
+ const history = task.history.map((m) =>
158
+ m.messageId === messageId ? { ...m, ...updates } : m,
159
+ );
160
+ this.updateTask(id, { history } as Partial<Task>);
161
+ }
162
+
163
+ getMessage(id: string, messageId: string): Message | undefined {
164
+ const task = this.get(id);
165
+ return task?.history?.find((m) => m.messageId === messageId);
166
+ }
167
+
168
+ getMessages(id: string): Message[] {
169
+ const task = this.get(id);
170
+ return task?.history ?? [];
171
+ }
172
+
173
+ clearMessages(id: string): void {
174
+ this.updateTask(id, { history: [] } as Partial<Task>);
175
+ }
176
+ }
177
+
178
+ export const a2aTaskStore = new A2ATaskStore();