@desplega.ai/agent-swarm 1.83.0 → 1.83.2
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.
- package/openapi.json +177 -10
- package/package.json +6 -6
- package/src/artifact-sdk/server.ts +23 -1
- package/src/be/budget-admission.ts +28 -4
- package/src/be/budget-refusal-notify.ts +19 -3
- package/src/be/db-queries/oauth.ts +43 -0
- package/src/be/db.ts +37 -4
- package/src/be/migrations/074_user_budget_scope.sql +85 -0
- package/src/be/schedules/validate.ts +21 -0
- package/src/be/skill-sync.ts +65 -15
- package/src/commands/resume-session.ts +118 -0
- package/src/commands/runner.ts +178 -121
- package/src/http/core.ts +4 -1
- package/src/http/index.ts +16 -0
- package/src/http/integrations.ts +26 -0
- package/src/http/mcp-user.ts +111 -0
- package/src/http/poll.ts +19 -5
- package/src/http/schedules.ts +35 -10
- package/src/http/skills.ts +27 -2
- package/src/http/users.ts +107 -2
- package/src/jira/client.ts +3 -5
- package/src/jira/oauth.ts +1 -0
- package/src/jira/sync.ts +2 -2
- package/src/oauth/ensure-token.ts +1 -0
- package/src/oauth/wrapper.ts +38 -7
- package/src/providers/claude-adapter.ts +7 -2
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +30 -0
- package/src/providers/opencode-adapter.ts +149 -14
- package/src/providers/pi-mono-adapter.ts +41 -1
- package/src/providers/types.ts +1 -1
- package/src/server-user.ts +117 -0
- package/src/tests/artifact-sdk.test.ts +23 -19
- package/src/tests/budget-user-scope.test.ts +376 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/codex-adapter.test.ts +192 -0
- package/src/tests/codex-rate-limit-parse.test.ts +256 -0
- package/src/tests/db-queries-oauth.test.ts +43 -0
- package/src/tests/ensure-token.test.ts +93 -0
- package/src/tests/error-tracker.test.ts +52 -0
- package/src/tests/fetch-resolved-env.test.ts +33 -20
- package/src/tests/http-api-integration.test.ts +36 -0
- package/src/tests/http-users.test.ts +29 -1
- package/src/tests/mcp-user-route.test.ts +325 -0
- package/src/tests/opencode-adapter.test.ts +75 -0
- package/src/tests/pi-mono-adapter.test.ts +21 -1
- package/src/tests/rate-limit-event.test.ts +69 -6
- package/src/tests/resume-session.test.ts +93 -0
- package/src/tests/runner-skills-refresh.test.ts +200 -0
- package/src/tests/schedule-validation-helper.test.ts +51 -0
- package/src/tests/skill-sync.test.ts +73 -9
- package/src/tests/skills-signature.test.ts +141 -0
- package/src/tests/task-tools-ctx.test.ts +100 -0
- package/src/tests/task-tools-ownership.test.ts +167 -0
- package/src/tests/update-schedule-mcp-tool.test.ts +161 -0
- package/src/tests/user-token-routes.test.ts +221 -0
- package/src/tools/cancel-task.ts +137 -83
- package/src/tools/get-task-details.ts +73 -59
- package/src/tools/get-tasks.ts +134 -126
- package/src/tools/schedules/update-schedule.ts +48 -8
- package/src/tools/send-task.ts +312 -312
- package/src/tools/slack-upload-file.ts +17 -5
- package/src/tools/task-action.ts +464 -367
- package/src/tools/task-tool-ctx.ts +43 -0
- package/src/types.ts +6 -2
- package/src/utils/error-tracker.ts +122 -9
- package/src/utils/skills-refresh.ts +123 -0
package/src/commands/runner.ts
CHANGED
|
@@ -38,10 +38,15 @@ import { getApiKey } from "../utils/api-key.ts";
|
|
|
38
38
|
import { computeBudgetBackoffMs } from "../utils/budget-backoff.ts";
|
|
39
39
|
import { getContextWindowSize } from "../utils/context-window.ts";
|
|
40
40
|
import { type CredentialSelection, resolveCredentialPools } from "../utils/credentials.ts";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
isRateLimitMessage,
|
|
43
|
+
MAX_RATE_LIMIT_RESET_MS,
|
|
44
|
+
parseRateLimitResetTime,
|
|
45
|
+
} from "../utils/error-tracker.ts";
|
|
42
46
|
import { resolveHarnessProvider } from "../utils/harness-provider.ts";
|
|
43
47
|
import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
|
|
44
48
|
import { scrubSecrets } from "../utils/secret-scrubber.ts";
|
|
49
|
+
import { refreshSkillsIfChanged } from "../utils/skills-refresh.ts";
|
|
45
50
|
import { detectVcsProvider } from "../vcs/index.ts";
|
|
46
51
|
import { interpolate } from "../workflows/template.ts";
|
|
47
52
|
import { awaitCredentials, BootMaxWaitExceededError, EX_CONFIG } from "./credential-wait.ts";
|
|
@@ -52,6 +57,11 @@ import {
|
|
|
52
57
|
reportCredStatus,
|
|
53
58
|
reportLatestModel,
|
|
54
59
|
} from "./provider-credentials.ts";
|
|
60
|
+
import {
|
|
61
|
+
type ResumeSessionCandidate,
|
|
62
|
+
type ResumeSessionResolution,
|
|
63
|
+
resolveResumeSession,
|
|
64
|
+
} from "./resume-session.ts";
|
|
55
65
|
// Side-effect import: registers runner trigger/resumption templates
|
|
56
66
|
import "./templates.ts";
|
|
57
67
|
|
|
@@ -989,6 +999,8 @@ async function getPausedTasksFromAPI(config: ApiConfig): Promise<
|
|
|
989
999
|
task: string;
|
|
990
1000
|
progress?: string;
|
|
991
1001
|
claudeSessionId?: string;
|
|
1002
|
+
provider?: ProviderName;
|
|
1003
|
+
providerMeta?: Record<string, unknown>;
|
|
992
1004
|
parentTaskId?: string;
|
|
993
1005
|
dir?: string;
|
|
994
1006
|
vcsRepo?: string;
|
|
@@ -1021,6 +1033,8 @@ async function getPausedTasksFromAPI(config: ApiConfig): Promise<
|
|
|
1021
1033
|
task: string;
|
|
1022
1034
|
progress?: string;
|
|
1023
1035
|
claudeSessionId?: string;
|
|
1036
|
+
provider?: ProviderName;
|
|
1037
|
+
providerMeta?: Record<string, unknown>;
|
|
1024
1038
|
parentTaskId?: string;
|
|
1025
1039
|
dir?: string;
|
|
1026
1040
|
vcsRepo?: string;
|
|
@@ -1423,24 +1437,51 @@ async function saveProviderSessionIdOnActiveSession(
|
|
|
1423
1437
|
});
|
|
1424
1438
|
}
|
|
1425
1439
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1440
|
+
interface ProviderSessionInfo {
|
|
1441
|
+
sessionId: string | null;
|
|
1442
|
+
provider?: ProviderName;
|
|
1443
|
+
providerMeta?: Record<string, unknown>;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/** Fetch provider session metadata for a task (for resume continuity). */
|
|
1447
|
+
async function fetchProviderSessionInfo(
|
|
1428
1448
|
apiUrl: string,
|
|
1429
1449
|
apiKey: string,
|
|
1430
1450
|
taskId: string,
|
|
1431
|
-
): Promise<
|
|
1451
|
+
): Promise<ProviderSessionInfo | null> {
|
|
1432
1452
|
const headers: Record<string, string> = {};
|
|
1433
1453
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
1434
1454
|
try {
|
|
1435
1455
|
const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, { headers });
|
|
1436
1456
|
if (!response.ok) return null;
|
|
1437
|
-
const data = (await response.json()) as {
|
|
1438
|
-
|
|
1457
|
+
const data = (await response.json()) as {
|
|
1458
|
+
claudeSessionId?: string;
|
|
1459
|
+
provider?: ProviderName;
|
|
1460
|
+
providerMeta?: Record<string, unknown>;
|
|
1461
|
+
};
|
|
1462
|
+
return {
|
|
1463
|
+
sessionId: data.claudeSessionId || null,
|
|
1464
|
+
provider: data.provider,
|
|
1465
|
+
providerMeta: data.providerMeta,
|
|
1466
|
+
};
|
|
1439
1467
|
} catch {
|
|
1440
1468
|
return null;
|
|
1441
1469
|
}
|
|
1442
1470
|
}
|
|
1443
1471
|
|
|
1472
|
+
function logResumeResolution(role: string, resolution: ResumeSessionResolution): void {
|
|
1473
|
+
for (const skipped of resolution.skipped) {
|
|
1474
|
+
console.warn(
|
|
1475
|
+
`[${role}] Skipping ${skipped.source} session resume ${skipped.sessionId.slice(0, 8)}: ${skipped.reason}`,
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (resolution.resumeSessionId) {
|
|
1480
|
+
const source = resolution.source === "parent" ? "parent session" : "task's own session";
|
|
1481
|
+
console.log(`[${role}] Resuming ${source} ${resolution.resumeSessionId.slice(0, 8)}`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1444
1485
|
/** Register an active session with the API (fire-and-forget) */
|
|
1445
1486
|
async function registerActiveSession(
|
|
1446
1487
|
config: ApiConfig,
|
|
@@ -2101,6 +2142,7 @@ async function spawnProviderProcess(
|
|
|
2101
2142
|
iteration: number;
|
|
2102
2143
|
taskId?: string;
|
|
2103
2144
|
model?: string;
|
|
2145
|
+
resumeSessionId?: string;
|
|
2104
2146
|
harnessProvider: ProviderName;
|
|
2105
2147
|
cwd?: string;
|
|
2106
2148
|
vcsRepo?: string;
|
|
@@ -2166,6 +2208,7 @@ async function spawnProviderProcess(
|
|
|
2166
2208
|
vcsRepo: opts.vcsRepo,
|
|
2167
2209
|
logFile: opts.logFile,
|
|
2168
2210
|
additionalArgs: opts.additionalArgs,
|
|
2211
|
+
resumeSessionId: opts.resumeSessionId,
|
|
2169
2212
|
iteration: opts.iteration,
|
|
2170
2213
|
env: freshEnv as Record<string, string>,
|
|
2171
2214
|
// Propagate the selected OAuth slot so the adapter refreshes back to the
|
|
@@ -2812,54 +2855,61 @@ async function checkCompletedProcesses(
|
|
|
2812
2855
|
if (result.exitCode !== 0 && result.failureReason) {
|
|
2813
2856
|
failureReason = result.failureReason;
|
|
2814
2857
|
console.log(`[${role}] Detected error for task ${taskId.slice(0, 8)}: ${failureReason}`);
|
|
2858
|
+
}
|
|
2815
2859
|
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2860
|
+
// If rate-limited and we know which key was used, report it.
|
|
2861
|
+
// Codex adapter prefixes failure reasons with `[rate-limit]` /
|
|
2862
|
+
// `[usage-limit]` (see codex-adapter.formatTerminalError); Claude
|
|
2863
|
+
// surfaces "rate limit" / "hit your limit" via SessionErrorTracker.
|
|
2864
|
+
//
|
|
2865
|
+
// The gate must also fire on a bare structured rate_limit_event: a
|
|
2866
|
+
// `status: "rejected"` event sets result.rateLimitResetAt but does NOT
|
|
2867
|
+
// set hasErrors(), so failureReason can be empty even though the key is
|
|
2868
|
+
// exhausted. Gating on rateLimitResetAt != null ensures the structured
|
|
2869
|
+
// event alone still triggers the cooldown.
|
|
2870
|
+
if (
|
|
2871
|
+
credentialInfo &&
|
|
2872
|
+
(result.rateLimitResetAt != null ||
|
|
2873
|
+
(failureReason != null && isRateLimitMessage(failureReason)))
|
|
2874
|
+
) {
|
|
2875
|
+
// Three-tier reset-time resolver (most to least precise):
|
|
2876
|
+
// Tier 1: structured rate_limit_event from Claude CLI (resetsAt epoch sec)
|
|
2877
|
+
// Tier 2: regex on the error message (e.g. "resets 3pm (UTC)")
|
|
2878
|
+
// Tier 3: 5-min hard fallback — only when both structured and regex fail
|
|
2879
|
+
// Tiers 1 & 2 are clamped to [now+60s, now+7d] (weekly limits reset ~2 days out).
|
|
2880
|
+
const clampResetTime = (isoString: string): string => {
|
|
2881
|
+
const nowMs = Date.now();
|
|
2882
|
+
const minMs = nowMs + 60_000;
|
|
2883
|
+
const maxMs = nowMs + MAX_RATE_LIMIT_RESET_MS;
|
|
2884
|
+
const candidateMs = new Date(isoString).getTime();
|
|
2885
|
+
return new Date(Math.min(Math.max(candidateMs, minMs), maxMs)).toISOString();
|
|
2886
|
+
};
|
|
2836
2887
|
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2888
|
+
let rateLimitedUntil: string;
|
|
2889
|
+
if (result.rateLimitResetAt) {
|
|
2890
|
+
rateLimitedUntil = clampResetTime(result.rateLimitResetAt);
|
|
2891
|
+
console.log(`[credentials] Rate limit reset from rate_limit_event: ${rateLimitedUntil}`);
|
|
2892
|
+
} else if (failureReason != null) {
|
|
2893
|
+
const parsedResetTime = parseRateLimitResetTime(failureReason);
|
|
2894
|
+
if (parsedResetTime) {
|
|
2895
|
+
rateLimitedUntil = clampResetTime(parsedResetTime);
|
|
2840
2896
|
console.log(
|
|
2841
|
-
`[credentials]
|
|
2897
|
+
`[credentials] Parsed rate limit reset time from error: ${rateLimitedUntil}`,
|
|
2842
2898
|
);
|
|
2843
2899
|
} else {
|
|
2844
|
-
|
|
2845
|
-
if (parsedResetTime) {
|
|
2846
|
-
rateLimitedUntil = clampResetTime(parsedResetTime);
|
|
2847
|
-
console.log(
|
|
2848
|
-
`[credentials] Parsed rate limit reset time from error: ${rateLimitedUntil}`,
|
|
2849
|
-
);
|
|
2850
|
-
} else {
|
|
2851
|
-
rateLimitedUntil = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
|
2852
|
-
}
|
|
2900
|
+
rateLimitedUntil = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
|
2853
2901
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
apiConfig.apiKey,
|
|
2857
|
-
credentialInfo.keyType,
|
|
2858
|
-
credentialInfo.keySuffix,
|
|
2859
|
-
credentialInfo.keyIndex,
|
|
2860
|
-
rateLimitedUntil,
|
|
2861
|
-
).catch(() => {});
|
|
2902
|
+
} else {
|
|
2903
|
+
rateLimitedUntil = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
|
2862
2904
|
}
|
|
2905
|
+
reportKeyRateLimit(
|
|
2906
|
+
apiConfig.apiUrl,
|
|
2907
|
+
apiConfig.apiKey,
|
|
2908
|
+
credentialInfo.keyType,
|
|
2909
|
+
credentialInfo.keySuffix,
|
|
2910
|
+
credentialInfo.keyIndex,
|
|
2911
|
+
rateLimitedUntil,
|
|
2912
|
+
).catch(() => {});
|
|
2863
2913
|
}
|
|
2864
2914
|
await ensureTaskFinished(
|
|
2865
2915
|
apiConfig,
|
|
@@ -3511,34 +3561,6 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3511
3561
|
}
|
|
3512
3562
|
}
|
|
3513
3563
|
|
|
3514
|
-
// Fetch installed skills for system prompt
|
|
3515
|
-
try {
|
|
3516
|
-
const skillsResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills`, {
|
|
3517
|
-
headers: {
|
|
3518
|
-
Authorization: `Bearer ${apiKey}`,
|
|
3519
|
-
"X-Agent-ID": agentId,
|
|
3520
|
-
},
|
|
3521
|
-
});
|
|
3522
|
-
if (skillsResp.ok) {
|
|
3523
|
-
const skillsData = (await skillsResp.json()) as {
|
|
3524
|
-
skills: {
|
|
3525
|
-
name: string;
|
|
3526
|
-
description: string;
|
|
3527
|
-
isActive: boolean;
|
|
3528
|
-
isEnabled: boolean;
|
|
3529
|
-
}[];
|
|
3530
|
-
};
|
|
3531
|
-
agentSkillsSummary = skillsData.skills
|
|
3532
|
-
.filter((s) => s.isActive && s.isEnabled)
|
|
3533
|
-
.map((s) => ({ name: s.name, description: s.description }));
|
|
3534
|
-
if (agentSkillsSummary.length > 0) {
|
|
3535
|
-
console.log(`[${role}] Loaded ${agentSkillsSummary.length} skills for system prompt`);
|
|
3536
|
-
}
|
|
3537
|
-
}
|
|
3538
|
-
} catch {
|
|
3539
|
-
// Non-fatal — skills are optional
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
3564
|
// Fetch installed MCP servers for system prompt
|
|
3543
3565
|
try {
|
|
3544
3566
|
const mcpServersResp = await fetch(`${apiUrl}/api/agents/${agentId}/mcp-servers`, {
|
|
@@ -3649,35 +3671,32 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3649
3671
|
}
|
|
3650
3672
|
}
|
|
3651
3673
|
|
|
3652
|
-
// ==========
|
|
3653
|
-
|
|
3674
|
+
// ========== Boot-time skill load (signature-gated, replaces the standalone
|
|
3675
|
+
// skill-fetch + FS sync blocks). The polling loop below calls the same
|
|
3676
|
+
// helper per task to hot-reload skills mid-flight. Skipped for
|
|
3677
|
+
// `claude-managed` (cloud sandbox owns skill delivery).
|
|
3678
|
+
const lastSkillHash: { current: string | null } = { current: null };
|
|
3679
|
+
if (state.harnessProvider !== "claude-managed") {
|
|
3654
3680
|
console.log(`[${role}] Syncing skills to filesystem...`);
|
|
3655
|
-
const
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
if (
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
});
|
|
3664
|
-
if (syncRes.ok) {
|
|
3665
|
-
const syncResult = (await syncRes.json()) as {
|
|
3666
|
-
synced: number;
|
|
3667
|
-
removed: number;
|
|
3668
|
-
errors: string[];
|
|
3669
|
-
};
|
|
3670
|
-
console.log(
|
|
3671
|
-
`[${role}] Skills synced: ${syncResult.synced} written, ${syncResult.removed} removed`,
|
|
3672
|
-
);
|
|
3673
|
-
if (syncResult.errors.length > 0) {
|
|
3674
|
-
console.warn(`[${role}] Skill sync errors: ${syncResult.errors.join(", ")}`);
|
|
3681
|
+
const skillResult = await refreshSkillsIfChanged(
|
|
3682
|
+
{ apiUrl, swarmUrl, apiKey, agentId, role },
|
|
3683
|
+
lastSkillHash,
|
|
3684
|
+
);
|
|
3685
|
+
if (skillResult.changed && skillResult.summary) {
|
|
3686
|
+
agentSkillsSummary = skillResult.summary;
|
|
3687
|
+
if (agentSkillsSummary.length > 0) {
|
|
3688
|
+
console.log(`[${role}] Loaded ${agentSkillsSummary.length} skills for system prompt`);
|
|
3675
3689
|
}
|
|
3676
|
-
|
|
3677
|
-
|
|
3690
|
+
// Rebuild base prompt now that we have skills.
|
|
3691
|
+
basePrompt = await buildSystemPrompt();
|
|
3692
|
+
resolvedSystemPrompt = additionalSystemPrompt
|
|
3693
|
+
? `${basePrompt}\n\n${additionalSystemPrompt}`
|
|
3694
|
+
: basePrompt;
|
|
3678
3695
|
}
|
|
3679
|
-
}
|
|
3680
|
-
console.
|
|
3696
|
+
} else {
|
|
3697
|
+
console.log(
|
|
3698
|
+
`[${role}] Skipping skill sync (claude-managed reads skills from agent definition)`,
|
|
3699
|
+
);
|
|
3681
3700
|
}
|
|
3682
3701
|
|
|
3683
3702
|
// ========== Resume paused tasks with PRIORITY ==========
|
|
@@ -3733,18 +3752,30 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3733
3752
|
console.log(`[${role}] Injected relevant memories into resumed task prompt`);
|
|
3734
3753
|
}
|
|
3735
3754
|
|
|
3736
|
-
// Resolve
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3755
|
+
// Resolve provider-aware resume: prefer own session, then parent.
|
|
3756
|
+
const resumeCandidates: ResumeSessionCandidate[] = [
|
|
3757
|
+
{
|
|
3758
|
+
source: "task",
|
|
3759
|
+
taskId: task.id,
|
|
3760
|
+
sessionId: task.claudeSessionId,
|
|
3761
|
+
provider: task.provider,
|
|
3762
|
+
providerMeta: task.providerMeta,
|
|
3763
|
+
},
|
|
3764
|
+
];
|
|
3765
|
+
if (task.parentTaskId) {
|
|
3766
|
+
const parentSession = await fetchProviderSessionInfo(apiUrl, apiKey, task.parentTaskId);
|
|
3767
|
+
if (parentSession?.sessionId) {
|
|
3768
|
+
resumeCandidates.push({
|
|
3769
|
+
source: "parent",
|
|
3770
|
+
taskId: task.parentTaskId,
|
|
3771
|
+
sessionId: parentSession.sessionId,
|
|
3772
|
+
provider: parentSession.provider,
|
|
3773
|
+
providerMeta: parentSession.providerMeta,
|
|
3774
|
+
});
|
|
3746
3775
|
}
|
|
3747
3776
|
}
|
|
3777
|
+
const resumeResolution = resolveResumeSession(state.harnessProvider, resumeCandidates);
|
|
3778
|
+
logResumeResolution(role, resumeResolution);
|
|
3748
3779
|
|
|
3749
3780
|
// Spawn Claude process for resumed task
|
|
3750
3781
|
iteration++;
|
|
@@ -3810,7 +3841,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
3810
3841
|
prompt: resumePrompt,
|
|
3811
3842
|
logFile,
|
|
3812
3843
|
systemPrompt: resolvedSystemPrompt,
|
|
3813
|
-
additionalArgs:
|
|
3844
|
+
additionalArgs: opts.additionalArgs,
|
|
3845
|
+
resumeSessionId: resumeResolution.resumeSessionId,
|
|
3814
3846
|
role,
|
|
3815
3847
|
apiUrl,
|
|
3816
3848
|
apiKey,
|
|
@@ -4075,20 +4107,27 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4075
4107
|
}
|
|
4076
4108
|
}
|
|
4077
4109
|
|
|
4078
|
-
// Resolve
|
|
4079
|
-
let
|
|
4110
|
+
// Resolve provider-aware resume for child tasks with parentTaskId.
|
|
4111
|
+
let resumeSessionId: string | undefined;
|
|
4080
4112
|
const taskObj = trigger.task as { parentTaskId?: string } | undefined;
|
|
4081
4113
|
if (taskObj?.parentTaskId) {
|
|
4082
|
-
const
|
|
4114
|
+
const parentSession = await fetchProviderSessionInfo(
|
|
4083
4115
|
apiUrl,
|
|
4084
4116
|
apiKey,
|
|
4085
4117
|
taskObj.parentTaskId,
|
|
4086
4118
|
);
|
|
4087
|
-
if (
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4119
|
+
if (parentSession?.sessionId) {
|
|
4120
|
+
const resumeResolution = resolveResumeSession(state.harnessProvider, [
|
|
4121
|
+
{
|
|
4122
|
+
source: "parent",
|
|
4123
|
+
taskId: taskObj.parentTaskId,
|
|
4124
|
+
sessionId: parentSession.sessionId,
|
|
4125
|
+
provider: parentSession.provider,
|
|
4126
|
+
providerMeta: parentSession.providerMeta,
|
|
4127
|
+
},
|
|
4128
|
+
]);
|
|
4129
|
+
logResumeResolution(role, resumeResolution);
|
|
4130
|
+
resumeSessionId = resumeResolution.resumeSessionId;
|
|
4092
4131
|
} else {
|
|
4093
4132
|
console.log(`[${role}] Child task — parent session ID not found, starting fresh`);
|
|
4094
4133
|
}
|
|
@@ -4168,6 +4207,23 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4168
4207
|
cwdWarning = `\n\nNote: The task requested working directory "${taskDir}" but it does not exist. Falling back to default directory.`;
|
|
4169
4208
|
}
|
|
4170
4209
|
|
|
4210
|
+
// Per-task skill hot-reload. Reuses the boot-time helper; signature
|
|
4211
|
+
// probe short-circuits when nothing changed. Skipped for
|
|
4212
|
+
// `claude-managed`. Read state.harnessProvider live so an adapter
|
|
4213
|
+
// swap mid-loop honors the new provider.
|
|
4214
|
+
if (state.harnessProvider !== "claude-managed") {
|
|
4215
|
+
const skillResult = await refreshSkillsIfChanged(
|
|
4216
|
+
{ apiUrl, swarmUrl, apiKey, agentId, role },
|
|
4217
|
+
lastSkillHash,
|
|
4218
|
+
);
|
|
4219
|
+
if (skillResult.changed && skillResult.summary) {
|
|
4220
|
+
agentSkillsSummary = skillResult.summary;
|
|
4221
|
+
console.log(
|
|
4222
|
+
`[${role}] Skills changed — refreshing system prompt (${agentSkillsSummary.length} skills)`,
|
|
4223
|
+
);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4171
4227
|
// Rebuild system prompt with per-task repo context
|
|
4172
4228
|
const taskBasePrompt = await buildSystemPrompt();
|
|
4173
4229
|
const taskSystemPrompt =
|
|
@@ -4210,7 +4266,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
4210
4266
|
prompt: triggerPrompt,
|
|
4211
4267
|
logFile,
|
|
4212
4268
|
systemPrompt: taskSystemPrompt,
|
|
4213
|
-
additionalArgs:
|
|
4269
|
+
additionalArgs: opts.additionalArgs,
|
|
4270
|
+
resumeSessionId,
|
|
4214
4271
|
role,
|
|
4215
4272
|
apiUrl,
|
|
4216
4273
|
apiKey,
|
package/src/http/core.ts
CHANGED
|
@@ -240,7 +240,10 @@ export async function handleCore(
|
|
|
240
240
|
// fall through to the bearer check (fail-closed).
|
|
241
241
|
if (apiKey) {
|
|
242
242
|
const pathSegments = getPathSegments(req.url || "");
|
|
243
|
-
|
|
243
|
+
const isUserMcpRoute = req.url === "/mcp-user";
|
|
244
|
+
// `/mcp-user` runs its own `aswt_`-token auth in `handleMcpUser`; the swarm
|
|
245
|
+
// API key must not gate it.
|
|
246
|
+
if (!isUserMcpRoute && !isPublicRoute(req.method, pathSegments)) {
|
|
244
247
|
const authHeader = req.headers.authorization;
|
|
245
248
|
const providedKey = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
246
249
|
|
package/src/http/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { handleKv } from "./kv";
|
|
|
43
43
|
import { handleMcp } from "./mcp";
|
|
44
44
|
import { handleMcpOAuth, startMcpOAuthPendingGc, stopMcpOAuthPendingGc } from "./mcp-oauth";
|
|
45
45
|
import { handleMcpServers } from "./mcp-servers";
|
|
46
|
+
import { handleMcpUser } from "./mcp-user";
|
|
46
47
|
import { handleMemory } from "./memory";
|
|
47
48
|
import { handlePageProxy } from "./page-proxy";
|
|
48
49
|
import { handlePages } from "./pages";
|
|
@@ -89,6 +90,8 @@ const apiKey = getApiKey();
|
|
|
89
90
|
const globalState = globalThis as typeof globalThis & {
|
|
90
91
|
__httpServer?: Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
91
92
|
__transports?: Record<string, StreamableHTTPServerTransport>;
|
|
93
|
+
__transportsUser?: Record<string, StreamableHTTPServerTransport>;
|
|
94
|
+
__sessionUsers?: Record<string, string>;
|
|
92
95
|
__sigintRegistered?: boolean;
|
|
93
96
|
__runId?: string;
|
|
94
97
|
};
|
|
@@ -100,6 +103,9 @@ if (globalState.__httpServer) {
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
const transports: Record<string, StreamableHTTPServerTransport> = globalState.__transports ?? {};
|
|
106
|
+
const transportsUser: Record<string, StreamableHTTPServerTransport> =
|
|
107
|
+
globalState.__transportsUser ?? {};
|
|
108
|
+
const sessionUsers: Record<string, string> = globalState.__sessionUsers ?? {};
|
|
103
109
|
|
|
104
110
|
const httpServer = createHttpServer(async (req, res) => {
|
|
105
111
|
const startTime = performance.now();
|
|
@@ -234,6 +240,7 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
234
240
|
() => handleInboxState(req, res, pathSegments, queryParams),
|
|
235
241
|
() => handleTaskTemplates(req, res, pathSegments, queryParams),
|
|
236
242
|
() => handleMcp(req, res, transports),
|
|
243
|
+
() => handleMcpUser(req, res, transportsUser, sessionUsers),
|
|
237
244
|
];
|
|
238
245
|
|
|
239
246
|
try {
|
|
@@ -271,6 +278,8 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
271
278
|
// Store references in globalThis for hot reload persistence
|
|
272
279
|
globalState.__httpServer = httpServer;
|
|
273
280
|
globalState.__transports = transports;
|
|
281
|
+
globalState.__transportsUser = transportsUser;
|
|
282
|
+
globalState.__sessionUsers = sessionUsers;
|
|
274
283
|
|
|
275
284
|
async function shutdown() {
|
|
276
285
|
console.log("Shutting down HTTP server...");
|
|
@@ -303,6 +312,13 @@ async function shutdown() {
|
|
|
303
312
|
delete transports[id];
|
|
304
313
|
}
|
|
305
314
|
|
|
315
|
+
for (const [id, transport] of Object.entries(transportsUser)) {
|
|
316
|
+
console.log(`[HTTP] Closing user transport ${id}`);
|
|
317
|
+
transport.close();
|
|
318
|
+
delete transportsUser[id];
|
|
319
|
+
delete sessionUsers[id];
|
|
320
|
+
}
|
|
321
|
+
|
|
306
322
|
// Close all active connections forcefully
|
|
307
323
|
httpServer.closeAllConnections();
|
|
308
324
|
httpServer.close(() => {
|
package/src/http/integrations.ts
CHANGED
|
@@ -45,6 +45,20 @@ const claudeManagedTestRoute = route({
|
|
|
45
45
|
},
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
const mcpUserConfigRoute = route({
|
|
49
|
+
method: "get",
|
|
50
|
+
path: "/api/integrations/mcp-user/config",
|
|
51
|
+
pattern: ["api", "integrations", "mcp-user", "config"],
|
|
52
|
+
summary: "Get server-derived config for end-user MCP clients.",
|
|
53
|
+
tags: ["Integrations"],
|
|
54
|
+
responses: {
|
|
55
|
+
200: {
|
|
56
|
+
description:
|
|
57
|
+
"Server-derived MCP user config. `mcpBaseUrl` is the API server base URL and `mcpUserUrl` appends `/mcp-user`.",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
48
62
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
49
63
|
|
|
50
64
|
/**
|
|
@@ -73,6 +87,12 @@ function resolveConfigValue(key: string): string | null {
|
|
|
73
87
|
return null;
|
|
74
88
|
}
|
|
75
89
|
|
|
90
|
+
function resolveMcpBaseUrl(): string {
|
|
91
|
+
const configured = resolveConfigValue("MCP_BASE_URL");
|
|
92
|
+
const fallback = `http://localhost:${process.env.PORT || "3013"}`;
|
|
93
|
+
return (configured || fallback).replace(/\/+$/, "");
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
// ─── Public handler factory ──────────────────────────────────────────────────
|
|
77
97
|
|
|
78
98
|
/**
|
|
@@ -89,6 +109,12 @@ export function createIntegrationsHandler(deps: TestConnectionDeps = {}) {
|
|
|
89
109
|
res: ServerResponse,
|
|
90
110
|
pathSegments: string[],
|
|
91
111
|
): Promise<boolean> {
|
|
112
|
+
if (mcpUserConfigRoute.match(req.method, pathSegments)) {
|
|
113
|
+
const mcpBaseUrl = resolveMcpBaseUrl();
|
|
114
|
+
json(res, { mcpBaseUrl, mcpUserUrl: `${mcpBaseUrl}/mcp-user` });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
if (claudeManagedTestRoute.match(req.method, pathSegments)) {
|
|
93
119
|
const apiKey = resolveConfigValue("ANTHROPIC_API_KEY");
|
|
94
120
|
const agentId = resolveConfigValue("MANAGED_AGENT_ID");
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { resolveUserByToken } from "@/be/users";
|
|
6
|
+
import { createUserServer } from "@/server-user";
|
|
7
|
+
import type { User } from "@/types";
|
|
8
|
+
|
|
9
|
+
function unauthorized(res: ServerResponse): true {
|
|
10
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
11
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractBearer(req: IncomingMessage): string | null {
|
|
16
|
+
const header = req.headers.authorization;
|
|
17
|
+
if (!header?.startsWith("Bearer ")) return null;
|
|
18
|
+
const token = header.slice("Bearer ".length).trim();
|
|
19
|
+
return token.startsWith("aswt_") ? token : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveActiveUser(req: IncomingMessage): User | null {
|
|
23
|
+
const token = extractBearer(req);
|
|
24
|
+
if (!token) return null;
|
|
25
|
+
const user = resolveUserByToken(token);
|
|
26
|
+
if (!user || user.status !== "active") return null;
|
|
27
|
+
return user;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleMcpUser(
|
|
31
|
+
req: IncomingMessage,
|
|
32
|
+
res: ServerResponse,
|
|
33
|
+
transports: Record<string, StreamableHTTPServerTransport>,
|
|
34
|
+
sessionUsers: Record<string, string>,
|
|
35
|
+
): Promise<boolean> {
|
|
36
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
37
|
+
|
|
38
|
+
if (req.url !== "/mcp-user") {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const user = resolveActiveUser(req);
|
|
43
|
+
if (!user) return unauthorized(res);
|
|
44
|
+
|
|
45
|
+
if (sessionId && transports[sessionId] && sessionUsers[sessionId] !== user.id) {
|
|
46
|
+
return unauthorized(res);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (req.method === "POST") {
|
|
50
|
+
const chunks: Buffer[] = [];
|
|
51
|
+
for await (const chunk of req) {
|
|
52
|
+
chunks.push(chunk);
|
|
53
|
+
}
|
|
54
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
55
|
+
|
|
56
|
+
let transport: StreamableHTTPServerTransport;
|
|
57
|
+
|
|
58
|
+
if (sessionId && transports[sessionId]) {
|
|
59
|
+
transport = transports[sessionId];
|
|
60
|
+
} else if (!sessionId && isInitializeRequest(body)) {
|
|
61
|
+
transport = new StreamableHTTPServerTransport({
|
|
62
|
+
sessionIdGenerator: () => randomUUID(),
|
|
63
|
+
onsessioninitialized: (id) => {
|
|
64
|
+
transports[id] = transport;
|
|
65
|
+
sessionUsers[id] = user.id;
|
|
66
|
+
},
|
|
67
|
+
onsessionclosed: (id) => {
|
|
68
|
+
delete transports[id];
|
|
69
|
+
delete sessionUsers[id];
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
transport.onclose = () => {
|
|
74
|
+
if (transport.sessionId) {
|
|
75
|
+
delete transports[transport.sessionId];
|
|
76
|
+
delete sessionUsers[transport.sessionId];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const server = createUserServer(user);
|
|
81
|
+
await server.connect(transport);
|
|
82
|
+
} else {
|
|
83
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
84
|
+
res.end(
|
|
85
|
+
JSON.stringify({
|
|
86
|
+
jsonrpc: "2.0",
|
|
87
|
+
error: { code: -32000, message: "Invalid session" },
|
|
88
|
+
id: null,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await transport.handleRequest(req, res, body);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (req.method === "GET" || req.method === "DELETE") {
|
|
99
|
+
if (sessionId && transports[sessionId]) {
|
|
100
|
+
await transports[sessionId].handleRequest(req, res);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
res.writeHead(400);
|
|
104
|
+
res.end("Invalid session");
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
res.writeHead(405);
|
|
109
|
+
res.end("Method not allowed");
|
|
110
|
+
return true;
|
|
111
|
+
}
|