@creezio/tasks 0.22.0

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 (106) hide show
  1. package/README.md +279 -0
  2. package/dist/.creezio-src-hash.json +4 -0
  3. package/dist/ai-task-agent.d.ts +47 -0
  4. package/dist/ai-task-agent.d.ts.map +1 -0
  5. package/dist/ai-task-agent.js +558 -0
  6. package/dist/ai-task-agent.js.map +1 -0
  7. package/dist/ai-task-runner.d.ts +78 -0
  8. package/dist/ai-task-runner.d.ts.map +1 -0
  9. package/dist/ai-task-runner.js +645 -0
  10. package/dist/ai-task-runner.js.map +1 -0
  11. package/dist/api-mount.d.ts +4 -0
  12. package/dist/api-mount.d.ts.map +1 -0
  13. package/dist/api-mount.js +91 -0
  14. package/dist/api-mount.js.map +1 -0
  15. package/dist/assistant-adapter.d.ts +7 -0
  16. package/dist/assistant-adapter.d.ts.map +1 -0
  17. package/dist/assistant-adapter.js +96 -0
  18. package/dist/assistant-adapter.js.map +1 -0
  19. package/dist/brand/config.d.ts +165 -0
  20. package/dist/brand/config.d.ts.map +1 -0
  21. package/dist/brand/config.js +39 -0
  22. package/dist/brand/config.js.map +1 -0
  23. package/dist/env-bridge.d.ts +12 -0
  24. package/dist/env-bridge.d.ts.map +1 -0
  25. package/dist/env-bridge.js +33 -0
  26. package/dist/env-bridge.js.map +1 -0
  27. package/dist/hono-routes.d.ts +8 -0
  28. package/dist/hono-routes.d.ts.map +1 -0
  29. package/dist/hono-routes.js +563 -0
  30. package/dist/hono-routes.js.map +1 -0
  31. package/dist/index.d.ts +29 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +21 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/kanban-service.d.ts +136 -0
  36. package/dist/kanban-service.d.ts.map +1 -0
  37. package/dist/kanban-service.js +580 -0
  38. package/dist/kanban-service.js.map +1 -0
  39. package/dist/mcp-host-tools.d.ts +59 -0
  40. package/dist/mcp-host-tools.d.ts.map +1 -0
  41. package/dist/mcp-host-tools.js +326 -0
  42. package/dist/mcp-host-tools.js.map +1 -0
  43. package/dist/mcp-workspace-tools.d.ts +15 -0
  44. package/dist/mcp-workspace-tools.d.ts.map +1 -0
  45. package/dist/mcp-workspace-tools.js +338 -0
  46. package/dist/mcp-workspace-tools.js.map +1 -0
  47. package/dist/memory-store.d.ts +3 -0
  48. package/dist/memory-store.d.ts.map +1 -0
  49. package/dist/memory-store.js +75 -0
  50. package/dist/memory-store.js.map +1 -0
  51. package/dist/sqlite-driver.d.ts +13 -0
  52. package/dist/sqlite-driver.d.ts.map +1 -0
  53. package/dist/sqlite-driver.js +13 -0
  54. package/dist/sqlite-driver.js.map +1 -0
  55. package/dist/sqlite-store.d.ts +15 -0
  56. package/dist/sqlite-store.d.ts.map +1 -0
  57. package/dist/sqlite-store.js +113 -0
  58. package/dist/sqlite-store.js.map +1 -0
  59. package/dist/task-runs.d.ts +108 -0
  60. package/dist/task-runs.d.ts.map +1 -0
  61. package/dist/task-runs.js +427 -0
  62. package/dist/task-runs.js.map +1 -0
  63. package/dist/types.d.ts +35 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +15 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist-cjs/ai-task-agent.js +569 -0
  68. package/dist-cjs/ai-task-agent.js.map +1 -0
  69. package/dist-cjs/ai-task-runner.js +660 -0
  70. package/dist-cjs/ai-task-runner.js.map +1 -0
  71. package/dist-cjs/api-mount.js +94 -0
  72. package/dist-cjs/api-mount.js.map +1 -0
  73. package/dist-cjs/assistant-adapter.js +99 -0
  74. package/dist-cjs/assistant-adapter.js.map +1 -0
  75. package/dist-cjs/brand/config.js +47 -0
  76. package/dist-cjs/brand/config.js.map +1 -0
  77. package/dist-cjs/env-bridge.js +36 -0
  78. package/dist-cjs/env-bridge.js.map +1 -0
  79. package/dist-cjs/hono-routes.js +566 -0
  80. package/dist-cjs/hono-routes.js.map +1 -0
  81. package/dist-cjs/index.js +117 -0
  82. package/dist-cjs/index.js.map +1 -0
  83. package/dist-cjs/kanban-service.js +601 -0
  84. package/dist-cjs/kanban-service.js.map +1 -0
  85. package/dist-cjs/mcp-host-tools.js +333 -0
  86. package/dist-cjs/mcp-host-tools.js.map +1 -0
  87. package/dist-cjs/mcp-workspace-tools.js +342 -0
  88. package/dist-cjs/mcp-workspace-tools.js.map +1 -0
  89. package/dist-cjs/memory-store.js +81 -0
  90. package/dist-cjs/memory-store.js.map +1 -0
  91. package/dist-cjs/package.json +3 -0
  92. package/dist-cjs/sqlite-driver.js +16 -0
  93. package/dist-cjs/sqlite-driver.js.map +1 -0
  94. package/dist-cjs/sqlite-store.js +119 -0
  95. package/dist-cjs/sqlite-store.js.map +1 -0
  96. package/dist-cjs/task-runs.js +457 -0
  97. package/dist-cjs/task-runs.js.map +1 -0
  98. package/dist-cjs/types.js +18 -0
  99. package/dist-cjs/types.js.map +1 -0
  100. package/package.json +79 -0
  101. package/scripts/port-from-tempoflow.mjs +405 -0
  102. package/ui/ai-activity-panel.tsx +559 -0
  103. package/ui/index.ts +7 -0
  104. package/ui/task-detail-sheet.tsx +668 -0
  105. package/ui/tasks-kanban-client.tsx +647 -0
  106. package/ui/tasks-types.ts +120 -0
