@desplega.ai/agent-swarm 1.92.1 → 1.93.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.
- package/openapi.json +63 -3
- package/package.json +5 -5
- package/src/be/db.ts +180 -6
- package/src/be/memory/boot-reembed.ts +84 -0
- package/src/be/memory/constants.ts +42 -1
- package/src/be/memory/providers/openai-embedding.ts +13 -0
- package/src/be/memory/providers/sqlite-store.ts +75 -26
- package/src/be/memory/raters/llm-client.ts +12 -5
- package/src/be/memory/reranker.ts +35 -17
- package/src/be/memory/types.ts +11 -0
- package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
- package/src/be/migrations/089_harness_variant.sql +2 -0
- package/src/be/modelsdev-cache.json +6478 -3099
- package/src/be/seed-pricing.ts +1 -0
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +371 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
- package/src/be/seed-scripts/index.ts +5 -5
- package/src/be/skill-sync.ts +28 -179
- package/src/commands/runner.ts +124 -7
- package/src/http/api-keys.ts +42 -0
- package/src/http/index.ts +9 -0
- package/src/http/mcp-bridge.ts +1 -1
- package/src/http/memory.ts +27 -24
- package/src/http/tasks.ts +10 -6
- package/src/providers/claude-adapter.ts +33 -1
- package/src/providers/claude-managed-adapter.ts +3 -0
- package/src/providers/claude-managed-models.ts +7 -0
- package/src/providers/codex-adapter.ts +8 -1
- package/src/providers/codex-models.ts +1 -0
- package/src/providers/codex-oauth/auth-json.ts +1 -0
- package/src/providers/harness-version.ts +7 -0
- package/src/providers/opencode-adapter.ts +11 -4
- package/src/providers/pi-mono-adapter.ts +12 -2
- package/src/providers/types.ts +2 -0
- package/src/scripts-runtime/egress-secrets.ts +83 -0
- package/src/scripts-runtime/eval-harness.ts +4 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/loader.ts +2 -0
- package/src/server-user.ts +2 -2
- package/src/slack/channel-join.ts +41 -0
- package/src/tasks/worker-follow-up.ts +12 -0
- package/src/tests/additive-buffer.test.ts +0 -1
- package/src/tests/api-key-tracking.test.ts +113 -0
- package/src/tests/approval-requests.test.ts +0 -6
- package/src/tests/claude-managed-setup.test.ts +0 -4
- package/src/tests/codex-pool.test.ts +2 -6
- package/src/tests/http-api-integration.test.ts +4 -6
- package/src/tests/memory-e2e.test.ts +6 -6
- package/src/tests/memory-edges.test.ts +0 -2
- package/src/tests/memory-rate-endpoint.test.ts +0 -2
- package/src/tests/memory-rater-e2e.test.ts +4 -7
- package/src/tests/memory-reranker.test.ts +135 -124
- package/src/tests/memory-store.test.ts +19 -1
- package/src/tests/memory.test.ts +64 -12
- package/src/tests/model-control.test.ts +1 -1
- package/src/tests/reload-config.test.ts +33 -17
- package/src/tests/runner-skills-refresh.test.ts +216 -46
- package/src/tests/script-runs-http.test.ts +7 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
- package/src/tests/seed-scripts.test.ts +218 -1
- package/src/tests/session-attach.test.ts +6 -6
- package/src/tests/skill-fs-writer.test.ts +250 -0
- package/src/tests/slack-attachments-block.test.ts +0 -1
- package/src/tests/slack-blocks.test.ts +0 -1
- package/src/tests/slack-channel-join.test.ts +80 -0
- package/src/tests/slack-identity-resolution.test.ts +0 -1
- package/src/tests/structured-output.test.ts +0 -2
- package/src/tests/task-cascade-fail.test.ts +304 -0
- package/src/tests/use-dismissible-card.test.ts +0 -4
- package/src/tools/schedules/create-schedule.ts +2 -2
- package/src/tools/schedules/update-schedule.ts +1 -1
- package/src/tools/send-task.ts +2 -2
- package/src/tools/slack-post.ts +18 -15
- package/src/tools/slack-read.ts +9 -11
- package/src/tools/slack-reply.ts +18 -15
- package/src/tools/slack-start-thread.ts +17 -14
- package/src/tools/task-action.ts +2 -2
- package/src/types.ts +11 -0
- package/src/utils/context-window.ts +3 -0
- package/src/utils/credentials.ts +22 -2
- package/src/utils/skill-fs-writer.ts +220 -0
- package/src/utils/skills-refresh.ts +123 -40
- package/templates/workflows/llm-safe-release-context/config.json +13 -0
- package/templates/workflows/llm-safe-release-context/content.md +69 -0
package/src/be/skill-sync.ts
CHANGED
|
@@ -6,85 +6,19 @@
|
|
|
6
6
|
* so Claude Code, Pi, and Codex discover them natively.
|
|
7
7
|
*
|
|
8
8
|
* This runs on the API side — workers call it via POST /api/skills/sync-filesystem.
|
|
9
|
+
* The actual FS write logic lives in the worker-safe src/utils/skill-fs-writer.ts
|
|
10
|
+
* so workers can also call it locally with their own homedir().
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import type { Dirent } from "node:fs";
|
|
12
|
-
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
type SkillFsEntry,
|
|
16
|
+
type SkillSyncResult,
|
|
17
|
+
writeSkillsToFilesystem,
|
|
18
|
+
} from "../utils/skill-fs-writer";
|
|
15
19
|
import { getAgentSkills, getSkillFiles } from "./db";
|
|
16
20
|
|
|
17
|
-
export
|
|
18
|
-
synced: number;
|
|
19
|
-
removed: number;
|
|
20
|
-
errors: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Marker file written into every swarm-managed skill directory. Cleanup
|
|
25
|
-
* only ever removes directories that contain this marker, so unrelated
|
|
26
|
-
* personal skills the user installed via the harness's own tooling (e.g.
|
|
27
|
-
* `codex skills add ...` writing into `~/.codex/skills/<name>/`) are left
|
|
28
|
-
* untouched even when the API server shares a HOME with the worker (local
|
|
29
|
-
* dev). See `~/.codex/skills` blast-radius note in PR #555.
|
|
30
|
-
*/
|
|
31
|
-
const SWARM_MARKER_FILE = ".swarm-managed";
|
|
32
|
-
|
|
33
|
-
function reconcileManagedSkillFiles(skillDir: string, currentRelativeFiles: Set<string>): number {
|
|
34
|
-
if (!existsSync(join(skillDir, SWARM_MARKER_FILE))) return 0;
|
|
35
|
-
|
|
36
|
-
let removed = 0;
|
|
37
|
-
|
|
38
|
-
const walk = (dir: string, relativeDir = ""): boolean => {
|
|
39
|
-
let entries: Dirent[];
|
|
40
|
-
try {
|
|
41
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
42
|
-
} catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let hasEntries = false;
|
|
47
|
-
for (const entry of entries) {
|
|
48
|
-
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
49
|
-
const fullPath = join(dir, entry.name);
|
|
50
|
-
|
|
51
|
-
if (entry.isDirectory()) {
|
|
52
|
-
const childHasEntries = walk(fullPath, relativePath);
|
|
53
|
-
if (!childHasEntries) {
|
|
54
|
-
try {
|
|
55
|
-
rmSync(fullPath, { recursive: true, force: true });
|
|
56
|
-
} catch {
|
|
57
|
-
hasEntries = true;
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
hasEntries = true;
|
|
61
|
-
}
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
relativePath === "SKILL.md" ||
|
|
67
|
-
relativePath === SWARM_MARKER_FILE ||
|
|
68
|
-
currentRelativeFiles.has(relativePath)
|
|
69
|
-
) {
|
|
70
|
-
hasEntries = true;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
rmSync(fullPath, { force: true });
|
|
76
|
-
removed++;
|
|
77
|
-
} catch {
|
|
78
|
-
hasEntries = true;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return hasEntries;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
walk(skillDir);
|
|
86
|
-
return removed;
|
|
87
|
-
}
|
|
21
|
+
export type { SkillSyncResult };
|
|
88
22
|
|
|
89
23
|
/**
|
|
90
24
|
* Sync agent's installed skills to the filesystem.
|
|
@@ -92,6 +26,9 @@ function reconcileManagedSkillFiles(skillDir: string, currentRelativeFiles: Set<
|
|
|
92
26
|
* For simple skills (content in DB): writes SKILL.md to ~/.claude/skills/<name>/
|
|
93
27
|
* For DB-backed complex skills: writes SKILL.md plus bundled skill_files rows.
|
|
94
28
|
* Legacy complex skills without skill_files remain handled by npx in entrypoint.
|
|
29
|
+
*
|
|
30
|
+
* API-side adapter: fetches skill data from DB, builds SkillFsEntry[], then
|
|
31
|
+
* delegates all FS writes to writeSkillsToFilesystem() from skill-fs-writer.ts.
|
|
95
32
|
*/
|
|
96
33
|
export function syncSkillsToFilesystem(
|
|
97
34
|
agentId: string,
|
|
@@ -100,112 +37,24 @@ export function syncSkillsToFilesystem(
|
|
|
100
37
|
): SkillSyncResult {
|
|
101
38
|
const skills = getAgentSkills(agentId);
|
|
102
39
|
const home = homeOverride ?? homedir();
|
|
103
|
-
const errors: string[] = [];
|
|
104
|
-
let synced = 0;
|
|
105
|
-
let removed = 0;
|
|
106
|
-
|
|
107
|
-
// Directories to write to
|
|
108
|
-
const skillDirs: string[] = [];
|
|
109
|
-
if (harnessType === "claude" || harnessType === "all") {
|
|
110
|
-
skillDirs.push(join(home, ".claude", "skills"));
|
|
111
|
-
}
|
|
112
|
-
if (harnessType === "pi" || harnessType === "all") {
|
|
113
|
-
skillDirs.push(join(home, ".pi", "agent", "skills"));
|
|
114
|
-
}
|
|
115
|
-
if (harnessType === "codex" || harnessType === "all") {
|
|
116
|
-
skillDirs.push(join(home, ".codex", "skills"));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Ensure base dirs exist
|
|
120
|
-
for (const dir of skillDirs) {
|
|
121
|
-
mkdirSync(dir, { recursive: true });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Track which skill names we write (for cleanup)
|
|
125
|
-
const writtenNames = new Set<string>();
|
|
126
|
-
|
|
127
|
-
for (const skill of skills) {
|
|
128
|
-
if (!skill.isActive || !skill.isEnabled) continue;
|
|
129
|
-
const bundledFiles = skill.isComplex ? getSkillFiles(skill.id) : [];
|
|
130
|
-
if (skill.isComplex && bundledFiles.length === 0) continue; // Legacy complex skills handled by npx
|
|
131
|
-
if (!skill.content) continue;
|
|
132
|
-
|
|
133
|
-
// Sanitize skill name to prevent path traversal (strip /, .., and non-safe chars)
|
|
134
|
-
const safeName = skill.name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
135
|
-
if (!safeName) continue;
|
|
136
|
-
|
|
137
|
-
writtenNames.add(safeName);
|
|
138
|
-
const currentBundledFilePaths = new Set(
|
|
139
|
-
bundledFiles.filter((file) => !file.isBinary).map((file) => file.path),
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
for (const baseDir of skillDirs) {
|
|
143
|
-
const skillDir = join(baseDir, safeName);
|
|
144
|
-
const skillFile = join(skillDir, "SKILL.md");
|
|
145
|
-
const markerFile = join(skillDir, SWARM_MARKER_FILE);
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
mkdirSync(skillDir, { recursive: true });
|
|
149
|
-
removed += reconcileManagedSkillFiles(skillDir, currentBundledFilePaths);
|
|
150
|
-
writeFileSync(skillFile, skill.content, "utf-8");
|
|
151
|
-
writeFileSync(markerFile, "", "utf-8");
|
|
152
|
-
synced++;
|
|
153
|
-
} catch (err) {
|
|
154
|
-
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
155
|
-
errors.push(`${skill.name} -> ${skillDir}: ${msg}`);
|
|
156
|
-
console.error(
|
|
157
|
-
`[skill-sync] Failed to write SKILL.md for ${skill.name} to ${skillDir}: ${msg}`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
for (const file of bundledFiles) {
|
|
162
|
-
if (file.isBinary) {
|
|
163
|
-
console.log(`[skill-sync] Skipping binary skill file ${skill.name}/${file.path}`);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const targetPath = join(skillDir, file.path);
|
|
168
|
-
try {
|
|
169
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
170
|
-
writeFileSync(targetPath, file.content, "utf-8");
|
|
171
|
-
} catch (err) {
|
|
172
|
-
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
173
|
-
errors.push(`${skill.name}/${file.path} -> ${targetPath}: ${msg}`);
|
|
174
|
-
console.error(
|
|
175
|
-
`[skill-sync] Failed to write bundled file ${skill.name}/${file.path} to ${targetPath}: ${msg}`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Cleanup: only remove directories WE previously created (marker file
|
|
183
|
-
// present). Leaves user-installed personal skills alone — important on
|
|
184
|
-
// local dev where ~/.codex/skills holds skills the user installed
|
|
185
|
-
// outside the swarm.
|
|
186
|
-
for (const baseDir of skillDirs) {
|
|
187
|
-
if (!existsSync(baseDir)) continue;
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const existing = readdirSync(baseDir, { withFileTypes: true });
|
|
191
|
-
for (const entry of existing) {
|
|
192
|
-
if (!entry.isDirectory()) continue;
|
|
193
|
-
if (writtenNames.has(entry.name)) continue;
|
|
194
|
-
const skillDir = join(baseDir, entry.name);
|
|
195
|
-
if (!existsSync(join(skillDir, SWARM_MARKER_FILE))) continue;
|
|
196
|
-
try {
|
|
197
|
-
rmSync(skillDir, { recursive: true, force: true });
|
|
198
|
-
removed++;
|
|
199
|
-
} catch {
|
|
200
|
-
// Non-fatal — skip cleanup errors
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
} catch {
|
|
204
|
-
// Non-fatal — skip if we can't read the directory
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
40
|
|
|
208
|
-
|
|
41
|
+
const entries: SkillFsEntry[] = skills.map((skill) => ({
|
|
42
|
+
id: skill.id,
|
|
43
|
+
name: skill.name,
|
|
44
|
+
content: skill.content ?? null,
|
|
45
|
+
isComplex: skill.isComplex,
|
|
46
|
+
isEnabled: skill.isEnabled,
|
|
47
|
+
isActive: skill.isActive,
|
|
48
|
+
files: skill.isComplex
|
|
49
|
+
? getSkillFiles(skill.id).map((f) => ({
|
|
50
|
+
path: f.path,
|
|
51
|
+
content: f.content,
|
|
52
|
+
isBinary: f.isBinary,
|
|
53
|
+
}))
|
|
54
|
+
: [],
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
return writeSkillsToFilesystem(entries, harnessType, home);
|
|
209
58
|
}
|
|
210
59
|
|
|
211
60
|
export interface SkillsSignature {
|
package/src/commands/runner.ts
CHANGED
|
@@ -1110,6 +1110,35 @@ async function reportKeyRateLimit(
|
|
|
1110
1110
|
}
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
|
+
/** Clear a stale rate-limit record after a successful task (fire-and-forget) */
|
|
1114
|
+
async function reportKeyClearRateLimit(
|
|
1115
|
+
apiUrl: string,
|
|
1116
|
+
apiKey: string,
|
|
1117
|
+
keyType: string,
|
|
1118
|
+
keySuffix: string,
|
|
1119
|
+
): Promise<void> {
|
|
1120
|
+
try {
|
|
1121
|
+
const resp = await fetch(`${apiUrl}/api/keys/clear-rate-limit`, {
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
headers: {
|
|
1124
|
+
"Content-Type": "application/json",
|
|
1125
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1126
|
+
},
|
|
1127
|
+
body: JSON.stringify({ keyType, keySuffix }),
|
|
1128
|
+
});
|
|
1129
|
+
if (resp.ok) {
|
|
1130
|
+
const data = (await resp.json()) as { cleared?: boolean };
|
|
1131
|
+
if (data.cleared) {
|
|
1132
|
+
console.log(
|
|
1133
|
+
`[credentials] Cleared stale rate-limit for ...${keySuffix} after successful task`,
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
} catch {
|
|
1138
|
+
// Non-blocking
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1113
1142
|
/**
|
|
1114
1143
|
* Supersede a task via the API (for graceful shutdown / context-limit /
|
|
1115
1144
|
* operator-triggered). Returns `{ ok: true, resumeTaskId }` on success.
|
|
@@ -1472,6 +1501,8 @@ interface RunningTask {
|
|
|
1472
1501
|
* provider before it completed, and vice versa).
|
|
1473
1502
|
*/
|
|
1474
1503
|
hasLocalEnvironment: boolean;
|
|
1504
|
+
/** Harness variant captured on session_init (e.g. "bridge" or "stock") */
|
|
1505
|
+
harnessVariant?: string;
|
|
1475
1506
|
}
|
|
1476
1507
|
|
|
1477
1508
|
/** Runner state for tracking concurrent tasks */
|
|
@@ -1590,6 +1621,8 @@ async function saveProviderSessionId(
|
|
|
1590
1621
|
provider?: ProviderName,
|
|
1591
1622
|
providerMeta?: Record<string, unknown>,
|
|
1592
1623
|
model?: string,
|
|
1624
|
+
harnessVariant?: string,
|
|
1625
|
+
harnessVariantMeta?: Record<string, unknown>,
|
|
1593
1626
|
): Promise<void> {
|
|
1594
1627
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1595
1628
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
@@ -1597,13 +1630,45 @@ async function saveProviderSessionId(
|
|
|
1597
1630
|
if (provider !== undefined) body.provider = provider;
|
|
1598
1631
|
if (providerMeta !== undefined) body.providerMeta = providerMeta;
|
|
1599
1632
|
if (model !== undefined && model !== "") body.model = model;
|
|
1600
|
-
|
|
1633
|
+
if (harnessVariant !== undefined) body.harnessVariant = harnessVariant;
|
|
1634
|
+
if (harnessVariantMeta !== undefined) body.harnessVariantMeta = harnessVariantMeta;
|
|
1635
|
+
await fetch(`${apiUrl}/api/tasks/${taskId}/session`, {
|
|
1601
1636
|
method: "PUT",
|
|
1602
1637
|
headers,
|
|
1603
1638
|
body: JSON.stringify(body),
|
|
1604
1639
|
});
|
|
1605
1640
|
}
|
|
1606
1641
|
|
|
1642
|
+
async function findBridgeFailureArtifact(cwd: string): Promise<string | undefined> {
|
|
1643
|
+
try {
|
|
1644
|
+
const bridgeDir = `${cwd}/.claude-bridge/runs`;
|
|
1645
|
+
const dir = await Array.fromAsync(
|
|
1646
|
+
new Bun.Glob("*/tmux-pane-final.txt").scan({ cwd: bridgeDir, absolute: true }),
|
|
1647
|
+
);
|
|
1648
|
+
if (dir.length === 0) return undefined;
|
|
1649
|
+
dir.sort();
|
|
1650
|
+
return dir[dir.length - 1];
|
|
1651
|
+
} catch {
|
|
1652
|
+
return undefined;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
async function updateHarnessVariantMeta(
|
|
1657
|
+
apiUrl: string,
|
|
1658
|
+
apiKey: string,
|
|
1659
|
+
taskId: string,
|
|
1660
|
+
claudeSessionId: string,
|
|
1661
|
+
meta: Record<string, unknown>,
|
|
1662
|
+
): Promise<void> {
|
|
1663
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1664
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
1665
|
+
await fetch(`${apiUrl}/api/tasks/${taskId}/session`, {
|
|
1666
|
+
method: "PUT",
|
|
1667
|
+
headers,
|
|
1668
|
+
body: JSON.stringify({ claudeSessionId, harnessVariantMeta: meta }),
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1607
1672
|
/** Cache of tasks that already have VCS linked — prevents repeated gh pr list calls */
|
|
1608
1673
|
const vcsDetectedTasks = new Set<string>();
|
|
1609
1674
|
|
|
@@ -2493,10 +2558,19 @@ async function spawnProviderProcess(
|
|
|
2493
2558
|
// Resolve Codex OAuth pool slot BEFORE building ProviderSessionConfig so we
|
|
2494
2559
|
// can pass codexSlot through and the adapter writes token refreshes back to
|
|
2495
2560
|
// the correct slot key (codex_oauth_<slot>) instead of defaulting to slot 0.
|
|
2561
|
+
//
|
|
2562
|
+
// Always resolve for codex (not just when credentialSelections is empty) so
|
|
2563
|
+
// that if the OPENAI_API_KEY credential is rate-limited we can fail over to
|
|
2564
|
+
// a CODEX_OAUTH slot — even though the keyType differs.
|
|
2496
2565
|
let oauthSelection: CredentialSelection | undefined;
|
|
2497
|
-
if (adapter.name === "codex"
|
|
2566
|
+
if (adapter.name === "codex") {
|
|
2498
2567
|
oauthSelection = (await resolveCodexOAuthCredentialInfo(opts.apiUrl, opts.apiKey)) ?? undefined;
|
|
2499
|
-
|
|
2568
|
+
const oauthIsPrimary =
|
|
2569
|
+
credentialSelections.length === 0 ||
|
|
2570
|
+
(credentialSelections[0]?.isRateLimitFallback &&
|
|
2571
|
+
oauthSelection &&
|
|
2572
|
+
!oauthSelection.isRateLimitFallback);
|
|
2573
|
+
if (oauthSelection && realTaskId && oauthIsPrimary) {
|
|
2500
2574
|
reportKeyUsage(
|
|
2501
2575
|
opts.apiUrl,
|
|
2502
2576
|
opts.apiKey,
|
|
@@ -2676,6 +2750,8 @@ async function spawnProviderProcess(
|
|
|
2676
2750
|
event.provider,
|
|
2677
2751
|
event.providerMeta,
|
|
2678
2752
|
model,
|
|
2753
|
+
event.harnessVariant,
|
|
2754
|
+
event.harnessVariantMeta,
|
|
2679
2755
|
).catch((err) => console.warn(`[runner] Failed to save session ID: ${err}`));
|
|
2680
2756
|
} else {
|
|
2681
2757
|
// Pool task: save provider session ID on active session so it can be
|
|
@@ -3085,8 +3161,23 @@ async function spawnProviderProcess(
|
|
|
3085
3161
|
}),
|
|
3086
3162
|
);
|
|
3087
3163
|
|
|
3088
|
-
// Build credential info for rate limit tracking
|
|
3089
|
-
|
|
3164
|
+
// Build credential info for rate limit tracking.
|
|
3165
|
+
// For codex: when OPENAI_API_KEY is rate-limited but CODEX_OAUTH has
|
|
3166
|
+
// available slots (or vice versa), prefer the healthy credential.
|
|
3167
|
+
let primarySelection: CredentialSelection | undefined;
|
|
3168
|
+
const firstCred = credentialSelections[0];
|
|
3169
|
+
if (firstCred && oauthSelection) {
|
|
3170
|
+
if (firstCred.isRateLimitFallback && !oauthSelection.isRateLimitFallback) {
|
|
3171
|
+
primarySelection = oauthSelection;
|
|
3172
|
+
console.log(
|
|
3173
|
+
`[credentials] Cross-keyType failover: ${firstCred.keyType} all rate-limited, using ${oauthSelection.keyType} [...${oauthSelection.keySuffix}]`,
|
|
3174
|
+
);
|
|
3175
|
+
} else {
|
|
3176
|
+
primarySelection = firstCred;
|
|
3177
|
+
}
|
|
3178
|
+
} else {
|
|
3179
|
+
primarySelection = firstCred ?? oauthSelection;
|
|
3180
|
+
}
|
|
3090
3181
|
const credentialInfo = primarySelection
|
|
3091
3182
|
? {
|
|
3092
3183
|
keyType: primarySelection.keyType,
|
|
@@ -3160,7 +3251,14 @@ async function checkCompletedProcesses(
|
|
|
3160
3251
|
}
|
|
3161
3252
|
|
|
3162
3253
|
// Remove completed tasks from the map and ensure they're marked as finished
|
|
3163
|
-
for (const {
|
|
3254
|
+
for (const {
|
|
3255
|
+
taskId,
|
|
3256
|
+
result,
|
|
3257
|
+
cursorUpdates,
|
|
3258
|
+
workingDir,
|
|
3259
|
+
credentialInfo,
|
|
3260
|
+
harnessProvider,
|
|
3261
|
+
} of completedTasks) {
|
|
3164
3262
|
state.activeTasks.delete(taskId);
|
|
3165
3263
|
vcsDetectedTasks.delete(taskId);
|
|
3166
3264
|
vcsCheckTimestamps.delete(taskId);
|
|
@@ -3251,9 +3349,28 @@ async function checkCompletedProcesses(
|
|
|
3251
3349
|
result.exitCode,
|
|
3252
3350
|
failureReason,
|
|
3253
3351
|
result.output,
|
|
3254
|
-
|
|
3352
|
+
harnessProvider,
|
|
3255
3353
|
);
|
|
3256
3354
|
|
|
3355
|
+
if (result.exitCode === 0 && credentialInfo) {
|
|
3356
|
+
reportKeyClearRateLimit(
|
|
3357
|
+
apiConfig.apiUrl,
|
|
3358
|
+
apiConfig.apiKey,
|
|
3359
|
+
credentialInfo.keyType,
|
|
3360
|
+
credentialInfo.keySuffix,
|
|
3361
|
+
).catch(() => {});
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
if (result.exitCode !== 0 && harnessProvider === "claude" && workingDir && result.sessionId) {
|
|
3365
|
+
const artifactPath = await findBridgeFailureArtifact(workingDir);
|
|
3366
|
+
if (artifactPath) {
|
|
3367
|
+
console.log(`[${role}] Bridge failure artifact found: ${artifactPath}`);
|
|
3368
|
+
updateHarnessVariantMeta(apiConfig.apiUrl, apiConfig.apiKey, taskId, result.sessionId, {
|
|
3369
|
+
failureArtifact: artifactPath,
|
|
3370
|
+
}).catch((err) => console.warn(`[runner] Failed to update harness variant meta: ${err}`));
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3257
3374
|
ensure({
|
|
3258
3375
|
id: "worker_process_finished",
|
|
3259
3376
|
flow: "task",
|
package/src/http/api-keys.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
|
+
clearKeyRateLimit,
|
|
4
5
|
getAvailableKeyIndices,
|
|
5
6
|
getKeyCostSummary,
|
|
6
7
|
getKeyStatuses,
|
|
@@ -134,6 +135,26 @@ const setKeyName = route({
|
|
|
134
135
|
auth: { apiKey: true },
|
|
135
136
|
});
|
|
136
137
|
|
|
138
|
+
const clearRateLimitRoute = route({
|
|
139
|
+
method: "post",
|
|
140
|
+
path: "/api/keys/clear-rate-limit",
|
|
141
|
+
pattern: ["api", "keys", "clear-rate-limit"],
|
|
142
|
+
summary: "Clear rate-limited status for a key after a successful use proves it is healthy",
|
|
143
|
+
tags: ["API Keys"],
|
|
144
|
+
body: z.object({
|
|
145
|
+
keyType: z.string(),
|
|
146
|
+
keySuffix: z.string().min(1).max(10),
|
|
147
|
+
scope: z.string().optional(),
|
|
148
|
+
scopeId: z.string().optional(),
|
|
149
|
+
}),
|
|
150
|
+
responses: {
|
|
151
|
+
200: { description: "Rate limit cleared (or key was not rate-limited)" },
|
|
152
|
+
400: { description: "Validation error" },
|
|
153
|
+
401: { description: "Unauthorized" },
|
|
154
|
+
},
|
|
155
|
+
auth: { apiKey: true },
|
|
156
|
+
});
|
|
157
|
+
|
|
137
158
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
138
159
|
|
|
139
160
|
export async function handleApiKeys(
|
|
@@ -242,5 +263,26 @@ export async function handleApiKeys(
|
|
|
242
263
|
return true;
|
|
243
264
|
}
|
|
244
265
|
|
|
266
|
+
// POST /api/keys/clear-rate-limit
|
|
267
|
+
if (clearRateLimitRoute.match(req.method, pathSegments)) {
|
|
268
|
+
const parsed = await clearRateLimitRoute.parse(req, res, pathSegments, queryParams);
|
|
269
|
+
if (!parsed) return true;
|
|
270
|
+
|
|
271
|
+
const { keyType, keySuffix, scope, scopeId } = parsed.body;
|
|
272
|
+
try {
|
|
273
|
+
const cleared = clearKeyRateLimit(keyType, keySuffix, scope, scopeId ?? null);
|
|
274
|
+
json(res, {
|
|
275
|
+
success: true,
|
|
276
|
+
cleared,
|
|
277
|
+
message: cleared
|
|
278
|
+
? `Rate limit cleared for ...${keySuffix}`
|
|
279
|
+
: `Key ...${keySuffix} was not rate-limited`,
|
|
280
|
+
});
|
|
281
|
+
} catch (err) {
|
|
282
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to clear rate limit", 500);
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
245
287
|
return false;
|
|
246
288
|
}
|
package/src/http/index.ts
CHANGED
|
@@ -556,6 +556,15 @@ httpServer
|
|
|
556
556
|
|
|
557
557
|
// Start expired-memory garbage collector (1-hour tick, immediate first run)
|
|
558
558
|
startMemoryGc();
|
|
559
|
+
|
|
560
|
+
// Background backfill: re-embed any agent_memory rows with wrong-dimension
|
|
561
|
+
// embeddings (e.g. 1536d instead of 512d). Non-blocking, idempotent, no-op
|
|
562
|
+
// when the DB is clean. See src/be/memory/boot-reembed.ts.
|
|
563
|
+
import("../be/memory/boot-reembed")
|
|
564
|
+
.then(({ runBootReembed }) => runBootReembed())
|
|
565
|
+
.catch((err) => {
|
|
566
|
+
console.error("[boot-reembed] startup backfill failed (non-fatal):", err);
|
|
567
|
+
});
|
|
559
568
|
})
|
|
560
569
|
.on("error", (err) => {
|
|
561
570
|
console.error("HTTP Server Error:", err);
|
package/src/http/mcp-bridge.ts
CHANGED
package/src/http/memory.ts
CHANGED
|
@@ -387,6 +387,8 @@ export async function handleMemory(
|
|
|
387
387
|
name: r.name,
|
|
388
388
|
content: r.content,
|
|
389
389
|
similarity: r.similarity,
|
|
390
|
+
rawSimilarity: r.rawSimilarity,
|
|
391
|
+
compositeScore: r.compositeScore,
|
|
390
392
|
source: r.source,
|
|
391
393
|
scope: r.scope,
|
|
392
394
|
})),
|
|
@@ -404,6 +406,7 @@ export async function handleMemory(
|
|
|
404
406
|
|
|
405
407
|
const { query, agentId, scope, source, sourcePath, limit, offset } = parsed.body;
|
|
406
408
|
const store = getMemoryStore();
|
|
409
|
+
const pageLimit = Math.min(limit, 100);
|
|
407
410
|
const pathNeedle = sourcePath?.trim().toLowerCase();
|
|
408
411
|
const matchesPath = (p: string | null) =>
|
|
409
412
|
!pathNeedle || (p?.toLowerCase().includes(pathNeedle) ?? false);
|
|
@@ -414,11 +417,14 @@ export async function handleMemory(
|
|
|
414
417
|
const queryEmbedding = await provider.embed(query.trim());
|
|
415
418
|
|
|
416
419
|
if (!queryEmbedding) {
|
|
417
|
-
json(res, { results: [], total: 0, mode: "semantic" });
|
|
420
|
+
json(res, { results: [], total: 0, limit: pageLimit, offset, mode: "semantic" });
|
|
418
421
|
return true;
|
|
419
422
|
}
|
|
420
423
|
|
|
421
|
-
const candidateLimit = Math.min(
|
|
424
|
+
const candidateLimit = Math.min(
|
|
425
|
+
4096,
|
|
426
|
+
Math.max(offset + pageLimit, pageLimit) * CANDIDATE_SET_MULTIPLIER,
|
|
427
|
+
);
|
|
422
428
|
let candidates = store.search(queryEmbedding, agentId ?? "", {
|
|
423
429
|
scope,
|
|
424
430
|
limit: candidateLimit,
|
|
@@ -431,10 +437,11 @@ export async function handleMemory(
|
|
|
431
437
|
if (pathNeedle) {
|
|
432
438
|
candidates = candidates.filter((c) => matchesPath(c.sourcePath));
|
|
433
439
|
}
|
|
434
|
-
const ranked = rerank(candidates, { limit:
|
|
440
|
+
const ranked = rerank(candidates, { limit: candidates.length });
|
|
441
|
+
const page = ranked.slice(offset, offset + pageLimit);
|
|
435
442
|
|
|
436
443
|
json(res, {
|
|
437
|
-
results:
|
|
444
|
+
results: page.map((r) => ({
|
|
438
445
|
id: r.id,
|
|
439
446
|
name: r.name,
|
|
440
447
|
content: r.content,
|
|
@@ -442,6 +449,8 @@ export async function handleMemory(
|
|
|
442
449
|
scope: r.scope,
|
|
443
450
|
source: r.source,
|
|
444
451
|
similarity: r.similarity,
|
|
452
|
+
rawSimilarity: r.rawSimilarity,
|
|
453
|
+
compositeScore: r.compositeScore,
|
|
445
454
|
createdAt: r.createdAt,
|
|
446
455
|
accessedAt: r.accessedAt,
|
|
447
456
|
accessCount: r.accessCount ?? 0,
|
|
@@ -453,33 +462,25 @@ export async function handleMemory(
|
|
|
453
462
|
totalChunks: r.totalChunks,
|
|
454
463
|
tags: r.tags,
|
|
455
464
|
})),
|
|
456
|
-
total:
|
|
465
|
+
total: candidates.length,
|
|
466
|
+
limit: pageLimit,
|
|
467
|
+
offset,
|
|
457
468
|
mode: "semantic",
|
|
458
469
|
});
|
|
459
470
|
return true;
|
|
460
471
|
}
|
|
461
472
|
|
|
462
|
-
|
|
463
|
-
// page isn't gutted by the in-memory filter.
|
|
464
|
-
const fetchLimit = pathNeedle
|
|
465
|
-
? Math.min(500, Math.max(limit * 10, 100))
|
|
466
|
-
: Math.min(limit, 100);
|
|
467
|
-
let rows = store.list(agentId ?? "", {
|
|
473
|
+
const listOptions = {
|
|
468
474
|
scope,
|
|
469
|
-
limit:
|
|
475
|
+
limit: pageLimit,
|
|
470
476
|
offset,
|
|
471
477
|
isLead: true,
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
if (pathNeedle) {
|
|
480
|
-
rows = rows.filter((r) => matchesPath(r.sourcePath));
|
|
481
|
-
}
|
|
482
|
-
rows = rows.slice(0, Math.min(limit, 100));
|
|
478
|
+
ownerAgentId: agentId,
|
|
479
|
+
source,
|
|
480
|
+
sourcePath: pathNeedle,
|
|
481
|
+
};
|
|
482
|
+
const rows = store.list(agentId ?? "", listOptions);
|
|
483
|
+
const total = store.count(agentId ?? "", listOptions);
|
|
483
484
|
|
|
484
485
|
json(res, {
|
|
485
486
|
results: rows.map((r) => ({
|
|
@@ -500,7 +501,9 @@ export async function handleMemory(
|
|
|
500
501
|
totalChunks: r.totalChunks,
|
|
501
502
|
tags: r.tags,
|
|
502
503
|
})),
|
|
503
|
-
total
|
|
504
|
+
total,
|
|
505
|
+
limit: pageLimit,
|
|
506
|
+
offset,
|
|
504
507
|
mode: "list",
|
|
505
508
|
});
|
|
506
509
|
} catch (err) {
|
package/src/http/tasks.ts
CHANGED
|
@@ -98,11 +98,11 @@ const createTask = route({
|
|
|
98
98
|
},
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
const
|
|
101
|
+
const updateSession = route({
|
|
102
102
|
method: "put",
|
|
103
|
-
path: "/api/tasks/{id}/
|
|
104
|
-
pattern: ["api", "tasks", null, "
|
|
105
|
-
summary: "Update
|
|
103
|
+
path: "/api/tasks/{id}/session",
|
|
104
|
+
pattern: ["api", "tasks", null, "session"],
|
|
105
|
+
summary: "Update provider session ID and harness metadata for a task",
|
|
106
106
|
tags: ["Tasks"],
|
|
107
107
|
params: z.object({ id: z.string() }),
|
|
108
108
|
body: z.union([
|
|
@@ -121,6 +121,8 @@ const updateClaudeSession = route({
|
|
|
121
121
|
provider: ProviderNameSchema.exclude(["devin"]).optional(),
|
|
122
122
|
model: z.string().optional(),
|
|
123
123
|
providerMeta: z.object({}).optional(),
|
|
124
|
+
harnessVariant: z.string().optional(),
|
|
125
|
+
harnessVariantMeta: z.record(z.string(), z.unknown()).optional(),
|
|
124
126
|
}),
|
|
125
127
|
]),
|
|
126
128
|
responses: {
|
|
@@ -427,8 +429,8 @@ export async function handleTasks(
|
|
|
427
429
|
return true;
|
|
428
430
|
}
|
|
429
431
|
|
|
430
|
-
if (
|
|
431
|
-
const parsed = await
|
|
432
|
+
if (updateSession.match(req.method, pathSegments)) {
|
|
433
|
+
const parsed = await updateSession.parse(req, res, pathSegments, queryParams);
|
|
432
434
|
if (!parsed) return true;
|
|
433
435
|
const task = updateTaskClaudeSessionId(
|
|
434
436
|
parsed.params.id,
|
|
@@ -436,6 +438,8 @@ export async function handleTasks(
|
|
|
436
438
|
parsed.body.provider,
|
|
437
439
|
parsed.body.providerMeta,
|
|
438
440
|
parsed.body.model,
|
|
441
|
+
"harnessVariant" in parsed.body ? parsed.body.harnessVariant : undefined,
|
|
442
|
+
"harnessVariantMeta" in parsed.body ? parsed.body.harnessVariantMeta : undefined,
|
|
439
443
|
);
|
|
440
444
|
if (!task) {
|
|
441
445
|
jsonError(res, "Task not found", 404);
|