@desplega.ai/agent-swarm 1.74.4 → 1.76.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 (88) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +1264 -46
  3. package/package.json +2 -2
  4. package/src/be/db.ts +563 -9
  5. package/src/be/memory/edges-store.ts +69 -0
  6. package/src/be/memory/providers/sqlite-store.ts +4 -0
  7. package/src/be/memory/raters/explicit-self.ts +22 -0
  8. package/src/be/memory/raters/implicit-citation.ts +44 -0
  9. package/src/be/memory/raters/llm-client.ts +172 -0
  10. package/src/be/memory/raters/llm-summarizer.ts +218 -0
  11. package/src/be/memory/raters/llm.ts +375 -0
  12. package/src/be/memory/raters/noop.ts +14 -0
  13. package/src/be/memory/raters/registry.ts +86 -0
  14. package/src/be/memory/raters/retrieval.ts +88 -0
  15. package/src/be/memory/raters/run-server-raters.ts +97 -0
  16. package/src/be/memory/raters/store.ts +228 -0
  17. package/src/be/memory/raters/types.ts +101 -0
  18. package/src/be/memory/reranker.ts +32 -2
  19. package/src/be/memory/retrieval-store.ts +116 -0
  20. package/src/be/memory/types.ts +3 -0
  21. package/src/be/migrations/051_memory_posteriors_and_retrieval.sql +67 -0
  22. package/src/be/migrations/052_memory_edges.sql +36 -0
  23. package/src/be/migrations/053_agent_waiting_for_credentials_status.sql +61 -0
  24. package/src/be/migrations/054_agent_harness_provider.sql +21 -0
  25. package/src/be/migrations/055_agent_cred_status.sql +15 -0
  26. package/src/be/migrations/056_drop_agent_tasks_source_check.sql +139 -0
  27. package/src/be/migrations/057_inbox_item_state.sql +27 -0
  28. package/src/be/migrations/058_task_templates.sql +31 -0
  29. package/src/be/swarm-config-guard.ts +24 -0
  30. package/src/commands/credential-wait.ts +186 -0
  31. package/src/commands/provider-credentials.ts +434 -0
  32. package/src/commands/runner.ts +253 -21
  33. package/src/hooks/hook.ts +143 -66
  34. package/src/http/agents.ts +191 -1
  35. package/src/http/config.ts +11 -1
  36. package/src/http/core.ts +5 -0
  37. package/src/http/inbox-state.ts +89 -0
  38. package/src/http/index.ts +10 -0
  39. package/src/http/memory.ts +230 -1
  40. package/src/http/sessions.ts +86 -0
  41. package/src/http/status.ts +665 -0
  42. package/src/http/task-templates.ts +51 -0
  43. package/src/http/tasks.ts +85 -5
  44. package/src/http/users.ts +134 -0
  45. package/src/prompts/memories.ts +62 -0
  46. package/src/providers/claude-adapter.ts +22 -0
  47. package/src/providers/claude-managed-adapter.ts +24 -0
  48. package/src/providers/codex-adapter.ts +43 -1
  49. package/src/providers/devin-adapter.ts +18 -0
  50. package/src/providers/index.ts +7 -0
  51. package/src/providers/opencode-adapter.ts +60 -0
  52. package/src/providers/pi-mono-adapter.ts +71 -0
  53. package/src/providers/types.ts +34 -0
  54. package/src/server.ts +2 -0
  55. package/src/slack/handlers.ts +0 -1
  56. package/src/tests/agents-harness-provider.test.ts +333 -0
  57. package/src/tests/credential-check.test.ts +367 -0
  58. package/src/tests/credential-status-api.test.ts +223 -0
  59. package/src/tests/credential-status-routing.test.ts +150 -0
  60. package/src/tests/credential-wait.test.ts +282 -0
  61. package/src/tests/harness-provider-resolution.test.ts +242 -0
  62. package/src/tests/jira-sync.test.ts +1 -1
  63. package/src/tests/memory-edges.test.ts +722 -0
  64. package/src/tests/memory-rate-endpoint.test.ts +330 -0
  65. package/src/tests/memory-rate-tool.test.ts +252 -0
  66. package/src/tests/memory-rater-e2e.test.ts +578 -0
  67. package/src/tests/memory-rater-implicit-citation.test.ts +304 -0
  68. package/src/tests/memory-rater-llm-summarizer.test.ts +317 -0
  69. package/src/tests/memory-rater-llm.test.ts +964 -0
  70. package/src/tests/memory-rater-store.test.ts +249 -0
  71. package/src/tests/memory-reranker.test.ts +161 -2
  72. package/src/tests/migration-runner-regressions.test.ts +17 -2
  73. package/src/tests/mocks/mock-llm-rater-client.ts +35 -0
  74. package/src/tests/run-server-raters.test.ts +291 -0
  75. package/src/tests/sessions.test.ts +141 -0
  76. package/src/tests/status.test.ts +843 -0
  77. package/src/tests/stop-hook-task-resolution.test.ts +98 -0
  78. package/src/tests/template-recommendations.test.ts +148 -0
  79. package/src/tests/tool-annotations.test.ts +2 -2
  80. package/src/tests/use-dismissible-card.test.ts +140 -0
  81. package/src/tools/memory-rate.ts +166 -0
  82. package/src/tools/memory-search.ts +18 -0
  83. package/src/tools/store-progress.ts +37 -0
  84. package/src/tools/swarm-config/set-config.ts +17 -1
  85. package/src/tools/tool-config.ts +1 -0
  86. package/src/types.ts +122 -1
  87. package/src/utils/harness-provider.ts +32 -0
  88. package/tsconfig.json +0 -2