@@ -0,0 +1,660 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.aiRunnerEnabled = aiRunnerEnabled;
4
+ exports.hostBridgeReady = hostBridgeReady;
5
+ exports.resolveHostTargetUserId = resolveHostTargetUserId;
6
+ exports.checkAiRunQuotas = checkAiRunQuotas;
7
+ exports.enqueueAiRunForTask = enqueueAiRunForTask;
8
+ exports.processAiTaskQueue = processAiTaskQueue;
9
+ exports.recoverInterruptedRuns = recoverInterruptedRuns;
10
+ exports.parseRecurringSchedule = parseRecurringSchedule;
11
+ exports.processRecurringAiTasks = processRecurringAiTasks;
12
+ exports.stopAiRunnerLoop = stopAiRunnerLoop;
13
+ exports.ensureAiRunnerLoop = ensureAiRunnerLoop;
14
+ exports.getAiActivityForUser = getAiActivityForUser;
15
+ exports.retryFailedRun = retryFailedRun;
16
+ /**
17
+ * Runner host — exécute les tâches assignées à un collaborateur IA via la
18
+ * boucle LLM (ai-task-agent). Persona ACL = user AI ; surface = workspace
19
+ * Electron dédié (pas l'espace owner). Desktop bridge = host owner.
20
+ *
21
+ * Honnêteté des statuts (D4) : un run sans bridge desktop, sans LLM ou sans
22
+ * `finish_task(success=true)` est `failed` — jamais de succès de complaisance.
23
+ */
24
+ const ai_task_agent_js_1 = require("./ai-task-agent.js");
25
+ const config_js_1 = require("./brand/config.js");
26
+ const kanban_service_js_1 = require("./kanban-service.js");
27
+ const task_runs_js_1 = require("./task-runs.js");
28
+ function ensureAiWorkspaceOnHost(opts) {
29
+ return (0, config_js_1.requireTasksBrand)().workspace.ensureOnHost(opts);
30
+ }
31
+ function isDesktopOnline(userId) {
32
+ return (0, config_js_1.requireTasksBrand)().presence.isDesktopOnline(userId);
33
+ }
34
+ function listOnlineBridges() {
35
+ return (0, config_js_1.requireTasksBrand)().presence.listOnlineBridges();
36
+ }
37
+ function getOwner() {
38
+ return (0, config_js_1.requireTasksBrand)().users.getOwner();
39
+ }
40
+ function getUserById(id) {
41
+ return (0, config_js_1.requireTasksBrand)().users.getById(id);
42
+ }
43
+ const g = globalThis;
44
+ function inflight() {
45
+ if (!g.__creezioTasksAiRunnerInflight)
46
+ g.__creezioTasksAiRunnerInflight = new Set();
47
+ return g.__creezioTasksAiRunnerInflight;
48
+ }
49
+ /** Kill switch runner (`${envPrefix}_RUNNER_ENABLED=0` pour désactiver). */
50
+ function aiRunnerEnabled() {
51
+ const v = ((0, config_js_1.tasksEnv)("RUNNER_ENABLED", "1") || "1").toLowerCase();
52
+ return v !== "0" && v !== "false" && v !== "no";
53
+ }
54
+ function hostBridgeReady() {
55
+ const bridges = listOnlineBridges().filter((b) => b.online && b.bridgeConnected);
56
+ if (bridges.length > 0)
57
+ return true;
58
+ const owner = getOwner();
59
+ if (owner && isDesktopOnline(owner.id))
60
+ return true;
61
+ return false;
62
+ }
63
+ function resolveHostTargetUserId() {
64
+ const owner = getOwner();
65
+ if (owner && isDesktopOnline(owner.id))
66
+ return owner.id;
67
+ const bridges = listOnlineBridges().filter((b) => b.online && b.bridgeConnected);
68
+ return bridges[0]?.userId || owner?.id || null;
69
+ }
70
+ /** Quota env optionnel (null = illimité). */
71
+ function envQuota(suffix) {
72
+ const raw = Number((0, config_js_1.tasksEnv)(suffix));
73
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : null;
74
+ }
75
+ /**
76
+ * Quotas journaliers (minuit UTC) : ${requireTasksBrand().envPrefix}_MAX_RUNS_PER_DAY (nombre de runs
77
+ * créés) et ${requireTasksBrand().envPrefix}_MAX_TOKENS_PER_DAY (tokens LLM consommés). Non configurés
78
+ * = illimité.
79
+ */
80
+ function checkAiRunQuotas() {
81
+ const maxRuns = envQuota("MAX_RUNS_PER_DAY");
82
+ if (maxRuns !== null) {
83
+ const used = (0, task_runs_js_1.countRunsCreatedToday)();
84
+ if (used >= maxRuns) {
85
+ return {
86
+ ok: false,
87
+ reason: `quota journalier de runs atteint (${used}/${maxRuns} — ${(0, config_js_1.requireTasksBrand)().envPrefix}_MAX_RUNS_PER_DAY)`,
88
+ };
89
+ }
90
+ }
91
+ const maxTokens = envQuota("MAX_TOKENS_PER_DAY");
92
+ if (maxTokens !== null) {
93
+ const used = (0, task_runs_js_1.sumUsageTokensToday)();
94
+ if (used >= maxTokens) {
95
+ return {
96
+ ok: false,
97
+ reason: `quota journalier de tokens atteint (${used}/${maxTokens} — ${(0, config_js_1.requireTasksBrand)().envPrefix}_MAX_TOKENS_PER_DAY)`,
98
+ };
99
+ }
100
+ }
101
+ return { ok: true };
102
+ }
103
+ /**
104
+ * Enfile un run si la tâche (executor ai) est assignée à une IA active.
105
+ * Retourne le run (existant ou nouveau) ou null si non applicable.
106
+ * Lève `quota_exceeded: …` si un quota journalier est atteint.
107
+ */
108
+ function enqueueAiRunForTask(taskId, opts) {
109
+ const task = (0, kanban_service_js_1.getTask)(taskId);
110
+ if (!task?.assignee_user_id)
111
+ return null;
112
+ if (task.executor_kind !== "ai")
113
+ return null;
114
+ const assignee = getUserById(task.assignee_user_id);
115
+ if (!assignee || assignee.kind !== "ai" || !assignee.active)
116
+ return null;
117
+ const quota = checkAiRunQuotas();
118
+ if (!quota.ok) {
119
+ throw new Error(`quota_exceeded: ${quota.reason}`);
120
+ }
121
+ return (0, task_runs_js_1.enqueueTaskRun)({
122
+ taskId: task.id,
123
+ assigneeUserId: assignee.id,
124
+ retryOf: opts?.retryOf ?? null,
125
+ });
126
+ }
127
+ async function sleep(ms) {
128
+ await new Promise((r) => setTimeout(r, ms));
129
+ }
130
+ function hitlEnabled(task) {
131
+ const env = (0, config_js_1.tasksEnv)("HITL").toLowerCase();
132
+ if (env === "1" || env === "true")
133
+ return true;
134
+ const body = task.body || "";
135
+ return /\[\[confirm\]\]/i.test(body) || /\[\[hitl\]\]/i.test(body);
136
+ }
137
+ /** Pause HITL : prompt visible côté UI, reprise via /runs/:id/resume. */
138
+ async function waitForHitl(runId, prompt) {
139
+ (0, task_runs_js_1.setHitlPrompt)(runId, prompt);
140
+ (0, task_runs_js_1.appendAgentLog)({
141
+ runId,
142
+ level: "decision",
143
+ eventType: "await_human",
144
+ message: prompt,
145
+ payload: { hitl: true },
146
+ });
147
+ const deadline = Date.now() + 30 * 60 * 1000; // 30 min
148
+ while (Date.now() < deadline) {
149
+ await sleep(500);
150
+ const fresh = (0, task_runs_js_1.getTaskRun)(runId);
151
+ if (!fresh || fresh.status !== "running")
152
+ return { outcome: "cancelled" };
153
+ if (!fresh.hitl_prompt && fresh.hitl_response) {
154
+ const response = fresh.hitl_response;
155
+ (0, task_runs_js_1.appendAgentLog)({
156
+ runId,
157
+ level: "info",
158
+ eventType: "hitl_resume",
159
+ message: `Reprise humaine : ${response}`,
160
+ });
161
+ (0, task_runs_js_1.clearHitlResponse)(runId);
162
+ return { outcome: "resumed", response };
163
+ }
164
+ if (!fresh.hitl_prompt) {
165
+ // resume cleared prompt without response edge-case
166
+ return { outcome: "resumed" };
167
+ }
168
+ }
169
+ (0, task_runs_js_1.appendAgentLog)({
170
+ runId,
171
+ level: "error",
172
+ eventType: "hitl_timeout",
173
+ message: "Timeout human-in-the-loop (30 min)",
174
+ });
175
+ return { outcome: "timeout" };
176
+ }
177
+ async function runSteps(run, task) {
178
+ const assignee = getUserById(run.assignee_user_id);
179
+ const hostUserId = resolveHostTargetUserId();
180
+ const hostDevice = listOnlineBridges().find((b) => b.userId === hostUserId)?.deviceId || null;
181
+ const perms = assignee?.permissions || [];
182
+ (0, task_runs_js_1.appendAgentLog)({
183
+ runId: run.id,
184
+ level: "info",
185
+ eventType: "run_start",
186
+ message: `Démarrage du run pour « ${task.title} »`,
187
+ payload: {
188
+ task_id: task.id,
189
+ assignee: assignee?.username || run.assignee_user_id,
190
+ assignee_kind: assignee?.kind || "ai",
191
+ permissions: perms,
192
+ host_user_id: hostUserId,
193
+ host_online: hostBridgeReady(),
194
+ max_concurrent: (0, task_runs_js_1.maxConcurrentAiRuns)(),
195
+ retry_of: run.retry_of,
196
+ },
197
+ });
198
+ (0, task_runs_js_1.appendAgentLog)({
199
+ runId: run.id,
200
+ level: "decision",
201
+ eventType: "acl_check",
202
+ message: "ACL collaborateur IA appliquées (pas de bypass owner)",
203
+ payload: {
204
+ role: assignee?.role,
205
+ permissions: perms,
206
+ },
207
+ });
208
+ // D4 : pas de bridge desktop = échec explicite, jamais un faux succès.
209
+ if (!hostBridgeReady() || !hostUserId || !assignee) {
210
+ (0, task_runs_js_1.appendAgentLog)({
211
+ runId: run.id,
212
+ level: "error",
213
+ eventType: "desktop_offline",
214
+ message: "Bridge desktop hors ligne — l'IA ne peut pas travailler sans son workspace. Run en échec (relancer quand l'app desktop est ouverte).",
215
+ });
216
+ (0, task_runs_js_1.finishTaskRun)(run.id, {
217
+ status: "failed",
218
+ lastError: "desktop_offline",
219
+ hostDeviceId: hostDevice,
220
+ });
221
+ return;
222
+ }
223
+ // D4 : pas de LLM (BYOK OpenAI absent) = échec explicite.
224
+ if (!(0, ai_task_agent_js_1.hasAiAgentModel)()) {
225
+ (0, task_runs_js_1.appendAgentLog)({
226
+ runId: run.id,
227
+ level: "error",
228
+ eventType: "llm_missing",
229
+ message: "Aucune clé OpenAI (BYOK) — configurer Configuration → Clés IA avant de lancer un collaborateur IA.",
230
+ });
231
+ (0, task_runs_js_1.finishTaskRun)(run.id, {
232
+ status: "failed",
233
+ lastError: "llm_missing",
234
+ hostDeviceId: hostDevice,
235
+ });
236
+ return;
237
+ }
238
+ if (task.body?.trim()) {
239
+ (0, task_runs_js_1.appendAgentLog)({
240
+ runId: run.id,
241
+ level: "info",
242
+ eventType: "brief",
243
+ message: task.body.trim().slice(0, 500),
244
+ });
245
+ }
246
+ // Workspace Electron dédié de CETTE IA (partition + cookie persona).
247
+ (0, task_runs_js_1.appendAgentLog)({
248
+ runId: run.id,
249
+ level: "tool",
250
+ eventType: "tool_call",
251
+ message: `ai_workspace_ensure → ${assignee.username}`,
252
+ payload: {
253
+ tool: "ai_workspace_ensure",
254
+ ai_user_id: assignee.id,
255
+ target_user_id: hostUserId,
256
+ },
257
+ });
258
+ try {
259
+ const ensured = await ensureAiWorkspaceOnHost({
260
+ aiUserId: assignee.id,
261
+ hostUserId,
262
+ show: true,
263
+ label: assignee.username,
264
+ });
265
+ (0, task_runs_js_1.appendAgentLog)({
266
+ runId: run.id,
267
+ level: ensured.ok === false ? "warn" : "tool",
268
+ eventType: "tool_result",
269
+ message: ensured.ok === false
270
+ ? String(ensured.error || "Échec ensure workspace IA")
271
+ : "Espace workspace IA prêt (vue Electron)",
272
+ payload: {
273
+ ok: ensured.ok !== false,
274
+ code: ensured.code || null,
275
+ workspace: ensured.workspace || null,
276
+ },
277
+ });
278
+ if (ensured.ok === false) {
279
+ (0, task_runs_js_1.finishTaskRun)(run.id, {
280
+ status: "failed",
281
+ lastError: String(ensured.error || "workspace_ensure_failed"),
282
+ hostDeviceId: hostDevice,
283
+ });
284
+ return;
285
+ }
286
+ }
287
+ catch (e) {
288
+ const msg = e instanceof Error ? e.message : String(e);
289
+ (0, task_runs_js_1.appendAgentLog)({
290
+ runId: run.id,
291
+ level: "error",
292
+ eventType: "tool_result",
293
+ message: msg,
294
+ });
295
+ (0, task_runs_js_1.finishTaskRun)(run.id, {
296
+ status: "failed",
297
+ lastError: msg,
298
+ hostDeviceId: hostDevice,
299
+ });
300
+ return;
301
+ }
302
+ // HITL pré-run ([[confirm]]/[[hitl]] dans le brief, ou HITL (envPrefix)=1).
303
+ if (hitlEnabled(task)) {
304
+ const gate = await waitForHitl(run.id, "Validation humaine requise avant de démarrer le run (HITL).");
305
+ if (gate.outcome !== "resumed") {
306
+ (0, task_runs_js_1.finishTaskRun)(run.id, {
307
+ status: gate.outcome === "timeout" ? "failed" : "cancelled",
308
+ lastError: gate.outcome === "timeout" ? "hitl_timeout" : "hitl_cancelled",
309
+ hostDeviceId: hostDevice,
310
+ });
311
+ return;
312
+ }
313
+ }
314
+ (0, task_runs_js_1.appendAgentLog)({
315
+ runId: run.id,
316
+ level: "decision",
317
+ eventType: "plan",
318
+ message: "Boucle LLM démarrée (navigate / UI / delegate_to_hermes / finish_task)",
319
+ });
320
+ // Boucle LLM — chaque étape journalisée pour l'AiActivityPanel.
321
+ const outcome = await (0, ai_task_agent_js_1.runAiTaskAgent)({
322
+ task,
323
+ assignee,
324
+ hostUserId,
325
+ runId: run.id,
326
+ askHuman: (question) => waitForHitl(run.id, question),
327
+ onStep: (ev) => {
328
+ if (ev.kind === "assistant") {
329
+ (0, task_runs_js_1.appendAgentLog)({
330
+ runId: run.id,
331
+ level: "decision",
332
+ eventType: "assistant_message",
333
+ message: ev.content.slice(0, 1000),
334
+ });
335
+ }
336
+ else if (ev.kind === "tool_call") {
337
+ (0, task_runs_js_1.appendAgentLog)({
338
+ runId: run.id,
339
+ level: "tool",
340
+ eventType: "tool_call",
341
+ message: ev.name,
342
+ payload: { tool: ev.name, args: ev.args },
343
+ });
344
+ }
345
+ else {
346
+ (0, task_runs_js_1.appendAgentLog)({
347
+ runId: run.id,
348
+ level: ev.result.ok === false ? "warn" : "tool",
349
+ eventType: "tool_result",
350
+ message: ev.result.ok === false
351
+ ? String(ev.result.error || `${ev.name} en échec`)
352
+ : `${ev.name} ok`,
353
+ payload: { tool: ev.name, result: ev.result },
354
+ });
355
+ }
356
+ },
357
+ });
358
+ // Le run a pu être annulé pendant la boucle (HITL cancel, UI).
359
+ const still = (0, task_runs_js_1.getTaskRun)(run.id);
360
+ if (!still || still.status !== "running")
361
+ return;
362
+ if (outcome.success) {
363
+ (0, task_runs_js_1.appendAgentLog)({
364
+ runId: run.id,
365
+ level: "info",
366
+ eventType: "run_end",
367
+ message: outcome.summary || "Run terminé avec succès",
368
+ payload: { steps: outcome.loop.steps, tokens: outcome.loop.usageTokens },
369
+ });
370
+ (0, task_runs_js_1.finishTaskRun)(run.id, {
371
+ status: "succeeded",
372
+ hostDeviceId: hostDevice,
373
+ usageTokens: outcome.loop.usageTokens,
374
+ });
375
+ const fresh = (0, kanban_service_js_1.getTask)(task.id);
376
+ if (fresh && fresh.status === "in_progress") {
377
+ (0, kanban_service_js_1.updateTaskLocal)(task.id, { status: "done", result: outcome.summary });
378
+ }
379
+ return;
380
+ }
381
+ (0, task_runs_js_1.appendAgentLog)({
382
+ runId: run.id,
383
+ level: "error",
384
+ eventType: "run_end",
385
+ message: outcome.summary || `Run en échec (${outcome.loop.status})`,
386
+ payload: {
387
+ steps: outcome.loop.steps,
388
+ tokens: outcome.loop.usageTokens,
389
+ loop_status: outcome.loop.status,
390
+ },
391
+ });
392
+ (0, task_runs_js_1.finishTaskRun)(run.id, {
393
+ status: "failed",
394
+ lastError: outcome.summary.slice(0, 500) || outcome.loop.status,
395
+ hostDeviceId: hostDevice,
396
+ usageTokens: outcome.loop.usageTokens,
397
+ });
398
+ (0, kanban_service_js_1.updateTaskLocal)(task.id, { result: outcome.summary });
399
+ }
400
+ async function executeClaimedRun(run) {
401
+ const active = inflight();
402
+ if (active.has(run.id)) {
403
+ return { processed: false, runId: run.id, error: "inflight" };
404
+ }
405
+ active.add(run.id);
406
+ try {
407
+ const task = (0, kanban_service_js_1.getTask)(run.task_id);
408
+ if (!task) {
409
+ (0, task_runs_js_1.finishTaskRun)(run.id, {
410
+ status: "failed",
411
+ lastError: "Tâche introuvable",
412
+ });
413
+ return { processed: true, runId: run.id, error: "task_missing" };
414
+ }
415
+ const assignee = getUserById(run.assignee_user_id);
416
+ if (!assignee || assignee.kind !== "ai") {
417
+ (0, task_runs_js_1.finishTaskRun)(run.id, {
418
+ status: "failed",
419
+ lastError: "Assigné non-IA",
420
+ });
421
+ return { processed: true, runId: run.id, error: "not_ai" };
422
+ }
423
+ try {
424
+ await runSteps(run, task);
425
+ return { processed: true, runId: run.id };
426
+ }
427
+ catch (e) {
428
+ const msg = e instanceof Error ? e.message : String(e);
429
+ (0, task_runs_js_1.appendAgentLog)({
430
+ runId: run.id,
431
+ level: "error",
432
+ eventType: "run_end",
433
+ message: msg,
434
+ });
435
+ (0, task_runs_js_1.finishTaskRun)(run.id, { status: "failed", lastError: msg });
436
+ return { processed: true, runId: run.id, error: msg };
437
+ }
438
+ }
439
+ finally {
440
+ active.delete(run.id);
441
+ }
442
+ }
443
+ /**
444
+ * Claim et lance jusqu'à `maxConcurrent` runs en parallèle.
445
+ * Les runs déjà en HITL restent « running » mais ne sont pas re-claimés.
446
+ */
447
+ async function processAiTaskQueue() {
448
+ if (!aiRunnerEnabled()) {
449
+ return { processed: false, error: "runner_disabled" };
450
+ }
451
+ const max = (0, task_runs_js_1.maxConcurrentAiRuns)();
452
+ const claimed = [];
453
+ for (let i = 0; i < max; i++) {
454
+ const run = (0, task_runs_js_1.claimNextQueuedRun)(max);
455
+ if (!run)
456
+ break;
457
+ if ((0, task_runs_js_1.isHitlPaused)(run))
458
+ continue;
459
+ if (inflight().has(run.id))
460
+ continue;
461
+ claimed.push(run);
462
+ }
463
+ if (claimed.length === 0) {
464
+ return { processed: false, maxConcurrent: max };
465
+ }
466
+ const results = await Promise.all(claimed.map((run) => executeClaimedRun(run)));
467
+ const runIds = results.map((r) => r.runId);
468
+ const err = results.find((r) => r.error)?.error;
469
+ return {
470
+ processed: true,
471
+ runIds,
472
+ runId: runIds[0],
473
+ error: err,
474
+ maxConcurrent: max,
475
+ };
476
+ }
477
+ /**
478
+ * Récupération après redémarrage : les runs `running` qui n'appartiennent à
479
+ * aucune boucle de CE process sont orphelins (app/serveur redémarré pendant
480
+ * l'exécution). D4 : ils passent `failed` (motif `app_restart`) — et si la
481
+ * tâche est toujours `in_progress`, un nouveau run est ré-enfilé.
482
+ */
483
+ function recoverInterruptedRuns() {
484
+ let recovered = 0;
485
+ let requeued = 0;
486
+ for (const run of (0, task_runs_js_1.listRunningRuns)()) {
487
+ if (inflight().has(run.id))
488
+ continue; // vraiment en cours ici
489
+ (0, task_runs_js_1.appendAgentLog)({
490
+ runId: run.id,
491
+ level: "error",
492
+ eventType: "run_end",
493
+ message: "Run interrompu par un redémarrage de l'application — marqué failed (app_restart).",
494
+ payload: { recovered: true },
495
+ });
496
+ (0, task_runs_js_1.finishTaskRun)(run.id, { status: "failed", lastError: "app_restart" });
497
+ recovered += 1;
498
+ const task = (0, kanban_service_js_1.getTask)(run.task_id);
499
+ if (task && task.executor_kind === "ai" && task.status === "in_progress") {
500
+ try {
501
+ if (enqueueAiRunForTask(task.id, { retryOf: run.id }))
502
+ requeued += 1;
503
+ }
504
+ catch {
505
+ /* quota atteint — la tâche reste in_progress, relançable à la main */
506
+ }
507
+ }
508
+ }
509
+ return { recovered, requeued };
510
+ }
511
+ /* ── Récurrence (tâches IA) ────────────────────────────────────────────── */
512
+ /**
513
+ * Parseur minimal de `recurring_schedule` pour les tâches IA :
514
+ * - `every 15m` / `every 2h` / `every 1d` (aussi `min`, `j`) ;
515
+ * - `daily@HH:MM` (heure locale serveur).
516
+ * Retourne une fonction « prochaine occurrence après `from` », ou null si la
517
+ * syntaxe n'est pas reconnue (les crons Hermes restent gérés par Hermes).
518
+ */
519
+ function parseRecurringSchedule(raw) {
520
+ const s = String(raw || "").trim().toLowerCase();
521
+ if (!s)
522
+ return null;
523
+ const every = s.match(/^every\s+(\d+)\s*(m|min|mins|minutes?|h|heures?|hours?|d|j|jours?|days?)$/);
524
+ if (every) {
525
+ const n = Number(every[1]);
526
+ if (!Number.isFinite(n) || n <= 0)
527
+ return null;
528
+ const unit = (every[2] || "d")[0]; // m / h / d / j
529
+ const ms = unit === "m" ? n * 60_000 : unit === "h" ? n * 3_600_000 : n * 86_400_000;
530
+ if (ms < 60_000)
531
+ return null; // plancher 1 min
532
+ return (from) => new Date(from.getTime() + ms);
533
+ }
534
+ const daily = s.match(/^daily\s*@\s*(\d{1,2}):(\d{2})$/);
535
+ if (daily) {
536
+ const hh = Number(daily[1]);
537
+ const mm = Number(daily[2]);
538
+ if (hh > 23 || mm > 59)
539
+ return null;
540
+ return (from) => {
541
+ const next = new Date(from);
542
+ next.setHours(hh, mm, 0, 0);
543
+ if (next.getTime() <= from.getTime()) {
544
+ next.setDate(next.getDate() + 1);
545
+ }
546
+ return next;
547
+ };
548
+ }
549
+ return null;
550
+ }
551
+ /**
552
+ * Tick récurrence (1/min) : (ré)enfile les tâches IA récurrentes échues.
553
+ * Première passe : initialise next_run_at sans lancer (la création a déjà
554
+ * lancé un premier run le cas échéant).
555
+ */
556
+ function processRecurringAiTasks(now = new Date()) {
557
+ if (!aiRunnerEnabled())
558
+ return { launched: [], scheduled: 0 };
559
+ const launched = [];
560
+ let scheduled = 0;
561
+ for (const task of (0, kanban_service_js_1.listRecurringAiTasks)()) {
562
+ const next = parseRecurringSchedule(task.recurring_schedule);
563
+ if (!next)
564
+ continue; // syntaxe non gérée (cron Hermes, etc.)
565
+ if (!task.next_run_at) {
566
+ (0, kanban_service_js_1.setTaskNextRun)(task.id, next(now).toISOString());
567
+ scheduled += 1;
568
+ continue;
569
+ }
570
+ if (new Date(task.next_run_at).getTime() > now.getTime())
571
+ continue;
572
+ // Occurrence échue : replanifier d'abord (pas de rafale si erreur).
573
+ (0, kanban_service_js_1.setTaskNextRun)(task.id, next(now).toISOString());
574
+ scheduled += 1;
575
+ try {
576
+ if (task.status !== "in_progress") {
577
+ (0, kanban_service_js_1.updateTaskLocal)(task.id, { status: "in_progress" });
578
+ }
579
+ const run = enqueueAiRunForTask(task.id);
580
+ if (run)
581
+ launched.push(run.id);
582
+ }
583
+ catch {
584
+ /* quota atteint — occurrence sautée, la suivante retentera */
585
+ }
586
+ }
587
+ if (launched.length > 0)
588
+ void processAiTaskQueue();
589
+ return { launched, scheduled };
590
+ }
591
+ function unrefTimer(timer) {
592
+ if (timer && typeof timer === "object" && "unref" in timer) {
593
+ try {
594
+ timer.unref();
595
+ }
596
+ catch {
597
+ /* ignore */
598
+ }
599
+ }
600
+ }
601
+ /**
602
+ * Arrête les boucles runner + récurrence (teardown). Sans cet arrêt, le
603
+ * setInterval process-global continue de tick après la fermeture de la
604
+ * surface plateforme : le tick suivant jette `requireTasksBrand()` en
605
+ * unhandledRejection (config brand désormais absente) — vécu gate PNM.2.
606
+ * Un prochain appel à `ensureAiRunnerLoop` (requête tasks) relance tout.
607
+ */
608
+ function stopAiRunnerLoop() {
609
+ if (g.__creezioTasksAiRunnerTimer) {
610
+ clearInterval(g.__creezioTasksAiRunnerTimer);
611
+ g.__creezioTasksAiRunnerTimer = null;
612
+ }
613
+ if (g.__creezioTasksAiRecurrenceTimer) {
614
+ clearInterval(g.__creezioTasksAiRecurrenceTimer);
615
+ g.__creezioTasksAiRecurrenceTimer = null;
616
+ }
617
+ }
618
+ /** Démarre un poll léger du runner (idempotent, process-local). */
619
+ function ensureAiRunnerLoop(intervalMs = 2000) {
620
+ if (!aiRunnerEnabled())
621
+ return;
622
+ if (!g.__creezioTasksAiRecurrenceTimer) {
623
+ g.__creezioTasksAiRecurrenceTimer = setInterval(() => {
624
+ try {
625
+ processRecurringAiTasks();
626
+ }
627
+ catch {
628
+ /* jamais bloquant */
629
+ }
630
+ }, 60_000);
631
+ unrefTimer(g.__creezioTasksAiRecurrenceTimer);
632
+ }
633
+ if (g.__creezioTasksAiRunnerTimer)
634
+ return;
635
+ g.__creezioTasksAiRunnerTimer = setInterval(() => {
636
+ void processAiTaskQueue();
637
+ }, intervalMs);
638
+ unrefTimer(g.__creezioTasksAiRunnerTimer);
639
+ }
640
+ function getAiActivityForUser(userId) {
641
+ const active = (0, task_runs_js_1.getActiveRunForAssignee)(userId);
642
+ if (!active)
643
+ return { run: null, task: null };
644
+ return { run: active, task: (0, kanban_service_js_1.getTask)(active.task_id) };
645
+ }
646
+ function retryFailedRun(runId) {
647
+ const prev = (0, task_runs_js_1.getTaskRun)(runId);
648
+ if (!prev)
649
+ return null;
650
+ if (prev.status !== "failed" && prev.status !== "cancelled")
651
+ return null;
652
+ const task = (0, kanban_service_js_1.getTask)(prev.task_id);
653
+ if (!task)
654
+ return null;
655
+ if (task.status === "done" || task.status === "cancelled") {
656
+ (0, kanban_service_js_1.updateTaskLocal)(task.id, { status: "in_progress" });
657
+ }
658
+ return enqueueAiRunForTask(task.id, { retryOf: prev.id });
659
+ }
660
+ //# sourceMappingURL=ai-task-runner.js.map