@@ -9,6 +9,7 @@ import {
9
9
  generateDefaultSoulMd,
10
10
  generateDefaultToolsMd,
11
11
  } from "../prompts/defaults.ts";
12
+ import { renderMemoriesPrompt } from "../prompts/memories.ts";
12
13
  import { configureHttpResolver, resolveTemplateAsync } from "../prompts/resolver.ts";
13
14
  import { authJsonToCredentialSelection } from "../providers/codex-oauth/auth-json.js";
14
15
  import {
@@ -24,10 +25,17 @@ import { computeBudgetBackoffMs } from "../utils/budget-backoff.ts";
24
25
  import { getContextWindowSize } from "../utils/context-window.ts";
25
26
  import { type CredentialSelection, resolveCredentialPools } from "../utils/credentials.ts";
26
27
  import { parseRateLimitResetTime } from "../utils/error-tracker.ts";
28
+ import { resolveHarnessProvider } from "../utils/harness-provider.ts";
27
29
  import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
28
30
  import { scrubSecrets } from "../utils/secret-scrubber.ts";
29
31
  import { detectVcsProvider } from "../vcs/index.ts";
30
32
  import { interpolate } from "../workflows/template.ts";
33
+ import { awaitCredentials, BootMaxWaitExceededError, EX_CONFIG } from "./credential-wait.ts";
34
+ import {
35
+ buildCredStatusReport,
36
+ isCredCheckDisabled,
37
+ reportCredStatus,
38
+ } from "./provider-credentials.ts";
31
39
  // Side-effect import: registers runner trigger/resumption templates
32
40
  import "./templates.ts";
33
41
 
@@ -202,6 +210,13 @@ async function closeAgent(config: ApiConfig, role: string): Promise<void> {
202
210
  interface ResolvedEnvResult {
203
211
  env: Record<string, string | undefined>;
204
212
  credentialSelections: CredentialSelection[];
213
+ /**
214
+ * Effective `HARNESS_PROVIDER` after layering swarm_config over the base
215
+ * env. Callers should prefer this over `process.env.HARNESS_PROVIDER` so
216
+ * that an operator's swarm_config row (repo > agent > global) actually
217
+ * takes effect on the worker.
218
+ */
219
+ resolvedProvider: ProviderName;
205
220
  }
206
221
 
207
222
  async function fetchResolvedEnv(
@@ -239,6 +254,8 @@ async function fetchResolvedEnv(
239
254
  }
240
255
  }
241
256
 
257
+ const resolvedProvider = resolveHarnessProvider(env, baseEnv);
258
+
242
259
  const credentialSelections = await resolveCredentialPools(env, {
243
260
  apiUrl,
244
261
  apiKey,
@@ -246,10 +263,13 @@ async function fetchResolvedEnv(
246
263
  // CLAUDE_CODE_OAUTH_TOKEN stamped on their task record (and vice
247
264
  // versa) just because both env vars happen to be set in the worker
248
265
  // container. See `PROVIDER_CREDENTIAL_VARS` in src/utils/credentials.ts.
249
- provider: process.env.HARNESS_PROVIDER,
266
+ //
267
+ // Use the resolved provider (swarm_config > env) so an operator can flip
268
+ // the worker's harness from the dashboard without restarting the container.
269
+ provider: resolvedProvider,
250
270
  });
251
271
 
252
- return { env, credentialSelections };
272
+ return { env, credentialSelections, resolvedProvider };
253
273
  }
254
274
 
255
275
  /** Tools that produce noise — skip auto-progress for these */
@@ -533,6 +553,12 @@ export async function ensureTaskFinished(
533
553
  exitCode: number,
534
554
  failureReason?: string,
535
555
  providerOutput?: string,
556
+ /**
557
+ * Active provider for this task. When provided, gates the structured-output
558
+ * fallback path correctly even if `process.env.HARNESS_PROVIDER` differs
559
+ * from the resolved swarm_config value. Falls back to env when omitted.
560
+ */
561
+ provider?: ProviderName,
536
562
  ): Promise<void> {
537
563
  const headers: Record<string, string> = {
538
564
  "X-Agent-ID": config.agentId,
@@ -558,7 +584,7 @@ export async function ensureTaskFinished(
558
584
  body.output = providerOutput;
559
585
  } else {
560
586
  // Try structured output fallback if the task has an outputSchema
561
- const adapterType = process.env.HARNESS_PROVIDER || "claude";
587
+ const adapterType = provider ?? process.env.HARNESS_PROVIDER ?? "claude";
562
588
  const fallback = await handleStructuredOutputFallback(config, taskId, adapterType);
563
589
 
564
590
  console.log(`[${role}] Task ${taskId.slice(0, 8)} fallback result: ${fallback.kind}`);
@@ -890,7 +916,15 @@ function setupShutdownHandlers(
890
916
  console.warn(
891
917
  `[${role}] Failed to pause task ${taskId.slice(0, 8)}, marking as failed instead`,
892
918
  );
893
- await ensureTaskFinished(apiConfig, role, taskId, 1);
919
+ await ensureTaskFinished(
920
+ apiConfig,
921
+ role,
922
+ taskId,
923
+ 1,
924
+ undefined,
925
+ undefined,
926
+ state.harnessProvider,
927
+ );
894
928
  }
895
929
  }
896
930
  }
@@ -958,6 +992,14 @@ interface RunningTask {
958
992
  interface RunnerState {
959
993
  activeTasks: Map<string, RunningTask>;
960
994
  maxConcurrent: number;
995
+ /**
996
+ * Effective harness provider for this worker boot session — resolved
997
+ * from `swarm_config` (overlay) > `process.env.HARNESS_PROVIDER` > "claude".
998
+ * Used by error / cleanup paths so the structured-output fallback runs the
999
+ * right adapter even when env disagrees with swarm_config. Section 4
1000
+ * (per-task live re-resolution) will mutate this between tasks.
1001
+ */
1002
+ harnessProvider: ProviderName;
961
1003
  }
962
1004
 
963
1005
  /** Buffer for session logs */
@@ -1335,6 +1377,13 @@ async function registerAgent(opts: {
1335
1377
  role?: string;
1336
1378
  capabilities?: string[];
1337
1379
  maxTasks?: number;
1380
+ /**
1381
+ * Resolved harness provider (swarm_config > env > "claude"). Sent as both
1382
+ * the legacy `provider` field and the canonical `harness_provider` column.
1383
+ * Defaults to `process.env.HARNESS_PROVIDER || "claude"` for callers that
1384
+ * haven't migrated to passing it explicitly.
1385
+ */
1386
+ harnessProvider?: ProviderName;
1338
1387
  }): Promise<void> {
1339
1388
  const headers: Record<string, string> = {
1340
1389
  "Content-Type": "application/json",
@@ -1344,7 +1393,16 @@ async function registerAgent(opts: {
1344
1393
  headers.Authorization = `Bearer ${opts.apiKey}`;
1345
1394
  }
1346
1395
 
1347
- const provider = (process.env.HARNESS_PROVIDER || "claude") as ProviderName;
1396
+ const provider: ProviderName =
1397
+ opts.harnessProvider ?? ((process.env.HARNESS_PROVIDER || "claude") as ProviderName);
1398
+
1399
+ // Phase 1.5 (cloud-personalization): also push the canonical
1400
+ // `harness_provider` field so the API can persist it in its own column
1401
+ // (`agents.harness_provider`). Always send the resolved provider value
1402
+ // (defaulting to "claude" when HARNESS_PROVIDER is unset) so agents that
1403
+ // don't explicitly set the env var still self-report instead of leaving
1404
+ // the column NULL — matches how `provider` already defaults above.
1405
+ const harnessProvider: ProviderName = provider;
1348
1406
 
1349
1407
  const response = await fetch(`${opts.apiUrl}/api/agents`, {
1350
1408
  method: "POST",
@@ -1356,6 +1414,7 @@ async function registerAgent(opts: {
1356
1414
  capabilities: opts.capabilities,
1357
1415
  maxTasks: opts.maxTasks,
1358
1416
  provider,
1417
+ harness_provider: harnessProvider,
1359
1418
  }),
1360
1419
  });
1361
1420
 
@@ -1543,6 +1602,7 @@ async function fetchRelevantMemories(
1543
1602
  apiKey: string,
1544
1603
  agentId: string,
1545
1604
  taskDescription: string,
1605
+ taskId?: string,
1546
1606
  ): Promise<string | null> {
1547
1607
  try {
1548
1608
  const headers: Record<string, string> = {
@@ -1550,6 +1610,11 @@ async function fetchRelevantMemories(
1550
1610
  "X-Agent-ID": agentId,
1551
1611
  };
1552
1612
  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
1613
+ // Memory rater v1.5: server uses this header to log `memory_retrieval`
1614
+ // rows so server-side raters (ImplicitCitationRater) can score the
1615
+ // memories they surface against this task's session_logs at completion.
1616
+ // Plan: thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-2.md §2
1617
+ if (taskId) headers["X-Source-Task-ID"] = taskId;
1553
1618
 
1554
1619
  const response = await fetch(`${apiUrl}/api/memory/search`, {
1555
1620
  method: "POST",
@@ -1563,14 +1628,7 @@ async function fetchRelevantMemories(
1563
1628
  results: Array<{ id: string; name: string; content: string; similarity: number }>;
1564
1629
  };
1565
1630
 
1566
- const useful = (data.results || []).filter((m) => m.similarity > 0.4);
1567
- if (useful.length === 0) return null;
1568
-
1569
- const memoryContext = useful
1570
- .map((m) => `- **${m.name}** (id: ${m.id}): ${m.content.substring(0, 300)}`)
1571
- .join("\n");
1572
-
1573
- return `\n\n### Relevant Past Knowledge\n\nThese memories from your previous sessions may be useful. Use \`memory-get\` with the memory ID to retrieve full details.\n\n${memoryContext}\n`;
1631
+ return renderMemoriesPrompt(data.results || []);
1574
1632
  } catch {
1575
1633
  // Non-blocking — don't fail task start because of memory search
1576
1634
  return null;
@@ -2185,6 +2243,7 @@ async function checkCompletedProcesses(
2185
2243
  result.exitCode,
2186
2244
  failureReason,
2187
2245
  result.output,
2246
+ state.harnessProvider,
2188
2247
  );
2189
2248
 
2190
2249
  ensure({
@@ -2295,9 +2354,6 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2295
2354
  // Initialize Business-Use SDK for worker-side instrumentation
2296
2355
  initialize();
2297
2356
 
2298
- // Create provider adapter based on HARNESS_PROVIDER env var (default: claude)
2299
- const adapter = createProviderAdapter(process.env.HARNESS_PROVIDER || "claude");
2300
-
2301
2357
  const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
2302
2358
  const baseLogDir = opts.logsDir || process.env.LOG_DIR || "/logs";
2303
2359
  const logDir = `${baseLogDir}/${sessionId}`;
@@ -2312,6 +2368,30 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2312
2368
 
2313
2369
  const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
2314
2370
  const swarmUrl = process.env.SWARM_URL || "localhost";
2371
+ const apiKey = process.env.API_KEY || "";
2372
+
2373
+ // Resolve the boot harness provider from swarm_config (repo > agent > global,
2374
+ // overlaid on top of `process.env`). This is what selects the adapter for
2375
+ // this worker's lifetime. On a fresh worker (agentId="unknown") only global
2376
+ // swarm_config applies; once registered, an operator writing an agent-scoped
2377
+ // HARNESS_PROVIDER row takes effect on the next reconciliation cycle (Section 4)
2378
+ // or worker restart.
2379
+ //
2380
+ // Failures (network, API down, malformed value) fall back to env then "claude"
2381
+ // so a swarm_config outage cannot wedge boot.
2382
+ let bootProvider: ProviderName;
2383
+ try {
2384
+ bootProvider = (await fetchResolvedEnv(apiUrl, apiKey, agentId)).resolvedProvider;
2385
+ } catch (err) {
2386
+ console.warn(`[runner] fetchResolvedEnv failed at boot, falling back to env: ${err}`);
2387
+ bootProvider = resolveHarnessProvider({}, process.env);
2388
+ }
2389
+ console.log(`[runner] Resolved HARNESS_PROVIDER: ${bootProvider}`);
2390
+
2391
+ // Create provider adapter using the resolved value. `let` so the poll-loop
2392
+ // reconciliation block (Section 4) can swap it live when an operator changes
2393
+ // HARNESS_PROVIDER in swarm_config — call sites read the current binding.
2394
+ let adapter = createProviderAdapter(bootProvider);
2315
2395
 
2316
2396
  // Configure HTTP-based template resolution (workers resolve via API, not local DB)
2317
2397
  if (process.env.API_KEY) {
@@ -2382,9 +2462,11 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2382
2462
  // Slack context for current task (gates Slack instructions in prompt)
2383
2463
  let currentTaskSlackContext: BasePromptArgs["slackContext"] | undefined;
2384
2464
 
2385
- // Generate base prompt (identity fields injected after profile fetch below)
2386
- const { traits } = adapter;
2465
+ // Generate base prompt (identity fields injected after profile fetch below).
2466
+ // Traits are read fresh on each call so a live adapter swap (Section 4)
2467
+ // produces a prompt matching the new provider's capabilities.
2387
2468
  const buildSystemPrompt = async () => {
2469
+ const { traits } = adapter;
2388
2470
  return getBasePrompt({
2389
2471
  role,
2390
2472
  agentId,
@@ -2460,7 +2542,6 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2460
2542
  console.log(`[${role}] Total system prompt length: ${resolvedSystemPrompt.length} chars`);
2461
2543
 
2462
2544
  const isAiLoop = opts.aiLoop || process.env.AI_LOOP === "true";
2463
- const apiKey = process.env.API_KEY || "";
2464
2545
 
2465
2546
  // Constants for polling
2466
2547
  const PollIntervalMs = 2000; // 2 seconds between polls
@@ -2508,11 +2589,27 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2508
2589
  const state: RunnerState = {
2509
2590
  activeTasks: new Map(),
2510
2591
  maxConcurrent,
2592
+ harnessProvider: bootProvider,
2511
2593
  };
2512
2594
 
2513
2595
  // Track tasks already signaled for cancellation to avoid repeated SIGTERM
2514
2596
  const cancelledSignaled = new Set<string>();
2515
2597
 
2598
+ // Migration 055 — cache the harness_provider value used when we last
2599
+ // built a `cred_status` snapshot. Re-runs the post-task check only when
2600
+ // the resolved provider changes. Section 4 of the swarm_config-overrides-
2601
+ // HARNESS_PROVIDER work makes this dynamic: state.harnessProvider is
2602
+ // reconciled below from `swarm_config`, so an operator's change reaches
2603
+ // here without a worker restart.
2604
+ let cachedCredHarnessProvider: string | null = null;
2605
+
2606
+ // Throttle for live HARNESS_PROVIDER reconciliation. Each reconciliation
2607
+ // calls `fetchResolvedEnv` which also re-resolves credential pools — we
2608
+ // don't want that on every 2s poll. 10s gives operator changes a near-
2609
+ // immediate effect from a UX perspective without hammering the API.
2610
+ let lastHarnessReconcileAt = 0;
2611
+ const HARNESS_RECONCILE_INTERVAL_MS = 10_000;
2612
+
2516
2613
  // Create API config for ping/close
2517
2614
  const apiConfig: ApiConfig = { apiUrl, apiKey, agentId };
2518
2615
 
@@ -2534,6 +2631,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2534
2631
  isLead,
2535
2632
  capabilities,
2536
2633
  maxTasks: maxConcurrent,
2634
+ harnessProvider: bootProvider,
2537
2635
  });
2538
2636
  console.log(`[${role}] Registered as "${agentName}" (ID: ${agentId})`);
2539
2637
  } catch (error) {
@@ -2541,6 +2639,65 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2541
2639
  process.exit(1);
2542
2640
  }
2543
2641
 
2642
+ // Block until harness credentials are present in env. This loop replaces
2643
+ // the old bash-level fail-fast in `docker-entrypoint.sh` — the worker is
2644
+ // already registered (visible to the dashboard) and self-heals once
2645
+ // creds appear in `swarm_config`. See plans/2026-05-06-worker-credential-safe-loop.md.
2646
+ //
2647
+ // CRED_CHECK_DISABLE=1 opts out entirely: the worker trusts the operator
2648
+ // and starts polling immediately, with a NULL `cred_status` row that the
2649
+ // dashboard surfaces as "unreported."
2650
+ const harnessProvider = bootProvider;
2651
+ cachedCredHarnessProvider = harnessProvider;
2652
+ if (isCredCheckDisabled(process.env)) {
2653
+ console.log(`[${role}] CRED_CHECK_DISABLE=1, skipping credential checks`);
2654
+ } else {
2655
+ try {
2656
+ await awaitCredentials({
2657
+ provider: harnessProvider,
2658
+ refreshEnv: async () => {
2659
+ const { env } = await fetchResolvedEnv(apiUrl, apiKey, agentId);
2660
+ return env;
2661
+ },
2662
+ onTick: (status) => {
2663
+ // Best-effort status report — the dispatcher uses it to route
2664
+ // around blocked agents. Failures are non-fatal (the wait loop
2665
+ // already swallows onTick exceptions). We do NOT include
2666
+ // `cred_status` here — the live test runs once the worker is
2667
+ // ready (below), and intermediate ticks are presence-only.
2668
+ fetch(`${apiUrl}/api/agents/${encodeURIComponent(agentId)}/credential-status`, {
2669
+ method: "PUT",
2670
+ headers: {
2671
+ Authorization: `Bearer ${apiKey}`,
2672
+ "X-Agent-ID": agentId,
2673
+ "Content-Type": "application/json",
2674
+ },
2675
+ body: JSON.stringify({ ready: status.ready, missing: status.missing }),
2676
+ }).catch(() => {
2677
+ // Swallowed — Phase 2 wait loop logs every tick anyway.
2678
+ });
2679
+ },
2680
+ });
2681
+ } catch (err) {
2682
+ if (err instanceof BootMaxWaitExceededError) {
2683
+ console.error(`[${role}] ${err.message}`);
2684
+ process.exit(EX_CONFIG);
2685
+ }
2686
+ throw err;
2687
+ }
2688
+
2689
+ // Migration 055: build the full snapshot (presence + live test) once
2690
+ // creds are ready and POST it to the agent row. Status endpoint reads
2691
+ // this instead of running predicates server-side.
2692
+ try {
2693
+ const snapshot = await buildCredStatusReport(harnessProvider, process.env, {}, "boot");
2694
+ await reportCredStatus(apiUrl, apiKey, agentId, snapshot);
2695
+ } catch (err) {
2696
+ // Non-fatal — worker proceeds even if reporting fails.
2697
+ console.warn(`[${role}] cred_status boot report failed (non-fatal): ${err}`);
2698
+ }
2699
+ }
2700
+
2544
2701
  // Clean up any stale active sessions from previous runs (crash recovery)
2545
2702
  await cleanupActiveSessions(apiConfig);
2546
2703
  console.log(`[${role}] Cleaned up stale active sessions`);
@@ -2865,6 +3022,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2865
3022
  apiKey,
2866
3023
  agentId,
2867
3024
  task.task,
3025
+ task.id,
2868
3026
  );
2869
3027
  if (resumeMemoryContext) {
2870
3028
  resumePrompt += resumeMemoryContext;
@@ -2970,7 +3128,15 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2970
3128
  console.error(
2971
3129
  `[${role}] Failed to spawn process for resumed task ${task.id.slice(0, 8)}: ${errMsg}`,
2972
3130
  );
2973
- await ensureTaskFinished(apiConfig, role, task.id, 1, `Spawn failed: ${errMsg}`);
3131
+ await ensureTaskFinished(
3132
+ apiConfig,
3133
+ role,
3134
+ task.id,
3135
+ 1,
3136
+ `Spawn failed: ${errMsg}`,
3137
+ undefined,
3138
+ state.harnessProvider,
3139
+ );
2974
3140
  continue;
2975
3141
  }
2976
3142
 
@@ -3020,6 +3186,64 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
3020
3186
  // Check for completed processes first and ensure tasks are marked as finished
3021
3187
  await checkCompletedProcesses(state, role, apiConfig);
3022
3188
 
3189
+ // Live HARNESS_PROVIDER reconciliation. Re-fetches `swarm_config` (overlaid
3190
+ // on env) and swaps the adapter if the resolved provider changed —
3191
+ // typically because an operator PATCH'd /api/agents/:id/harness-provider
3192
+ // (which writes a swarm_config row) or upserted a config row directly.
3193
+ //
3194
+ // Safety: in-flight sessions hold their own `ProviderSession` references
3195
+ // and continue on the old adapter unaffected. New spawns (below) read
3196
+ // the current `adapter` binding and pick up the swap. `basePrompt` is
3197
+ // rebuilt because traits (and therefore prompt content) may differ across
3198
+ // providers.
3199
+ if (Date.now() - lastHarnessReconcileAt > HARNESS_RECONCILE_INTERVAL_MS) {
3200
+ lastHarnessReconcileAt = Date.now();
3201
+ try {
3202
+ const { resolvedProvider } = await fetchResolvedEnv(apiUrl, apiKey, agentId);
3203
+ if (resolvedProvider !== state.harnessProvider) {
3204
+ const previous = state.harnessProvider;
3205
+ console.log(
3206
+ `[${role}] [harness] Reconciling adapter: ${previous} → ${resolvedProvider}`,
3207
+ );
3208
+ try {
3209
+ adapter = createProviderAdapter(resolvedProvider);
3210
+ state.harnessProvider = resolvedProvider;
3211
+ basePrompt = await buildSystemPrompt();
3212
+ resolvedSystemPrompt = additionalSystemPrompt
3213
+ ? `${basePrompt}\n\n${additionalSystemPrompt}`
3214
+ : basePrompt;
3215
+ // Force a fresh cred_status report below for the new provider.
3216
+ cachedCredHarnessProvider = null;
3217
+ console.log(
3218
+ `[${role}] [harness] Swapped to ${resolvedProvider} (basePrompt rebuilt: ${basePrompt.length} chars)`,
3219
+ );
3220
+ } catch (err) {
3221
+ console.warn(
3222
+ `[${role}] [harness] Failed to swap to ${resolvedProvider} (staying on ${previous}): ${err}`,
3223
+ );
3224
+ }
3225
+ }
3226
+ } catch (err) {
3227
+ console.warn(`[${role}] [harness] Reconcile fetch failed (non-fatal): ${err}`);
3228
+ }
3229
+ }
3230
+
3231
+ // Migration 055 — post-task credential refresh, cache-keyed on the
3232
+ // *resolved* harness_provider. Re-runs the snapshot when the provider
3233
+ // changes (boot, or after a live swap above) so the dashboard shows
3234
+ // up-to-date credential status for the active adapter.
3235
+ if (!isCredCheckDisabled(process.env)) {
3236
+ const currentHarness = state.harnessProvider;
3237
+ if (currentHarness !== cachedCredHarnessProvider) {
3238
+ cachedCredHarnessProvider = currentHarness;
3239
+ buildCredStatusReport(currentHarness, process.env, {}, "post_task")
3240
+ .then((snap) => reportCredStatus(apiUrl, apiKey, agentId, snap))
3241
+ .catch((err) =>
3242
+ console.warn(`[${role}] cred_status post_task report failed (non-fatal): ${err}`),
3243
+ );
3244
+ }
3245
+ }
3246
+
3023
3247
  // Periodic VCS detection for running tasks (fire-and-forget, throttled per task)
3024
3248
  const now = Date.now();
3025
3249
  for (const [taskId, task] of state.activeTasks) {
@@ -3150,7 +3374,13 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
3150
3374
  ? (trigger.task as { task: string; id?: string })
3151
3375
  : null;
3152
3376
  if (task?.task) {
3153
- const memoryContext = await fetchRelevantMemories(apiUrl, apiKey, agentId, task.task);
3377
+ const memoryContext = await fetchRelevantMemories(
3378
+ apiUrl,
3379
+ apiKey,
3380
+ agentId,
3381
+ task.task,
3382
+ task.id,
3383
+ );
3154
3384
  if (memoryContext) {
3155
3385
  triggerPrompt += memoryContext;
3156
3386
  console.log(`[${role}] Injected relevant memories into task prompt`);
@@ -3320,6 +3550,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
3320
3550
  trigger.taskId,
3321
3551
  1,
3322
3552
  `Spawn failed: ${errMsg}`,
3553
+ undefined,
3554
+ state.harnessProvider,
3323
3555
  );
3324
3556
  }
3325
3557
  continue;